Merge pull request #898 from matkoniecz/feature/display-of-planning-data
Feature/display of planning data
This commit is contained in:
commit
df1da50a4a
@ -5,8 +5,6 @@
|
|||||||
</Rule>
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<LineSymbolizer stroke="#222222aa" stroke-width="0.5" />
|
<LineSymbolizer stroke="#222222aa" stroke-width="0.5" />
|
||||||
<MaxScaleDenominator>17000</MaxScaleDenominator>
|
|
||||||
<MinScaleDenominator>0</MinScaleDenominator>
|
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
<Style name="base_night">
|
<Style name="base_night">
|
||||||
@ -19,6 +17,61 @@
|
|||||||
<MinScaleDenominator>0</MinScaleDenominator>
|
<MinScaleDenominator>0</MinScaleDenominator>
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style name="base_night_outlines">
|
||||||
|
<Rule>
|
||||||
|
<LineSymbolizer stroke="#ff0000ff" stroke-width="1" />
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
|
<Style name="base_boroughs">
|
||||||
|
<Rule>
|
||||||
|
<TextSymbolizer
|
||||||
|
face-name="DejaVu Sans Condensed Bold"
|
||||||
|
fill="#eee"
|
||||||
|
size="24"
|
||||||
|
halo-radius="2"
|
||||||
|
halo-fill="#333"
|
||||||
|
wrap-width="40"
|
||||||
|
clip="false"
|
||||||
|
margin="20"
|
||||||
|
>
|
||||||
|
[name]
|
||||||
|
</TextSymbolizer>
|
||||||
|
<MinScaleDenominator>10000</MinScaleDenominator>
|
||||||
|
<MaxScaleDenominator>20000</MaxScaleDenominator>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<TextSymbolizer
|
||||||
|
face-name="DejaVu Sans Condensed Bold"
|
||||||
|
fill="#eee"
|
||||||
|
size="21"
|
||||||
|
halo-radius="2"
|
||||||
|
halo-fill="#333"
|
||||||
|
wrap-width="35"
|
||||||
|
clip="false"
|
||||||
|
margin="20"
|
||||||
|
>
|
||||||
|
[name]
|
||||||
|
</TextSymbolizer>
|
||||||
|
<MinScaleDenominator>20000</MinScaleDenominator>
|
||||||
|
<MaxScaleDenominator>80000</MaxScaleDenominator>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<TextSymbolizer
|
||||||
|
face-name="DejaVu Sans Condensed Bold"
|
||||||
|
fill="#eee"
|
||||||
|
size="15"
|
||||||
|
halo-radius="2"
|
||||||
|
halo-fill="#333"
|
||||||
|
wrap-width="35"
|
||||||
|
clip="false"
|
||||||
|
margin="20"
|
||||||
|
>
|
||||||
|
[name]
|
||||||
|
</TextSymbolizer>
|
||||||
|
<MinScaleDenominator>80000</MinScaleDenominator>
|
||||||
|
<MaxScaleDenominator>400000</MaxScaleDenominator>
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
<Style name="number_labels">
|
<Style name="number_labels">
|
||||||
<Rule>
|
<Rule>
|
||||||
<TextSymbolizer
|
<TextSymbolizer
|
||||||
@ -308,11 +361,114 @@
|
|||||||
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style name="empty_map">
|
||||||
|
</Style>
|
||||||
<Style name="conservation_area">
|
<Style name="conservation_area">
|
||||||
<Rule>
|
<Rule>
|
||||||
<PolygonSymbolizer fill="#73ebaf" />
|
<PolygonSymbolizer fill="#73ebaf" />
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style name="planning_applications_status_all">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Submitted"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#a040a0"/>
|
||||||
|
<LineSymbolizer stroke="#a040a0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Approved"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#16cf15"/>
|
||||||
|
<LineSymbolizer stroke="#16cf15" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Appeal In Progress"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fff200"/>
|
||||||
|
<LineSymbolizer stroke="#fff200" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Rejected"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#e31d23"/>
|
||||||
|
<LineSymbolizer stroke="#e31d23" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Withdrawn"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#999999"/>
|
||||||
|
<LineSymbolizer stroke="#999999" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Withdrawn"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#262626"/>
|
||||||
|
<LineSymbolizer stroke="#262626" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] != "Submitted" and [status] != "Approved" and [status] != "Appeal In Progress" and [status] != "Rejected" and [status] != "Withdrawn"</Filter>
|
||||||
|
<PolygonSymbolizer fill="#eacad0"/>
|
||||||
|
<LineSymbolizer stroke="#eacad0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
|
<Style name="planning_applications_status_recent">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Submitted" and [days_since_registered_with_local_authority_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#a040a0"/>
|
||||||
|
<LineSymbolizer stroke="#a040a0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Approved" and [days_since_decision_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#16cf15"/>
|
||||||
|
<LineSymbolizer stroke="#16cf15" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Appeal In Progress" and [days_since_decision_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fff200"/>
|
||||||
|
<LineSymbolizer stroke="#fff200" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Rejected" and [days_since_decision_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#e31d23"/>
|
||||||
|
<LineSymbolizer stroke="#e31d23" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Withdrawn" and [days_since_registered_with_local_authority_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#999999"/>
|
||||||
|
<LineSymbolizer stroke="#999999" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] != "Submitted" and [status] != "Approved" and [status] != "Appeal In Progress" and [status] != "Rejected" and [status] != "Withdrawn" and [days_since_registered_with_local_authority_date] < 366</Filter>
|
||||||
|
<PolygonSymbolizer fill="#eacad0"/>
|
||||||
|
<LineSymbolizer stroke="#eacad0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
|
<Style name="planning_applications_status_very_recent">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Submitted" and [days_since_registered_with_local_authority_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#a040a0"/>
|
||||||
|
<LineSymbolizer stroke="#a040a0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Approved" and [days_since_decision_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#16cf15"/>
|
||||||
|
<LineSymbolizer stroke="#16cf15" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Appeal In Progress" and [days_since_decision_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fff200"/>
|
||||||
|
<LineSymbolizer stroke="#fff200" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Rejected" and [days_since_decision_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#e31d23"/>
|
||||||
|
<LineSymbolizer stroke="#e31d23" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] = "Withdrawn" and [days_since_registered_with_local_authority_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#999999"/>
|
||||||
|
<LineSymbolizer stroke="#999999" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[status] != "Submitted" and [status] != "Approved" and [status] != "Appeal In Progress" and [status] != "Rejected" and [status] != "Withdrawn" and [days_since_registered_with_local_authority_date] <= 30</Filter>
|
||||||
|
<PolygonSymbolizer fill="#eacad0"/>
|
||||||
|
<LineSymbolizer stroke="#eacad0" stroke-width="1.75" />
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
<Style name="planning_combined">
|
<Style name="planning_combined">
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[planning_in_conservation_area] = true</Filter>
|
<Filter>[planning_in_conservation_area] = true</Filter>
|
||||||
@ -597,7 +753,7 @@
|
|||||||
<Style name="landuse">
|
<Style name="landuse">
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[current_landuse_order] = "Agriculture And Fisheries"</Filter>
|
<Filter>[current_landuse_order] = "Agriculture And Fisheries"</Filter>
|
||||||
<PolygonSymbolizer fill="#fa667d" />
|
<PolygonSymbolizer fill="#73ccd1" />
|
||||||
</Rule>
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[current_landuse_order] = "Minerals"</Filter>
|
<Filter>[current_landuse_order] = "Minerals"</Filter>
|
||||||
@ -637,7 +793,7 @@
|
|||||||
</Rule>
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[current_landuse_order] = "Community Services"</Filter>
|
<Filter>[current_landuse_order] = "Community Services"</Filter>
|
||||||
<PolygonSymbolizer fill="#73ccd1" />
|
<PolygonSymbolizer fill="#fa667d" />
|
||||||
</Rule>
|
</Rule>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[current_landuse_order] = "Retail"</Filter>
|
<Filter>[current_landuse_order] = "Retail"</Filter>
|
||||||
|
40
app/public/geometries/boroughs.geojson
Normal file
40
app/public/geometries/boroughs.geojson
Normal file
File diff suppressed because one or more lines are too long
2027
app/public/geometries/conservation_areas.geojson
Normal file
2027
app/public/geometries/conservation_areas.geojson
Normal file
File diff suppressed because one or more lines are too long
6910
app/public/geometries/creative_enterprise_zones.geojson
Normal file
6910
app/public/geometries/creative_enterprise_zones.geojson
Normal file
File diff suppressed because it is too large
Load Diff
1
app/public/geometries/flood_zones_simplified.geojson
Normal file
1
app/public/geometries/flood_zones_simplified.geojson
Normal file
File diff suppressed because one or more lines are too long
1
app/public/geometries/housing_zones.geojson
Normal file
1
app/public/geometries/housing_zones.geojson
Normal file
File diff suppressed because one or more lines are too long
3754
app/public/geometries/parcels_city_of_london.geojson
Normal file
3754
app/public/geometries/parcels_city_of_london.geojson
Normal file
File diff suppressed because one or more lines are too long
64
app/public/geometries/protected_vistas.geojson
Normal file
64
app/public/geometries/protected_vistas.geojson
Normal file
File diff suppressed because one or more lines are too long
@ -173,11 +173,15 @@ export const buildingAttributesConfig = valueType<DataFieldConfig>()({ /* eslint
|
|||||||
edit: true,
|
edit: true,
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
work_on_site_is_completed_on_year: {
|
planning_crowdsourced_site_completion_status: {
|
||||||
edit: true,
|
edit: true,
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
planning_planning_application_id_crowdsourced: {
|
planning_crowdsourced_site_completion_year: {
|
||||||
|
edit: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
planning_crowdsourced_planning_id: {
|
||||||
edit: true,
|
edit: true,
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,23 @@ const getBuildingUPRNsById = asyncController(async (req: express.Request, res: e
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET building planning data
|
||||||
|
const getBuildingPlanningDataById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
|
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await buildingService.getBuildingPlanningDataById(buildingId);
|
||||||
|
|
||||||
|
if (typeof (result) === 'undefined') {
|
||||||
|
return res.send({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
res.send({data: result, buildingId: buildingId});
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
res.send({ error: 'Database error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const getBuildingUserAttributesById = asyncController(async (req: express.Request, res: express.Response) => {
|
const getBuildingUserAttributesById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||||
if(!req.session.user_id) {
|
if(!req.session.user_id) {
|
||||||
return res.send({ error: 'Must be logged in'});
|
return res.send({ error: 'Must be logged in'});
|
||||||
@ -202,6 +219,7 @@ export default {
|
|||||||
getBuildingById,
|
getBuildingById,
|
||||||
updateBuildingById,
|
updateBuildingById,
|
||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
|
getBuildingPlanningDataById,
|
||||||
getUserVerifiedAttributes,
|
getUserVerifiedAttributes,
|
||||||
verifyBuildingAttributes,
|
verifyBuildingAttributes,
|
||||||
getBuildingEditHistoryById,
|
getBuildingEditHistoryById,
|
||||||
|
@ -26,6 +26,7 @@ router.route('/:building_id.json')
|
|||||||
// GET building UPRNs
|
// GET building UPRNs
|
||||||
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
|
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
|
||||||
|
|
||||||
|
router.get('/:building_id/planning_data.json', buildingController.getBuildingPlanningDataById);
|
||||||
|
|
||||||
// POST verify building attribute
|
// POST verify building attribute
|
||||||
router.route('/:building_id/verify.json')
|
router.route('/:building_id/verify.json')
|
||||||
|
@ -56,4 +56,5 @@ export * from './edit';
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './query';
|
export * from './query';
|
||||||
export * from './uprn';
|
export * from './uprn';
|
||||||
|
export * from './planningData';
|
||||||
export * from './verify';
|
export * from './verify';
|
||||||
|
16
app/src/api/services/building/planningData.ts
Normal file
16
app/src/api/services/building/planningData.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import db from '../../../db';
|
||||||
|
|
||||||
|
export async function getBuildingPlanningDataById(id: number) {
|
||||||
|
try {
|
||||||
|
return await db.any(
|
||||||
|
'SELECT building_properties.uprn, building_properties.building_id, planning_data.description, planning_data.status, planning_data.status_before_aliasing, planning_data.status_explanation_note, planning_data.uprn, planning_data.planning_application_id, planning_application_link, to_char(planning_data.registered_with_local_authority_date, \'YYYY-MM-DD\') AS registered_with_local_authority_date, to_char(planning_data.decision_date, \'YYYY-MM-DD\') AS decision_date, to_char(planning_data.last_synced_date, \'YYYY-MM-DD\') AS last_synced_date, planning_data.data_source, planning_data.data_source_link, planning_data.address \
|
||||||
|
FROM building_properties \
|
||||||
|
INNER JOIN planning_data ON \
|
||||||
|
building_properties.uprn = planning_data.uprn WHERE building_id = $1',
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -21,12 +21,14 @@ export function useBuildingData(buildingId: number, preloadedData: Building, inc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let [building, buildingUprns] = await Promise.all([
|
let [building, buildingUprns, planningData] = await Promise.all([
|
||||||
apiGet(`/api/buildings/${buildingId}.json${includeUserAttributes ? '?user_attributes=true' : ''}`),
|
apiGet(`/api/buildings/${buildingId}.json${includeUserAttributes ? '?user_attributes=true' : ''}`),
|
||||||
apiGet(`/api/buildings/${buildingId}/uprns.json`)
|
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||||
|
apiGet(`/api/buildings/${buildingId}/planning_data.json`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
building.uprns = buildingUprns.uprns;
|
building.uprns = buildingUprns.uprns;
|
||||||
|
building.planning_data = planningData.data;
|
||||||
building = Object.assign(building, {...building.user_attributes});
|
building = Object.assign(building, {...building.user_attributes});
|
||||||
delete building.user_attributes;
|
delete building.user_attributes;
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import './app.css';
|
|||||||
|
|
||||||
import { AuthRoute, PrivateRoute } from './route';
|
import { AuthRoute, PrivateRoute } from './route';
|
||||||
import { AuthProvider } from './auth-context';
|
import { AuthProvider } from './auth-context';
|
||||||
|
import { DisplayPreferencesProvider } from './displayPreferences-context';
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import { MapApp } from './map-app';
|
import { MapApp } from './map-app';
|
||||||
import { Building, UserVerified } from './models/building';
|
import { Building, UserVerified } from './models/building';
|
||||||
@ -53,41 +54,43 @@ export const App: React.FC<AppProps> = props => {
|
|||||||
const mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?'];
|
const mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthProvider preloadedUser={props.user}>
|
<DisplayPreferencesProvider>
|
||||||
<Switch>
|
<AuthProvider preloadedUser={props.user}>
|
||||||
<Route exact path={mapAppPaths}>
|
<Switch>
|
||||||
<Header animateLogo={false} />
|
<Route exact path={mapAppPaths}>
|
||||||
</Route>
|
<Header animateLogo={false} />
|
||||||
<Route>
|
</Route>
|
||||||
<Header animateLogo={true} />
|
<Route>
|
||||||
</Route>
|
<Header animateLogo={true} />
|
||||||
</Switch>
|
</Route>
|
||||||
<Switch>
|
</Switch>
|
||||||
<Route exact path="/about.html" component={AboutPage} />
|
<Switch>
|
||||||
<AuthRoute exact path="/login.html" component={Login} />
|
<Route exact path="/about.html" component={AboutPage} />
|
||||||
<AuthRoute exact path="/forgotten-password.html" component={ForgottenPassword} />
|
<AuthRoute exact path="/login.html" component={Login} />
|
||||||
<AuthRoute exact path="/password-reset.html" component={PasswordReset} />
|
<AuthRoute exact path="/forgotten-password.html" component={ForgottenPassword} />
|
||||||
<AuthRoute exact path="/sign-up.html" component={SignUp} />
|
<AuthRoute exact path="/password-reset.html" component={PasswordReset} />
|
||||||
<PrivateRoute exact path="/my-account.html" component={MyAccountPage} />
|
<AuthRoute exact path="/sign-up.html" component={SignUp} />
|
||||||
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
<PrivateRoute exact path="/my-account.html" component={MyAccountPage} />
|
||||||
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
||||||
<Route exact path="/ordnance-survey-licence.html" component={OrdnanceSurveyLicencePage} />
|
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
||||||
<Route exact path="/ordnance-survey-uprn.html" component={OrdnanceSurveyUprnPage} />
|
<Route exact path="/ordnance-survey-licence.html" component={OrdnanceSurveyLicencePage} />
|
||||||
<Route exact path="/data-accuracy.html" component={DataAccuracyPage} />
|
<Route exact path="/ordnance-survey-uprn.html" component={OrdnanceSurveyUprnPage} />
|
||||||
<Route exact path="/data-extracts.html" component={DataExtracts} />
|
<Route exact path="/data-accuracy.html" component={DataAccuracyPage} />
|
||||||
<Route exact path="/contact.html" component={ContactPage} />
|
<Route exact path="/data-extracts.html" component={DataExtracts} />
|
||||||
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
<Route exact path="/contact.html" component={ContactPage} />
|
||||||
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
<Route exact path="/code-of-conduct.html" component={CodeOfConductPage} />
|
||||||
<Route exact path="/history.html" component={ChangesPage} />
|
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
||||||
<Route exact path={mapAppPaths} >
|
<Route exact path="/history.html" component={ChangesPage} />
|
||||||
<MapApp
|
<Route exact path={mapAppPaths} >
|
||||||
building={props.building}
|
<MapApp
|
||||||
user_verified={props.user_verified}
|
building={props.building}
|
||||||
revisionId={props.revisionId}
|
user_verified={props.user_verified}
|
||||||
/>
|
revisionId={props.revisionId}
|
||||||
</Route>
|
/>
|
||||||
<Route component={NotFound} />
|
</Route>
|
||||||
</Switch>
|
<Route component={NotFound} />
|
||||||
</AuthProvider>
|
</Switch>
|
||||||
|
</AuthProvider>
|
||||||
|
</DisplayPreferencesProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
|||||||
const [userError, setUserError] = useState<string>(undefined);
|
const [userError, setUserError] = useState<string>(undefined);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const login = useCallback(async (data: UserLoginData, cb: (err) => void = noop) => {
|
const login = useCallback(async (data: UserLoginData, cb: (err) => void = noop) => {
|
||||||
if(isAuthenticated) {
|
if(isAuthenticated) {
|
||||||
return;
|
return;
|
||||||
@ -199,7 +200,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
|||||||
logout,
|
logout,
|
||||||
signup,
|
signup,
|
||||||
generateApiKey,
|
generateApiKey,
|
||||||
deleteAccount
|
deleteAccount,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
|
@ -4,6 +4,7 @@ import { useAuth } from '../auth-context';
|
|||||||
import { categoriesConfig, Category } from '../config/categories-config';
|
import { categoriesConfig, Category } from '../config/categories-config';
|
||||||
import { categoryUiConfig } from '../config/category-ui-config';
|
import { categoryUiConfig } from '../config/category-ui-config';
|
||||||
import { Building, UserVerified } from '../models/building';
|
import { Building, UserVerified } from '../models/building';
|
||||||
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
|
|
||||||
import BuildingNotFound from './building-not-found';
|
import BuildingNotFound from './building-not-found';
|
||||||
|
|
||||||
@ -14,6 +15,8 @@ interface BuildingViewProps {
|
|||||||
user_verified?: any;
|
user_verified?: any;
|
||||||
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
||||||
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,6 +48,8 @@ const BuildingView: React.FunctionComponent<BuildingViewProps> = (props) => {
|
|||||||
intro={intro}
|
intro={intro}
|
||||||
inactive={inactive}
|
inactive={inactive}
|
||||||
user={user}
|
user={user}
|
||||||
|
mapColourScale={props.mapColourScale}
|
||||||
|
onMapColourScale={props.onMapColourScale}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
import InfoBox from '../../components/info-box';
|
||||||
|
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||||
|
|
||||||
|
interface PlanningDataOfficialDataEntryProps {
|
||||||
|
shownData: {
|
||||||
|
uprn: string;
|
||||||
|
building_id: number;
|
||||||
|
status?: string,
|
||||||
|
status_before_aliasing?: string,
|
||||||
|
description?: string;
|
||||||
|
planning_application_link?: string;
|
||||||
|
registered_with_local_authority_date?: string;
|
||||||
|
decision_date?: string;
|
||||||
|
last_synced_date?: string;
|
||||||
|
data_source: string;
|
||||||
|
data_source_link?: string;
|
||||||
|
address?: string;
|
||||||
|
}[];
|
||||||
|
messageOnMissingData: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const {useState} = React;
|
||||||
|
|
||||||
|
const LongText = ({ content,limit}) => {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
const showMore = () => setShowAll(true);
|
||||||
|
const showLess = () => setShowAll(false);
|
||||||
|
|
||||||
|
if (content == null) {
|
||||||
|
return <div>{MissingData}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length <= limit) {
|
||||||
|
return <div>{content}</div>
|
||||||
|
}
|
||||||
|
if (showAll) {
|
||||||
|
return <div>
|
||||||
|
{content}
|
||||||
|
<br/>
|
||||||
|
<b onClick={showLess}>Shorten description</b>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
const toShow = content.substring(0, limit).trim() + "... ";
|
||||||
|
return <div>
|
||||||
|
{toShow}
|
||||||
|
<br/>
|
||||||
|
<b onClick={showMore}>Show full description</b>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Disclaimer = () => { return <Fragment><i><div><u>Disclaimer</u>: Not all applications for London are displayed. Some boroughs do not yet provide planning data to the GLA. For comprehensive information on applications please visit the relevant local authority's planning website.</div></i></Fragment> }
|
||||||
|
|
||||||
|
const MissingData = "not provided by data source"
|
||||||
|
|
||||||
|
function ShowIfAvailable(data) {
|
||||||
|
return <>{data ? data.toString() : MissingData }</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LinkIfAvailable = (link) => {
|
||||||
|
return <>{link ? <a href={link.toString()}>{link.toString()}</a> : MissingData }</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusInfo = ({status, statusBeforeAliasing}) => {
|
||||||
|
if(status == null) {
|
||||||
|
return <>{LinkIfAvailable(null)}</>
|
||||||
|
}
|
||||||
|
if(status != statusBeforeAliasing) {
|
||||||
|
return <>{status} - status in data source was: {statusBeforeAliasing}</>
|
||||||
|
}
|
||||||
|
return <>{status}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanningDataOfficialDataEntry: React.FC<PlanningDataOfficialDataEntryProps> = (props) => {
|
||||||
|
const data = props.shownData || [];
|
||||||
|
if(data.length == 0) {
|
||||||
|
return (<Fragment>
|
||||||
|
<div className={`alert alert-dark`} role="alert" style={{ fontSize: 13, backgroundColor: "#f6f8f9" }}>
|
||||||
|
<Disclaimer />
|
||||||
|
</div>
|
||||||
|
<InfoBox type='success'>
|
||||||
|
{props.messageOnMissingData}
|
||||||
|
</InfoBox>
|
||||||
|
</Fragment>);
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<div className={`alert alert-dark`} role="alert" style={{ fontSize: 13, backgroundColor: "#f6f8f9"}}>
|
||||||
|
{/* TODO: data[0] is problematic here... Compute it from listed elements and show all distinct variants? Error if they are not distinct? Hardcode it? */}
|
||||||
|
<div>
|
||||||
|
<i>
|
||||||
|
Planning application status is streamed using live data uploaded by local authorities to {data[0]["data_source_link"] ? <a href={data[0]["data_source_link"]}>{data[0]["data_source"]}</a> : data[0]["data_source"] }.
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<Disclaimer />
|
||||||
|
</div>
|
||||||
|
{data.map((item) => (
|
||||||
|
<Fragment>
|
||||||
|
<InfoBox type='success'>
|
||||||
|
<Fragment>
|
||||||
|
<div><b>Current planning application status for this site:</b> <StatusInfo
|
||||||
|
statusBeforeAliasing={item["status_before_aliasing"]}
|
||||||
|
status={item["status"]}
|
||||||
|
/></div>
|
||||||
|
{item["status_explanation_note"] ? <div><b>Explanation</b>: {item["status_explanation_note"]}</div> : <></>}
|
||||||
|
<div><b>Planning application ID:</b> {ShowIfAvailable(item["planning_application_id"])}</div>
|
||||||
|
<div><b>Date registered by the planning authority (validation date)</b>: {ShowIfAvailable(item["registered_with_local_authority_date"])}</div>
|
||||||
|
<div><b>Decision date</b>: {ShowIfAvailable(item["decision_date"])}</div>
|
||||||
|
<div><b>Planning application link</b>: {LinkIfAvailable(item["planning_application_link"])}</div>
|
||||||
|
<div><b>Description of proposed work</b>: {item["description"] ? <LongText content = {item["description"]} limit = {400}/> : MissingData}</div>
|
||||||
|
<div><b>Address of the location as provided by local authority:</b> {ShowIfAvailable(item["address"])}</div>
|
||||||
|
<div><b>Most recent update by data provider:</b> {ShowIfAvailable(item["decision_date"])}</div>
|
||||||
|
</Fragment>
|
||||||
|
</InfoBox>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}</>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlanningDataOfficialDataEntry;
|
@ -9,6 +9,7 @@ import ErrorBox from '../components/error-box';
|
|||||||
import InfoBox from '../components/info-box';
|
import InfoBox from '../components/info-box';
|
||||||
import { compareObjects } from '../helpers';
|
import { compareObjects } from '../helpers';
|
||||||
import { Building, BuildingEdits, BuildingUserAttributes, UserVerified } from '../models/building';
|
import { Building, BuildingEdits, BuildingUserAttributes, UserVerified } from '../models/building';
|
||||||
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
|
|
||||||
import ContainerHeader from './container-header';
|
import ContainerHeader from './container-header';
|
||||||
@ -34,6 +35,9 @@ interface DataContainerProps {
|
|||||||
user_verified?: any;
|
user_verified?: any;
|
||||||
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
||||||
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
||||||
|
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataContainerState {
|
interface DataContainerState {
|
||||||
@ -43,6 +47,8 @@ interface DataContainerState {
|
|||||||
currentBuildingId: number;
|
currentBuildingId: number;
|
||||||
currentBuildingRevisionId: number;
|
currentBuildingRevisionId: number;
|
||||||
buildingEdits: BuildingEdits;
|
buildingEdits: BuildingEdits;
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataContainerType = React.ComponentType<DataContainerProps>;
|
export type DataContainerType = React.ComponentType<DataContainerProps>;
|
||||||
@ -66,7 +72,9 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
keys_to_copy: {},
|
keys_to_copy: {},
|
||||||
buildingEdits: {},
|
buildingEdits: {},
|
||||||
currentBuildingId: undefined,
|
currentBuildingId: undefined,
|
||||||
currentBuildingRevisionId: undefined
|
currentBuildingRevisionId: undefined,
|
||||||
|
mapColourScale: undefined,
|
||||||
|
onMapColourScale: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
@ -108,7 +116,9 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
keys_to_copy: categoryKeys,
|
keys_to_copy: categoryKeys,
|
||||||
buildingEdits: {},
|
buildingEdits: {},
|
||||||
currentBuildingId: newBuildingId,
|
currentBuildingId: newBuildingId,
|
||||||
currentBuildingRevisionId: newBuildingRevisionId
|
currentBuildingRevisionId: newBuildingRevisionId,
|
||||||
|
mapColourScale: props.mapColourScale,
|
||||||
|
onMapColourScale: props.onMapColourScale
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,6 +371,8 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
onSaveAdd={undefined}
|
onSaveAdd={undefined}
|
||||||
onSaveChange={undefined}
|
onSaveChange={undefined}
|
||||||
user_verified={[]}
|
user_verified={[]}
|
||||||
|
mapColourScale={undefined}
|
||||||
|
onMapColourScale={undefined}
|
||||||
/>
|
/>
|
||||||
</Fragment> :
|
</Fragment> :
|
||||||
this.props.building != undefined ?
|
this.props.building != undefined ?
|
||||||
@ -413,6 +425,8 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
|||||||
onSaveChange={this.handleSaveChange}
|
onSaveChange={this.handleSaveChange}
|
||||||
user_verified={this.props.user_verified}
|
user_verified={this.props.user_verified}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
|
mapColourScale={this.props.mapColourScale}
|
||||||
|
onMapColourScale={this.props.onMapColourScale}
|
||||||
/>
|
/>
|
||||||
</form> :
|
</form> :
|
||||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
<InfoBox msg="Select a building to view data"></InfoBox>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Building, BuildingAttributes } from '../../models/building';
|
import { Building, BuildingAttributes } from '../../models/building';
|
||||||
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
|
|
||||||
interface CopyProps {
|
interface CopyProps {
|
||||||
copying: boolean;
|
copying: boolean;
|
||||||
@ -38,6 +39,9 @@ interface CategoryViewProps {
|
|||||||
|
|
||||||
user_verified: any;
|
user_verified: any;
|
||||||
user?: any;
|
user?: any;
|
||||||
|
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -18,6 +18,22 @@ import { MultiDataEntry } from '../data-components/multi-data-entry/multi-data-e
|
|||||||
* Community view/edit section
|
* Community view/edit section
|
||||||
*/
|
*/
|
||||||
const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||||
|
const switchToLikesMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('likes')
|
||||||
|
}
|
||||||
|
const switchToLocalSignificanceMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('community_local_significance_total')
|
||||||
|
}
|
||||||
|
const switchToExpectedApplicationMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('community_expected_planning_application_total')
|
||||||
|
}
|
||||||
|
const switchToPublicOwnershipMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('community_in_public_ownership')
|
||||||
|
}
|
||||||
const worthKeepingReasonsNonEmpty = Object.values(props.building.community_type_worth_keeping_reasons ?? {}).some(x => x);
|
const worthKeepingReasonsNonEmpty = Object.values(props.building.community_type_worth_keeping_reasons ?? {}).some(x => x);
|
||||||
return <>
|
return <>
|
||||||
<InfoBox type='warning'>
|
<InfoBox type='warning'>
|
||||||
@ -38,6 +54,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToLikesMapStyle}>
|
||||||
|
{'Click here to switch map key to this info'}
|
||||||
|
</button>
|
||||||
<LogicalDataEntry
|
<LogicalDataEntry
|
||||||
slug='community_type_worth_keeping'
|
slug='community_type_worth_keeping'
|
||||||
title={buildingUserFields.community_type_worth_keeping.title}
|
title={buildingUserFields.community_type_worth_keeping.title}
|
||||||
@ -81,7 +100,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
/>
|
/>
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToLocalSignificanceMapStyle}>
|
||||||
|
{'Click here to switch map key to this info'}
|
||||||
|
</button>
|
||||||
<UserOpinionEntry
|
<UserOpinionEntry
|
||||||
slug='community_expected_planning_application'
|
slug='community_expected_planning_application'
|
||||||
title={buildingUserFields.community_expected_planning_application.title}
|
title={buildingUserFields.community_expected_planning_application.title}
|
||||||
@ -92,6 +113,10 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
/>
|
/>
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToExpectedApplicationMapStyle}>
|
||||||
|
{'Click here to switch map key to this info'}
|
||||||
|
</button>
|
||||||
|
<InfoBox>You can click and colour any other building on the map as well.</InfoBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InfoBox>Can you help add information on community use of buildings?</InfoBox>
|
<InfoBox>Can you help add information on community use of buildings?</InfoBox>
|
||||||
@ -149,6 +174,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
/>
|
/>
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToPublicOwnershipMapStyle}>
|
||||||
|
{'Click here to switch map key to this info'}
|
||||||
|
</button>
|
||||||
<Verification
|
<Verification
|
||||||
slug="community_public_ownership"
|
slug="community_public_ownership"
|
||||||
allow_verify={props.user !== undefined && props.building.community_public_ownership !== null && !props.edited}
|
allow_verify={props.user !== undefined && props.building.community_public_ownership !== null && !props.edited}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
.map-switcher-inline {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #FFC0CB;
|
||||||
|
margin: 12px;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
.map-switcher-inline.night {
|
||||||
|
background: #FFC0CB;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #343a40;
|
||||||
|
border-color: #ff6065;
|
||||||
|
}
|
||||||
|
.map-switcher-inline.night .btn:hover {
|
||||||
|
color: #343a40;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
border-color: #ff6065;
|
||||||
|
}
|
@ -1,137 +1,180 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
|
import './map-switcher-inline.css'; // import in a proper place
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import InfoBox from '../../components/info-box';
|
import InfoBox from '../../components/info-box';
|
||||||
import { dataFields } from '../../config/data-fields-config';
|
import NumericDataEntryWithFormattedLink from '../data-components/numeric-data-entry-with-formatted-link';
|
||||||
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
import { buildingUserFields, dataFields } from '../../config/data-fields-config';
|
||||||
|
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||||
|
import UserOpinionEntry from '../data-components/user-opinion-data-entry';
|
||||||
|
|
||||||
|
import DataEntry from '../data-components/data-entry';
|
||||||
import { LogicalDataEntry } from '../data-components/logical-data-entry/logical-data-entry';
|
import { LogicalDataEntry } from '../data-components/logical-data-entry/logical-data-entry';
|
||||||
import NumericDataEntryWithFormattedLink from '../data-components/numeric-data-entry-with-formatted-link';import DataEntry from '../data-components/data-entry';
|
|
||||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||||
import SelectDataEntry from '../data-components/select-data-entry';
|
import SelectDataEntry from '../data-components/select-data-entry';
|
||||||
import Verification from '../data-components/verification';
|
import Verification from '../data-components/verification';
|
||||||
import withCopyEdit from '../data-container';
|
import withCopyEdit from '../data-container';
|
||||||
|
import PlanningDataOfficialDataEntry from '../data-components/planning-data-entry';
|
||||||
import { CategoryViewProps } from './category-view-props';
|
import { CategoryViewProps } from './category-view-props';
|
||||||
|
import { Category } from '../../config/categories-config';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
import { processParam } from '../../../api/parameters';
|
||||||
|
|
||||||
/**
|
const currentTimestamp = new Date().valueOf();
|
||||||
* Planning view/edit section
|
const milisecondsInYear = 1000 * 60 * 60 * 24 * 365;
|
||||||
*/
|
|
||||||
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
// there is already "parseDate" in helpers
|
||||||
|
// but it is using timestamp as input, while this one
|
||||||
|
// uses lower accuracy (as actual data is using the same accuracy)
|
||||||
|
function parseDateSpecifiedWithDailyAccuracy(isoUtcDate: string): Date {
|
||||||
|
const [year, month, day] = isoUtcDate.match(/^(\d{4})-(\d\d)-(\d\d)$/)
|
||||||
|
.splice(1)
|
||||||
|
.map(x => parseInt(x, 10));
|
||||||
|
return new Date(Date.UTC(year, month-1, day));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArchived(item) {
|
||||||
|
const decisionDate = item.decision_date;
|
||||||
|
if(decisionDate != null) {
|
||||||
|
if ((currentTimestamp - parseDateSpecifiedWithDailyAccuracy(decisionDate).valueOf()) > milisecondsInYear) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(item.registered_with_local_authority_date != null) {
|
||||||
|
if ((currentTimestamp - parseDateSpecifiedWithDailyAccuracy(item.registered_with_local_authority_date).valueOf()) > milisecondsInYear) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||||
|
const switchToExpectedApplicationMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('community_expected_planning_application_total')
|
||||||
|
}
|
||||||
|
const switchToBuildingProtectionMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('planning_combined')
|
||||||
|
}
|
||||||
|
const switchToEmptyMapStyle = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onMapColourScale('empty_map')
|
||||||
|
}
|
||||||
|
const { flood, floodSwitchOnClick, housing, housingSwitchOnClick, creative, creativeSwitchOnClick, vista, vistaSwitchOnClick, parcel, parcelSwitchOnClick, conservation, conservationSwitchOnClick } = useDisplayPreferences();
|
||||||
|
const communityLinkUrl = `/${props.mode}/${Category.Community}/${props.building.building_id}`;
|
||||||
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InfoBox type='warning'>
|
<DataEntryGroup name="Planning application information" collapsed={true} >
|
||||||
This section is under development as part of the project CLPV Tool. For more details and progress <a href="https://github.com/colouring-cities/manual/wiki/G2.-Data-capture-(2).-Live-streaming-and-automated-methods">read here</a>.
|
<DataEntryGroup name="Current/active applications (official data)" collapsed={false} >
|
||||||
</InfoBox>
|
<InfoBox>
|
||||||
<DataEntryGroup name="Planning application information">
|
To see planning applications visualised for different periods click on the map key dropdown.
|
||||||
<CheckboxDataEntry
|
|
||||||
title="Is a planning application live for this site?"
|
|
||||||
slug="planning_live_application"
|
|
||||||
value={null}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
<CheckboxDataEntry
|
|
||||||
title={dataFields.planning_demolition_proposed.title}
|
|
||||||
slug="planning_demolition_proposed"
|
|
||||||
value={props.building.planning_demolition_proposed}
|
|
||||||
mode={props.mode}
|
|
||||||
copy={props.copy}
|
|
||||||
onChange={props.onChange}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
<CheckboxDataEntry
|
|
||||||
title="Has this application recently been been approved/refused?"
|
|
||||||
slug="planning_recent_outcome"
|
|
||||||
value={null}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
<CheckboxDataEntry
|
|
||||||
title="Has the work been carried out?"
|
|
||||||
slug="planning_carried_out"
|
|
||||||
value={null}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
<InfoBox msg="For historical planning applications see Planning Portal link" />
|
|
||||||
{/*
|
|
||||||
Move to Demolition:
|
|
||||||
|
|
||||||
<CheckboxDataEntry
|
To comment on an application follow the application link if provided, or visit the relevant local authority's planning page.
|
||||||
title={dataFields.planning_demolition_complete.title}
|
</InfoBox>
|
||||||
slug="planning_demolition_complete"
|
{props.building.planning_data ?
|
||||||
value={props.building.planning_demolition_complete}
|
<PlanningDataOfficialDataEntry
|
||||||
mode={props.mode}
|
shownData={props.building.planning_data.filter(item => isArchived(item) == false)}
|
||||||
copy={props.copy}
|
messageOnMissingData={
|
||||||
onChange={props.onChange}
|
props.building.planning_data.length > 0 ?
|
||||||
disabled={false}
|
"Only past application data is currently available for this site"
|
||||||
|
:
|
||||||
|
"No live planning data are currently available for this building from the Planning London DataHub."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</DataEntryGroup>
|
||||||
|
<DataEntryGroup name="Past applications (official data)" collapsed={true} >
|
||||||
|
<InfoBox>
|
||||||
|
This section provides data on past applications where available from the GLA, including those with no decision in over a year
|
||||||
|
</InfoBox>
|
||||||
|
{props.building.planning_data ?
|
||||||
|
<PlanningDataOfficialDataEntry
|
||||||
|
shownData={props.building.planning_data.filter(item => isArchived(item))}
|
||||||
|
messageOnMissingData={
|
||||||
|
props.building.planning_data.length > 0 ?
|
||||||
|
"Only current application data is currently available for this site"
|
||||||
|
:
|
||||||
|
"No live planning data are currently available for this building from the Planning London DataHub."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</DataEntryGroup>
|
||||||
|
<DataEntryGroup name="Possible future applications (crowdsourced data)" collapsed={true} >
|
||||||
|
<InfoBox type='info'>Click and colour buildings here if you think they may be subject to a future planning application involving demolition. To add your opinion on how well this building works, please also visit the <Link to={communityLinkUrl}>Community</Link> section.</InfoBox>
|
||||||
|
{
|
||||||
|
props.mapColourScale != "community_expected_planning_application_total" ?
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToExpectedApplicationMapStyle}>
|
||||||
|
{'Click here to view possible locations of future applications'}
|
||||||
|
</button>
|
||||||
|
:
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToEmptyMapStyle}>
|
||||||
|
{'Click here to hide possible locations of future applications'}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<UserOpinionEntry
|
||||||
|
slug='community_expected_planning_application'
|
||||||
|
title={buildingUserFields.community_expected_planning_application.title}
|
||||||
|
userValue={props.building.community_expected_planning_application}
|
||||||
|
|
||||||
|
onChange={props.onSaveChange}
|
||||||
|
mode={props.mode}
|
||||||
|
copy={props.copy}
|
||||||
/>
|
/>
|
||||||
<DataEntry
|
</DataEntryGroup>
|
||||||
title={dataFields.planning_demolition_history.title}
|
|
||||||
slug="planning_demolition_history"
|
|
||||||
value={props.building.planning_demolition_history}
|
|
||||||
mode={props.mode}
|
|
||||||
copy={props.copy}
|
|
||||||
onChange={props.onChange}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
*/}
|
|
||||||
</DataEntryGroup>
|
</DataEntryGroup>
|
||||||
<DataEntryGroup name="Planning zones" collapsed={true} >
|
<DataEntryGroup name="Planning zones" collapsed={true} >
|
||||||
<InfoBox type='success'>
|
<InfoBox>
|
||||||
Data in this section comes from the Greater London Authority's Planning London Datahub. Please check the original GLA source when using for planning purposes.
|
To view planning zone data for London click the pink buttons. You may need to <u>zoom out</u>.
|
||||||
|
Information on whether a specific building is in a zone will be added automatically in future.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
<div className={`alert alert-dark`} role="alert" style={{ fontSize: 13, backgroundColor: "#f6f8f9" }}>
|
||||||
|
<i>
|
||||||
|
Data in this section comes from the Greater London Authority's Planning London Datahub. Please check the original GLA source when using for planning purposes.
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
<LogicalDataEntry
|
<LogicalDataEntry
|
||||||
title="Is the building inside a flood zone?"
|
title="Is the building inside a Flood Zone?"
|
||||||
slug="planning_live_application"
|
slug="planning_live_application"
|
||||||
value={null}
|
value={null}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
tooltip={"GLA official description: \"All areas with more than a 1 in 1,000 annual probability of either river or sea flooding.\""}
|
||||||
/>
|
/>
|
||||||
{/*
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={floodSwitchOnClick}>
|
||||||
<form className={`layer-switcher-inline`}>
|
{(flood === 'enabled')? 'Click to hide Flood Zones' : 'Click to see Flood Zones mapped'}
|
||||||
<button className="btn btn-outline btn-outline-dark"
|
</button>
|
||||||
type="submit">
|
|
||||||
Click to see the data mapped
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
*/}
|
|
||||||
<LogicalDataEntry
|
<LogicalDataEntry
|
||||||
title="Is the building in a strategic development zone for housing?"
|
title="Is the building in a Housing Zone?"
|
||||||
slug="planning_live_application"
|
slug="planning_live_application"
|
||||||
value={null}
|
value={null}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
tooltip={"GLA official description: \"Housing zones are areas funded by the Mayor and government to attract developers and relevant partners to build new homes.\""}
|
||||||
/>
|
/>
|
||||||
{/*
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={housingSwitchOnClick}>
|
||||||
<form className={`layer-switcher-inline`}>
|
{(housing === 'enabled')? 'Click to hide Housing Zones' : 'Click to see Housing Zones mapped'}
|
||||||
<button className="btn btn-outline btn-outline-dark"
|
</button>
|
||||||
type="submit">
|
|
||||||
Click to see the data mapped
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
*/}
|
|
||||||
<LogicalDataEntry
|
<LogicalDataEntry
|
||||||
title="Is the building in a strategic development zone for commerce or industry?"
|
title="Is the building in a Creative Enterprise Zone?"
|
||||||
slug="planning_live_application"
|
slug="planning_live_application"
|
||||||
value={null}
|
value={null}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
tooltip={"GLA official description: \"Creative Enterprise Zones are a new Mayoral initiative to designate areas of London where artists and creative businesses can find permanent affordable space to work; are supported to start-up and grow; and where local people are helped to learn creative sector skills and find new jobs.\""}
|
||||||
/>
|
/>
|
||||||
{/*
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={creativeSwitchOnClick}>
|
||||||
<form className={`layer-switcher-inline`}>
|
{(creative === 'enabled')? 'Click to hide Creative Enterprise Zones' : 'Click to see Creative Enterprise Zones'}
|
||||||
<button className="btn btn-outline btn-outline-dark"
|
</button>
|
||||||
type="submit">
|
|
||||||
Click to see the data mapped
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
*/}
|
|
||||||
<LogicalDataEntry
|
<LogicalDataEntry
|
||||||
title="Is the building within a protected sightline?"
|
title="Is the building within a Protected Vista?"
|
||||||
slug="planning_live_application"
|
slug="planning_live_application"
|
||||||
value={null}
|
value={null}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
tooltip={"GLA official description: \"The Protected Vistas are established in the London Plan with more detailed guidance provided in the London View Management Framework (LVMF). The London Plan seeks to protect the significant views which help to define London, including the panoramas, linear views and townscape views in this layer.\""}
|
||||||
/>
|
/>
|
||||||
{/*
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={vistaSwitchOnClick}>
|
||||||
<form className={`layer-switcher-inline`}>
|
{(vista === 'enabled')? 'Click to hide Protected Vistas' : 'Click to see Protected Vistas'}
|
||||||
<button className="btn btn-outline btn-outline-dark"
|
</button>
|
||||||
type="submit">
|
|
||||||
Click to see the data mapped
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
*/}
|
|
||||||
{/*
|
{/*
|
||||||
<DataEntry
|
<DataEntry
|
||||||
title={dataFields.planning_glher_url.title}
|
title={dataFields.planning_glher_url.title}
|
||||||
@ -153,10 +196,23 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
/>
|
/>
|
||||||
*/}
|
*/}
|
||||||
</DataEntryGroup>
|
</DataEntryGroup>
|
||||||
<DataEntryGroup name="Building protection" collapsed={true} >
|
<DataEntryGroup name="Heritage assets and building protection" collapsed={true} >
|
||||||
<InfoBox type='success'>
|
<InfoBox>
|
||||||
This section provides information on heritage assets and building protection. To produce the most accurate spatial map possible we need to combine official data with crowdsourced data. Help us create this map together by checking, verifying and adding information.
|
Help us produce the most accurate map possible for London's designated/protected buildings. Please add data if missing or click "Verify" where entries are correct.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
{
|
||||||
|
props.mapColourScale != "planning_combined" ?
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToBuildingProtectionMapStyle}>
|
||||||
|
{'Click to see individual protected buildings mapped'}
|
||||||
|
</button>
|
||||||
|
:
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={switchToEmptyMapStyle}>
|
||||||
|
{'Click to hide individual protected buildings on map'}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={conservationSwitchOnClick}>
|
||||||
|
{(conservation === 'enabled')? 'Click to hide Convervation Areas' : 'Click to see Convervation Areas'}
|
||||||
|
</button>
|
||||||
<NumericDataEntryWithFormattedLink
|
<NumericDataEntryWithFormattedLink
|
||||||
title={dataFields.planning_list_id.title}
|
title={dataFields.planning_list_id.title}
|
||||||
slug="planning_list_id"
|
slug="planning_list_id"
|
||||||
@ -164,7 +220,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
placeholder="If yes, add ID here"
|
placeholder="add ID here"
|
||||||
linkTargetFunction={(id: String) => { return "https://historicengland.org.uk/listing/the-list/list-entry/" + id + "?section=official-list-entry" } }
|
linkTargetFunction={(id: String) => { return "https://historicengland.org.uk/listing/the-list/list-entry/" + id + "?section=official-list-entry" } }
|
||||||
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
||||||
/>
|
/>
|
||||||
@ -224,7 +280,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
placeholder="If yes, add ID here"
|
placeholder="add ID here"
|
||||||
linkTargetFunction={(id: String) => { return "https://whc.unesco.org/en/list/" + id } }
|
linkTargetFunction={(id: String) => { return "https://whc.unesco.org/en/list/" + id } }
|
||||||
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
||||||
/>
|
/>
|
||||||
@ -339,37 +395,124 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
|||||||
verified_count={props.building.verified.planning_in_apa_url}
|
verified_count={props.building.verified.planning_in_apa_url}
|
||||||
/>
|
/>
|
||||||
</DataEntryGroup>
|
</DataEntryGroup>
|
||||||
<DataEntryGroup name="Land ownership type" collapsed={true} >
|
<DataEntryGroup name="Forthcoming data (sections to be activated)" collapsed={true} >
|
||||||
<InfoBox type='success'>
|
<DataEntryGroup name="Active application info (crowdsourced)" collapsed={true} >
|
||||||
This section is designed to provide information on land parcels and their ownership type. Can you help us to crowdsource this information?
|
{/* will be titled "Other active application info (crowdsourced data)" once active" */}
|
||||||
|
<InfoBox type='warning'>
|
||||||
|
This category is not yet activated.
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
<SelectDataEntry
|
{/* that is placeholder display, will be replaced by actual code */}
|
||||||
slug='community_public_ownership'
|
<div className="data-title">
|
||||||
title={"What type of owner owns this land parcel? "}
|
<div className="data-title-text">
|
||||||
value={props.building.community_public_ownership}
|
<ul>
|
||||||
options={[
|
<li>Year of completion if known</li>
|
||||||
'Government-owned',
|
<li>If you know of a planning application that has been recently submitted for this site, and is not listed in the blue box above, please enter its planning application ID below:</li>
|
||||||
'Charity-owned',
|
<li>If any of the active planning applications are not mapped onto the correct site, please tick here</li>
|
||||||
'Community-owned/cooperative',
|
</ul>
|
||||||
'Owned by other non-profit body',
|
</div>
|
||||||
'Not in public/community ownership',
|
</div>
|
||||||
]}
|
{
|
||||||
|
/*
|
||||||
onChange={props.onChange}
|
<NumericDataEntry
|
||||||
|
title={dataFields.planning_crowdsourced_site_completion_year.title}
|
||||||
|
slug="planning_crowdsourced_site_completion_year"
|
||||||
|
value={props.building.planning_crowdsourced_site_completion_year}
|
||||||
mode={props.mode}
|
mode={props.mode}
|
||||||
copy={props.copy}
|
copy={props.copy}
|
||||||
/>
|
onChange={props.onChange}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
<Verification
|
<Verification
|
||||||
slug="community_public_ownership"
|
slug="planning_crowdsourced_site_completion_year"
|
||||||
allow_verify={props.user !== undefined && props.building.community_public_ownership !== null && !props.edited}
|
allow_verify={false}
|
||||||
onVerify={props.onVerify}
|
onVerify={props.onVerify}
|
||||||
user_verified={props.user_verified.hasOwnProperty("community_public_ownership")}
|
user_verified={props.user_verified.hasOwnProperty("planning_crowdsourced_site_completion_year")}
|
||||||
user_verified_as={props.user_verified.community_public_ownership}
|
user_verified_as={props.user_verified.planning_crowdsourced_site_completion_year}
|
||||||
verified_count={props.building.verified.community_public_ownership}
|
verified_count={props.building.verified.planning_crowdsourced_site_completion_year}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DataEntry
|
||||||
|
title={dataFields.planning_crowdsourced_planning_id.title}
|
||||||
|
slug="planning_crowdsourced_planning_id"
|
||||||
|
value={props.building.planning_crowdsourced_planning_id}
|
||||||
|
mode={props.mode}
|
||||||
|
copy={props.copy}
|
||||||
|
onChange={props.onChange}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
<Verification
|
||||||
|
slug="planning_crowdsourced_planning_id"
|
||||||
|
allow_verify={false && props.user !== undefined && props.building.planning_crowdsourced_planning_id !== null && !props.edited}
|
||||||
|
onVerify={props.onVerify}
|
||||||
|
user_verified={props.user_verified.hasOwnProperty("planning_crowdsourced_planning_id")}
|
||||||
|
user_verified_as={props.user_verified.planning_crowdsourced_planning_id}
|
||||||
|
verified_count={props.building.verified.planning_crowdsourced_planning_id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LogicalDataEntry
|
||||||
|
slug='community_expected_planning_application_is_inaccurate'
|
||||||
|
title={"If any of the active planning applications are not mapped onto the correct site, please tick here"}
|
||||||
|
value={null}
|
||||||
|
|
||||||
|
onChange={props.onSaveChange}
|
||||||
|
mode={props.mode}
|
||||||
|
copy={props.copy}
|
||||||
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
|
on enabling switch it to UserOpinionEntry, remove value and restore userValue
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
</DataEntryGroup>
|
||||||
|
<DataEntryGroup name="Land ownership type" collapsed={true} >
|
||||||
|
<InfoBox type='warning'>
|
||||||
|
This category is not yet activated.
|
||||||
|
</InfoBox>
|
||||||
|
<InfoBox>
|
||||||
|
This section is designed to provide information on land parcels and their ownership type. Can you help us to crowdsource this information?
|
||||||
|
</InfoBox>
|
||||||
|
<button className="map-switcher-inline btn btn-outline btn-outline-dark" onClick={parcelSwitchOnClick}>
|
||||||
|
{(parcel === 'enabled')? 'Click to hide sample of parcel data (in City)' : 'Click to see sample of parcel data (in City) mapped'}
|
||||||
|
</button>
|
||||||
|
<div className="data-title">
|
||||||
|
<div className="data-title-text">
|
||||||
|
<ul>
|
||||||
|
<li>What type of owner owns this land parcel?</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<SelectDataEntry
|
||||||
|
slug='community_public_ownership'
|
||||||
|
title={"What type of owner owns this land parcel? "}
|
||||||
|
value={props.building.community_public_ownership}
|
||||||
|
options={[
|
||||||
|
'Government-owned',
|
||||||
|
'Charity-owned',
|
||||||
|
'Community-owned/cooperative',
|
||||||
|
'Owned by other non-profit body',
|
||||||
|
'Not in public/community ownership',
|
||||||
|
]}
|
||||||
|
|
||||||
|
onChange={props.onChange}
|
||||||
|
mode={props.mode}
|
||||||
|
copy={props.copy}
|
||||||
|
/>
|
||||||
|
<Verification
|
||||||
|
slug="community_public_ownership"
|
||||||
|
allow_verify={props.user !== undefined && props.building.community_public_ownership !== null && !props.edited}
|
||||||
|
onVerify={props.onVerify}
|
||||||
|
user_verified={props.user_verified.hasOwnProperty("community_public_ownership")}
|
||||||
|
user_verified_as={props.user_verified.community_public_ownership}
|
||||||
|
verified_count={props.building.verified.community_public_ownership}
|
||||||
|
/>
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
</DataEntryGroup>
|
||||||
</DataEntryGroup>
|
</DataEntryGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
)};
|
||||||
const PlanningContainer = withCopyEdit(PlanningView);
|
const PlanningContainer = withCopyEdit(PlanningView);
|
||||||
|
|
||||||
export default PlanningContainer;
|
export default PlanningContainer;
|
||||||
|
@ -150,6 +150,9 @@
|
|||||||
position: static;
|
position: static;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
.data-section a {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
.data-list dd {
|
.data-list dd {
|
||||||
margin: 0 0 0.5rem;
|
margin: 0 0 0.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
@ -2,7 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
interface InfoBoxProps {
|
interface InfoBoxProps {
|
||||||
msg?: string;
|
msg?: string;
|
||||||
type?: 'info' | 'warning' | 'success'
|
// https://react-bootstrap.github.io/components/alerts/
|
||||||
|
// predefined valid values
|
||||||
|
type?: 'info' | 'warning' | 'success' | 'danger' | 'dark'
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoBox: React.FC<InfoBoxProps> = ({msg, children, type = 'info'}) => (
|
const InfoBox: React.FC<InfoBoxProps> = ({msg, children, type = 'info'}) => (
|
||||||
|
@ -142,8 +142,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
|||||||
{
|
{
|
||||||
mapStyle: 'community_expected_planning_application_total',
|
mapStyle: 'community_expected_planning_application_total',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Expected planning application',
|
title: 'Expected planning applications',
|
||||||
description: 'Sites identified by users as likely to be subject to planning application over the next six months',
|
disclaimer: 'Sites identified by users as likely to be subject to planning application over the next six months',
|
||||||
elements: [
|
elements: [
|
||||||
{ color: '#bd0026', text: '100+' },
|
{ color: '#bd0026', text: '100+' },
|
||||||
{ color: '#e31a1c', text: '50–99' },
|
{ color: '#e31a1c', text: '50–99' },
|
||||||
@ -167,23 +167,98 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[Category.Planning]: [{
|
[Category.Planning]: [
|
||||||
mapStyle: 'planning_combined',
|
{
|
||||||
legend: {
|
// this database commad allows to see statistics about decision dates per year
|
||||||
title: 'Designation/protection',
|
// SELECT COUNT(*), date_part('year', decision_date) as year from planning_data WHERE decision_date IS NOT NULL GROUP BY year ORDER BY year ASC;
|
||||||
disclaimer: 'All data relating to designated buildings should be checked against the National Heritage List for England and local authority websites. Designation data is currently incomplete. We are aiming for 100% coverage by April 2023.',
|
// SELECT COUNT(*), date_part('year', registered_with_local_authority_date) as year from planning_data WHERE decision_date IS NOT NULL GROUP BY year ORDER BY year ASC;
|
||||||
elements: [
|
mapStyle: 'planning_applications_status_all',
|
||||||
{ color: '#95beba', text: 'In Conservation Area'},
|
legend: {
|
||||||
{ color: '#c72e08', text: 'Grade I Listed'},
|
title: 'All planning applications available from GLA (official data)',
|
||||||
{ color: '#e75b42', text: 'Grade II* Listed'},
|
disclaimer: 'The map shows official data available from the GLA Planning London Datahub. What you are looking at is mainly applications from 2019 onwards.',
|
||||||
{ color: '#ffbea1', text: 'Grade II Listed'},
|
elements: [
|
||||||
{ color: '#85ffd4', text: 'Heritage at Risk'},
|
{ color: '#a040a0', text: 'Submitted, awaiting decision' },
|
||||||
{ color: '#858eff', text: 'In World Heritage Site'},
|
{ color: '#fff200', text: 'Appeal In Progress' },
|
||||||
{ color: '#8500d4', text: 'In Archaeological Priority Area'},
|
{ color: '#16cf15', text: 'Approved' },
|
||||||
{ color: '#858ed4', text: 'Locally Listed'},
|
{ color: '#e31d23', text: 'Rejected' },
|
||||||
]
|
{ color: '#999999', text: 'Withdrawn' },
|
||||||
|
{ color: '#eacad0', text: 'Other' },
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}],
|
{
|
||||||
|
mapStyle: 'planning_applications_status_recent',
|
||||||
|
legend: {
|
||||||
|
title: 'Last 12 months - planning applications submissions/decisions (official data)',
|
||||||
|
disclaimer: 'The map shows applications where the submission or decision data falls within last 12 months.',
|
||||||
|
elements: [
|
||||||
|
{ color: '#a040a0', text: 'Submitted, awaiting decision' },
|
||||||
|
{ color: '#fff200', text: 'Appeal In Progress' },
|
||||||
|
{ color: '#16cf15', text: 'Approved' },
|
||||||
|
{ color: '#e31d23', text: 'Rejected' },
|
||||||
|
{ color: '#999999', text: 'Withdrawn' },
|
||||||
|
{ color: '#eacad0', text: 'Other' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'planning_applications_status_very_recent',
|
||||||
|
legend: {
|
||||||
|
title: 'Last 30 days - planning applications submissions/decisions (official data)',
|
||||||
|
disclaimer: 'The map shows applications where the submission or decision data falls within last 30 days.',
|
||||||
|
elements: [
|
||||||
|
{ color: '#a040a0', text: 'Submitted, awaiting decision' },
|
||||||
|
{ color: '#fff200', text: 'Appeal In Progress' },
|
||||||
|
{ color: '#16cf15', text: 'Approved' },
|
||||||
|
{ color: '#e31d23', text: 'Rejected' },
|
||||||
|
{ color: '#999999', text: 'Withdrawn' },
|
||||||
|
{ color: '#eacad0', text: 'Other' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'community_expected_planning_application_total',
|
||||||
|
legend: {
|
||||||
|
title: 'Expected planning applications (crowdsourced data)',
|
||||||
|
disclaimer: 'Sites identified by users as likely to be subject to planning application over the next six months',
|
||||||
|
elements: [
|
||||||
|
{ color: '#bd0026', text: '100+' },
|
||||||
|
{ color: '#e31a1c', text: '50–99' },
|
||||||
|
{ color: '#fc4e2a', text: '20–49' },
|
||||||
|
{ color: '#fd8d3c', text: '10–19' },
|
||||||
|
{ color: '#feb24c', text: '3–9' },
|
||||||
|
{ color: '#fed976', text: '2' },
|
||||||
|
{ color: '#ffe8a9', text: '1'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'planning_combined',
|
||||||
|
legend: {
|
||||||
|
title: 'Designation/protection (official and crowdsourced data)',
|
||||||
|
disclaimer: 'All data relating to designated buildings should be checked against the National Heritage List for England and local authority websites. Designation data is currently incomplete. We are aiming for 100% coverage by April 2023.',
|
||||||
|
elements: [
|
||||||
|
{ color: '#95beba', text: 'In Conservation Area'},
|
||||||
|
{ color: '#c72e08', text: 'Grade I Listed'},
|
||||||
|
{ color: '#e75b42', text: 'Grade II* Listed'},
|
||||||
|
{ color: '#ffbea1', text: 'Grade II Listed'},
|
||||||
|
{ color: '#85ffd4', text: 'Heritage at Risk'},
|
||||||
|
{ color: '#858ed4', text: 'Locally Listed'},
|
||||||
|
{ color: '#858eff', text: 'In World Heritage Site'},
|
||||||
|
{ color: '#8500d4', text: 'In Archaeological Priority Area'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'empty_map',
|
||||||
|
legend: {
|
||||||
|
title: 'Empty map',
|
||||||
|
disclaimer: 'This is an empty map to see overlays without distraction.',
|
||||||
|
elements: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
[Category.Sustainability]: [{
|
[Category.Sustainability]: [{
|
||||||
mapStyle: 'sust_dec',
|
mapStyle: 'sust_dec',
|
||||||
legend: {
|
legend: {
|
||||||
@ -223,12 +298,12 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
|||||||
{ color: '#7025a6', text: 'Residential (verified)' },
|
{ color: '#7025a6', text: 'Residential (verified)' },
|
||||||
{ color: '#ff8c00', text: 'Retail' },
|
{ color: '#ff8c00', text: 'Retail' },
|
||||||
{ color: '#f5f58f', text: 'Industry & Business' },
|
{ color: '#f5f58f', text: 'Industry & Business' },
|
||||||
{ color: '#73ccd1', text: 'Community Services' },
|
{ color: '#fa667d', text: 'Community Services' },
|
||||||
{ color: '#ffbfbf', text: 'Recreation & Leisure' },
|
{ color: '#ffbfbf', text: 'Recreation & Leisure' },
|
||||||
{ color: '#b3de69', text: 'Transport' },
|
{ color: '#b3de69', text: 'Transport' },
|
||||||
{ color: '#cccccc', text: 'Utilities & Infrastructure' },
|
{ color: '#cccccc', text: 'Utilities & Infrastructure' },
|
||||||
{ color: '#898944', text: 'Defence' },
|
{ color: '#898944', text: 'Defence' },
|
||||||
{ color: '#fa667d', text: 'Agriculture' },
|
{ color: '#73ccd1', text: 'Agriculture' },
|
||||||
{ color: '#45cce3', text: 'Minerals' },
|
{ color: '#45cce3', text: 'Minerals' },
|
||||||
{ color: '#ffffff', text: 'Vacant & Derelict' },
|
{ color: '#ffffff', text: 'Vacant & Derelict' },
|
||||||
{ color: '#6c6f8e', text: 'Unclassified, presumed non-residential' }
|
{ color: '#6c6f8e', text: 'Unclassified, presumed non-residential' }
|
||||||
|
@ -50,6 +50,8 @@ export interface DataFieldDefinition {
|
|||||||
*
|
*
|
||||||
* Making it semantically correct is useful but not necessary.
|
* Making it semantically correct is useful but not necessary.
|
||||||
* E.g. for building attachment form, you could use "Detached" as example
|
* E.g. for building attachment form, you could use "Detached" as example
|
||||||
|
*
|
||||||
|
* This field is later processed by AttributesBasedOnExample
|
||||||
*/
|
*/
|
||||||
example: any;
|
example: any;
|
||||||
|
|
||||||
@ -109,7 +111,7 @@ export const buildingUserFields = {
|
|||||||
community_expected_planning_application: {
|
community_expected_planning_application: {
|
||||||
perUser: true,
|
perUser: true,
|
||||||
category: Category.Community,
|
category: Category.Community,
|
||||||
title: "Select any building that you think may be subject to a planning application over the next six months and tick the box below to colour it.",
|
title: "Do you think that this building may be subject to a planning application, involving demolition, over the next six months?",
|
||||||
example: true
|
example: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -170,6 +172,16 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
example: [{uprn: "", parent_uprn: "" }, {uprn: "", parent_uprn: "" }],
|
example: [{uprn: "", parent_uprn: "" }, {uprn: "", parent_uprn: "" }],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
planning_data: {
|
||||||
|
category: Category.Location,
|
||||||
|
title: "PLANNING DATA",
|
||||||
|
tooltip: "PLANNING DATA",
|
||||||
|
example: [{uprn: "", building_id: 1, data_source: ""},
|
||||||
|
{uprn: "", building_id: 1, data_source: "", status: "", status_before_aliasing: "", decision_date: "", description: "", planning_application_link: "", registered_with_local_authority_date: "", last_synced_date: "", data_source_link: "", address: ""},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
ref_osm_id: {
|
ref_osm_id: {
|
||||||
category: Category.Location,
|
category: Category.Location,
|
||||||
title: "OSM ID",
|
title: "OSM ID",
|
||||||
@ -440,9 +452,17 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
edit_history: {
|
||||||
|
category: Category.Planning,
|
||||||
|
title: "PLANNING DATA",
|
||||||
|
tooltip: "PLANNING DATA",
|
||||||
|
example: [{}],
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
planning_portal_link: {
|
planning_portal_link: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Planning portal link",
|
title: "Local authority planning application link",
|
||||||
example: "",
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
@ -452,6 +472,24 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
example: "",
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
planning_crowdsourced_site_completion_status: {
|
||||||
|
category: Category.Planning,
|
||||||
|
title: "Has the work on this site been completed?",
|
||||||
|
example: true,
|
||||||
|
//tooltip: ,
|
||||||
|
},
|
||||||
|
planning_crowdsourced_site_completion_year: {
|
||||||
|
category: Category.Planning,
|
||||||
|
title: "Year of completion if known",
|
||||||
|
example: 2022,
|
||||||
|
//tooltip: ,
|
||||||
|
},
|
||||||
|
planning_crowdsourced_planning_id: {
|
||||||
|
category: Category.Planning,
|
||||||
|
title: "If you know of a planning application that has been recently submitted for this site, and is not listed in the blue box above, please enter its planning application ID below:",
|
||||||
|
example: "1112/QWERTY",
|
||||||
|
//tooltip: ,
|
||||||
|
},
|
||||||
planning_in_conservation_area_id: {
|
planning_in_conservation_area_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Conservation Area identifier",
|
title: "Conservation Area identifier",
|
||||||
@ -466,7 +504,7 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
},
|
},
|
||||||
planning_list_id: {
|
planning_list_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is the building on the <a href=\"https://historicengland.org.uk/advice/hpg/heritage-assets/nhle/\" target=\"_blank\">National Heritage List for England</a>?",
|
title: "If the building is on the <a href=\"https://historicengland.org.uk/advice/hpg/heritage-assets/nhle/\" target=\"_blank\">National Heritage List for England (NHLE)</a> please add the ID:",
|
||||||
example: "121436",
|
example: "121436",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
@ -478,13 +516,13 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
|||||||
},
|
},
|
||||||
planning_heritage_at_risk_url: {
|
planning_heritage_at_risk_url: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is it on the <a href=\"https://historicengland.org.uk/advice/heritage-at-risk/search-register/\" target=\"_blank\">Heritage at Risk</a> register?",
|
title: "If the building is on the <a href=\"https://historicengland.org.uk/advice/heritage-at-risk/search-register/\" target=\"_blank\">Heritage at Risk</a> register please add the ID:",
|
||||||
example: "",
|
example: "",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
planning_world_list_id: {
|
planning_world_list_id: {
|
||||||
category: Category.Planning,
|
category: Category.Planning,
|
||||||
title: "Is it within a <a href=\"https://historicengland.org.uk/advice/hpg/has/whs/\" target=\"_blank\">World Heritage Site</a>?",
|
title: "If the building is on a <a href=\"https://historicengland.org.uk/advice/hpg/has/whs/\" target=\"_blank\">World Heritage Site</a> please add the ID:",
|
||||||
example: "488",
|
example: "488",
|
||||||
//tooltip: ,
|
//tooltip: ,
|
||||||
},
|
},
|
||||||
|
@ -8,9 +8,13 @@ export const initialMapViewport: MapViewport = {
|
|||||||
zoom: 16,
|
zoom: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MapTheme = 'light' | 'night';
|
export type MapTheme = 'light' | 'night' | 'night_outlines' | 'boroughs';
|
||||||
|
|
||||||
|
export type LayerEnablementState = 'enabled' | 'disabled';
|
||||||
|
|
||||||
export const mapBackgroundColor: Record<MapTheme, string> = {
|
export const mapBackgroundColor: Record<MapTheme, string> = {
|
||||||
light: '#F0EEEB',
|
light: '#F0EEEB',
|
||||||
night: '#162639'
|
night: '#162639',
|
||||||
|
night_outlines: '#162639',
|
||||||
|
boroughs: '#ff0000',
|
||||||
};
|
};
|
||||||
|
@ -11,13 +11,17 @@ export type BuildingMapTileset = 'date_year' |
|
|||||||
'community_local_significance_total' |
|
'community_local_significance_total' |
|
||||||
'community_expected_planning_application_total' |
|
'community_expected_planning_application_total' |
|
||||||
'community_in_public_ownership' |
|
'community_in_public_ownership' |
|
||||||
|
'planning_applications_status_all' |
|
||||||
|
'planning_applications_status_recent' |
|
||||||
|
'planning_applications_status_very_recent' |
|
||||||
'planning_combined' |
|
'planning_combined' |
|
||||||
|
'empty_map' |
|
||||||
'sust_dec' |
|
'sust_dec' |
|
||||||
'building_attachment_form' |
|
'building_attachment_form' |
|
||||||
'landuse' |
|
'landuse' |
|
||||||
'dynamics_demolished_count' |
|
'dynamics_demolished_count' |
|
||||||
'team';
|
'team';
|
||||||
|
|
||||||
export type SpecialMapTileset = 'base_light' | 'base_night' | 'highlight' | 'number_labels';
|
export type SpecialMapTileset = 'base_light' | 'base_night' | 'base_night_outlines' | 'highlight' | 'number_labels' | 'base_boroughs';
|
||||||
|
|
||||||
export type MapTileset = BuildingMapTileset | SpecialMapTileset;
|
export type MapTileset = BuildingMapTileset | SpecialMapTileset;
|
||||||
|
274
app/src/frontend/displayPreferences-context.tsx
Normal file
274
app/src/frontend/displayPreferences-context.tsx
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { LayerEnablementState, MapTheme } from './config/map-config';
|
||||||
|
|
||||||
|
interface DisplayPreferencesContextState {
|
||||||
|
vista: LayerEnablementState;
|
||||||
|
vistaSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
vistaSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
flood: LayerEnablementState;
|
||||||
|
floodSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
floodSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
creative: LayerEnablementState;
|
||||||
|
creativeSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
creativeSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
housing: LayerEnablementState;
|
||||||
|
housingSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
housingSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
conservation: LayerEnablementState;
|
||||||
|
conservationSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
conservationSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
parcel: LayerEnablementState;
|
||||||
|
parcelSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
parcelSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
borough: LayerEnablementState;
|
||||||
|
boroughSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
boroughSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
historicData: LayerEnablementState;
|
||||||
|
historicDataSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
historicDataSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
|
||||||
|
darkLightTheme: MapTheme;
|
||||||
|
darkLightThemeSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
darkLightThemeSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stub = (): never => {
|
||||||
|
throw new Error('DisplayPreferencesProvider not set up');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DisplayPreferencesContext = createContext<DisplayPreferencesContextState>({
|
||||||
|
vista: undefined,
|
||||||
|
vistaSwitch: stub,
|
||||||
|
vistaSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
flood: undefined,
|
||||||
|
floodSwitch: stub,
|
||||||
|
floodSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
creative: undefined,
|
||||||
|
creativeSwitch: stub,
|
||||||
|
creativeSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
housing: undefined,
|
||||||
|
housingSwitch: stub,
|
||||||
|
housingSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
conservation: undefined,
|
||||||
|
conservationSwitch: stub,
|
||||||
|
conservationSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
parcel: undefined,
|
||||||
|
parcelSwitch: stub,
|
||||||
|
parcelSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
borough: undefined,
|
||||||
|
boroughSwitch: stub,
|
||||||
|
boroughSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
historicData: undefined,
|
||||||
|
historicDataSwitch: stub,
|
||||||
|
historicDataSwitchOnClick: undefined,
|
||||||
|
|
||||||
|
darkLightTheme: undefined,
|
||||||
|
darkLightThemeSwitch: stub,
|
||||||
|
darkLightThemeSwitchOnClick: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
|
||||||
|
const [vista, setVista] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [flood, setFlood] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [creative, setCreative] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [housing, setHousing] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [borough, setBorough] = useState<LayerEnablementState>('enabled');
|
||||||
|
const [parcel, setParcel] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [conservation, setConservation] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [historicData, setHistoricData] = useState<LayerEnablementState>('disabled');
|
||||||
|
const [darkLightTheme, setDarkLightTheme] = useState<MapTheme>('night');
|
||||||
|
|
||||||
|
const vistaSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newVista = (vista === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setVista(newVista);
|
||||||
|
},
|
||||||
|
[vista],
|
||||||
|
)
|
||||||
|
|
||||||
|
const vistaSwitchOnClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newVista = (vista === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setVista(newVista);
|
||||||
|
}
|
||||||
|
|
||||||
|
const floodSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newFlood = (flood === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setFlood(newFlood);
|
||||||
|
},
|
||||||
|
[flood],
|
||||||
|
)
|
||||||
|
|
||||||
|
const floodSwitchOnClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newFlood = (flood === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setFlood(newFlood);
|
||||||
|
}
|
||||||
|
|
||||||
|
const housingSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newHousing = (housing === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setHousing(newHousing);
|
||||||
|
},
|
||||||
|
[housing],
|
||||||
|
)
|
||||||
|
|
||||||
|
const housingSwitchOnClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newHousing = (housing === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setHousing(newHousing);
|
||||||
|
}
|
||||||
|
|
||||||
|
const creativeSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newCreative = (creative === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setCreative(newCreative);
|
||||||
|
},
|
||||||
|
[creative],
|
||||||
|
)
|
||||||
|
|
||||||
|
const creativeSwitchOnClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newCreative = (creative === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setCreative(newCreative);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boroughSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
flipBorough(e)
|
||||||
|
},
|
||||||
|
[borough],
|
||||||
|
)
|
||||||
|
const boroughSwitchOnClick = (e) => {
|
||||||
|
flipBorough(e)
|
||||||
|
}
|
||||||
|
function flipBorough(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newBorough = (borough === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setBorough(newBorough);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parcelSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
flipParcel(e)
|
||||||
|
},
|
||||||
|
[parcel],
|
||||||
|
)
|
||||||
|
const parcelSwitchOnClick = (e) => {
|
||||||
|
flipParcel(e)
|
||||||
|
}
|
||||||
|
function flipParcel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newParcel = (parcel === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setParcel(newParcel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conservationSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
flipConservation(e)
|
||||||
|
},
|
||||||
|
[conservation],
|
||||||
|
)
|
||||||
|
const conservationSwitchOnClick = (e) => {
|
||||||
|
flipConservation(e)
|
||||||
|
}
|
||||||
|
function flipConservation(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newConservation = (conservation === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setConservation(newConservation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const historicDataSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
flipHistoricData(e)
|
||||||
|
},
|
||||||
|
[historicData],
|
||||||
|
)
|
||||||
|
const historicDataSwitchOnClick = (e) => {
|
||||||
|
flipHistoricData(e)
|
||||||
|
}
|
||||||
|
function flipHistoricData(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newHistoric = (historicData === 'enabled')? 'disabled' : 'enabled';
|
||||||
|
setHistoricData(newHistoric);
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkLightThemeSwitch = useCallback(
|
||||||
|
(e) => {
|
||||||
|
flipDarkLightTheme(e)
|
||||||
|
},
|
||||||
|
[darkLightTheme],
|
||||||
|
)
|
||||||
|
const darkLightThemeSwitchOnClick = (e) => {
|
||||||
|
flipDarkLightTheme(e)
|
||||||
|
}
|
||||||
|
function flipDarkLightTheme(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newDarkLightTheme = (darkLightTheme === 'light')? 'night' : 'light';
|
||||||
|
setDarkLightTheme(newDarkLightTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisplayPreferencesContext.Provider value={{
|
||||||
|
vista,
|
||||||
|
vistaSwitch,
|
||||||
|
vistaSwitchOnClick,
|
||||||
|
flood,
|
||||||
|
floodSwitch,
|
||||||
|
floodSwitchOnClick,
|
||||||
|
creative,
|
||||||
|
creativeSwitch,
|
||||||
|
creativeSwitchOnClick,
|
||||||
|
housing,
|
||||||
|
housingSwitch,
|
||||||
|
housingSwitchOnClick,
|
||||||
|
conservation,
|
||||||
|
conservationSwitch,
|
||||||
|
conservationSwitchOnClick,
|
||||||
|
parcel,
|
||||||
|
parcelSwitch,
|
||||||
|
parcelSwitchOnClick,
|
||||||
|
|
||||||
|
borough,
|
||||||
|
boroughSwitch,
|
||||||
|
boroughSwitchOnClick,
|
||||||
|
|
||||||
|
historicData,
|
||||||
|
historicDataSwitch,
|
||||||
|
historicDataSwitchOnClick,
|
||||||
|
|
||||||
|
darkLightTheme,
|
||||||
|
darkLightThemeSwitch,
|
||||||
|
darkLightThemeSwitchOnClick
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</DisplayPreferencesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDisplayPreferences = (): DisplayPreferencesContextState => {
|
||||||
|
return useContext(DisplayPreferencesContext);
|
||||||
|
};
|
13
app/src/frontend/globalContext.ts
Normal file
13
app/src/frontend/globalContext.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
type UserContextType = {
|
||||||
|
context: string | null,
|
||||||
|
setContext: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const iUserContextState = {
|
||||||
|
context: null,
|
||||||
|
setContext: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserContext = createContext<UserContextType>(iUserContextState)
|
||||||
|
|
||||||
|
export default UserContext
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
|
|
||||||
@ -18,7 +18,8 @@ import Welcome from './pages/welcome';
|
|||||||
import { PrivateRoute } from './route';
|
import { PrivateRoute } from './route';
|
||||||
import { useLastNotEmpty } from './hooks/use-last-not-empty';
|
import { useLastNotEmpty } from './hooks/use-last-not-empty';
|
||||||
import { Category } from './config/categories-config';
|
import { Category } from './config/categories-config';
|
||||||
import { defaultMapCategory } from './config/category-maps-config';
|
import { BuildingMapTileset } from './config/tileserver-config';
|
||||||
|
import { defaultMapCategory, categoryMapsConfig } from './config/category-maps-config';
|
||||||
import { useMultiEditData } from './hooks/use-multi-edit-data';
|
import { useMultiEditData } from './hooks/use-multi-edit-data';
|
||||||
import { useAuth } from './auth-context';
|
import { useAuth } from './auth-context';
|
||||||
import { sendBuildingUpdate } from './api-data/building-update';
|
import { sendBuildingUpdate } from './api-data/building-update';
|
||||||
@ -59,6 +60,15 @@ function setOrToggle<T>(currentValue: T, newValue: T): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useStateWithOptions<T>(defaultValue: T, options: T[]): [T, (x: T) => void] {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
|
||||||
|
const effectiveValue = options.includes(value) ? value : options[0];
|
||||||
|
const handleChange = useCallback((x) => setValue(x), []);
|
||||||
|
|
||||||
|
return [effectiveValue, handleChange];
|
||||||
|
}
|
||||||
|
|
||||||
export const MapApp: React.FC<MapAppProps> = props => {
|
export const MapApp: React.FC<MapAppProps> = props => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [categoryUrlParam] = useUrlCategoryParam();
|
const [categoryUrlParam] = useUrlCategoryParam();
|
||||||
@ -121,6 +131,11 @@ export const MapApp: React.FC<MapAppProps> = props => {
|
|||||||
}
|
}
|
||||||
}, [selectedBuildingId, updateUserVerified, reloadBuilding, userVerified]);
|
}, [selectedBuildingId, updateUserVerified, reloadBuilding, userVerified]);
|
||||||
|
|
||||||
|
|
||||||
|
const categoryMapDefinitions = useMemo(() => categoryMapsConfig[displayCategory], [displayCategory]);
|
||||||
|
const availableMapStyles = useMemo(() => categoryMapDefinitions.map(x => x.mapStyle), [categoryMapDefinitions]);
|
||||||
|
const [mapColourScale, setMapColourScale] = useStateWithOptions<BuildingMapTileset>(undefined, availableMapStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
|
<PrivateRoute path="/:mode(edit|multi-edit)" /> {/* empty private route to ensure auth for editing */}
|
||||||
@ -146,6 +161,8 @@ export const MapApp: React.FC<MapAppProps> = props => {
|
|||||||
user_verified={userVerified ?? {}}
|
user_verified={userVerified ?? {}}
|
||||||
onBuildingUpdate={handleBuildingUpdate}
|
onBuildingUpdate={handleBuildingUpdate}
|
||||||
onUserVerifiedUpdate={handleUserVerifiedUpdate}
|
onUserVerifiedUpdate={handleUserVerifiedUpdate}
|
||||||
|
mapColourScale={mapColourScale}
|
||||||
|
onMapColourScale={setMapColourScale}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -158,9 +175,11 @@ export const MapApp: React.FC<MapAppProps> = props => {
|
|||||||
<ColouringMap
|
<ColouringMap
|
||||||
selectedBuildingId={selectedBuildingId}
|
selectedBuildingId={selectedBuildingId}
|
||||||
mode={mode || 'basic'}
|
mode={mode || 'basic'}
|
||||||
category={displayCategory}
|
|
||||||
revisionId={revisionId}
|
revisionId={revisionId}
|
||||||
onBuildingAction={mode === 'multi-edit' ? colourBuilding : selectBuilding}
|
onBuildingAction={mode === 'multi-edit' ? colourBuilding : selectBuilding}
|
||||||
|
mapColourScale={mapColourScale}
|
||||||
|
onMapColourScale={setMapColourScale}
|
||||||
|
categoryMapDefinitions={categoryMapDefinitions}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
16
app/src/frontend/map/borough-switcher.tsx
Normal file
16
app/src/frontend/map/borough-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const BoroughSwitcher: React.FC<{}> = () => {
|
||||||
|
const { borough, boroughSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`borough-switcher map-button ${darkLightTheme}`} onSubmit={boroughSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(borough === 'enabled')? 'Switch off Borough Boundaries' : 'Switch on Borough Boundaries'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
16
app/src/frontend/map/conservation-switcher.tsx
Normal file
16
app/src/frontend/map/conservation-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const ConservationAreaSwitcher: React.FC<{}> = (props) => {
|
||||||
|
const { conservation, conservationSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`conservation-switcher map-button ${darkLightTheme}`} onSubmit={conservationSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(conservation === 'enabled')? 'Switch off Conservation Areas' : 'Switch on Conservation Areas'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
16
app/src/frontend/map/creative-switcher.tsx
Normal file
16
app/src/frontend/map/creative-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const CreativeSwitcher: React.FC<{}> = () => {
|
||||||
|
const { creative, creativeSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`creative-switcher map-button ${darkLightTheme}`} onSubmit={creativeSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(creative === 'enabled')? 'Switch off Creative Enterprise Zones' : 'Switch on Creative Enterprise Zones'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
23
app/src/frontend/map/data-switcher.tsx
Normal file
23
app/src/frontend/map/data-switcher.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
interface DataLayerSwitcherProps {
|
||||||
|
currentDisplay: string;
|
||||||
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataLayerSwitcher: React.FC<DataLayerSwitcherProps> = (props) => {
|
||||||
|
const { darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`data-switcher map-button ${darkLightTheme}`} onSubmit={props.onSubmit}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(props.currentDisplay === 'enabled')? 'Hide layer options' : 'Show layer options'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DataLayerSwitcher;
|
16
app/src/frontend/map/flood-switcher.tsx
Normal file
16
app/src/frontend/map/flood-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const FloodSwitcher: React.FC<{}> = () => {
|
||||||
|
const { flood, floodSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`flood-switcher map-button ${darkLightTheme}`} onSubmit={floodSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(flood === 'enabled')? 'Switch off Flood Zones' : 'Switch on Flood Zones'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
16
app/src/frontend/map/historic-data-switcher.tsx
Normal file
16
app/src/frontend/map/historic-data-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const HistoricDataSwitcher: React.FC<{}> = (props) => {
|
||||||
|
const { historicData, historicDataSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`historic-data-switcher map-button ${darkLightTheme}`} onSubmit={historicDataSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(historicData === 'enabled')? 'Switch off the OS 1890s Historical Map' : 'Switch on the OS 1890s Historical Map'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
16
app/src/frontend/map/housing-switcher.tsx
Normal file
16
app/src/frontend/map/housing-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const HousingSwitcher: React.FC<{}> = () => {
|
||||||
|
const { housing, housingSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`housing-switcher map-button ${darkLightTheme}`} onSubmit={housingSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(housing === 'enabled')? 'Switch off Housing Zones' : 'Switch on Housing Zones'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
37
app/src/frontend/map/layers/borough-boundary-layer.tsx
Normal file
37
app/src/frontend/map/layers/borough-boundary-layer.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { BuildingBaseLayerAllZoom } from './building-base-layer-all-zoom';
|
||||||
|
|
||||||
|
export function BoroughBoundaryLayer({}) {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { borough } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/boroughs.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(borough == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<><GeoJSON
|
||||||
|
attribution='Borough boundary from <a href=https://data.london.gov.uk/dataset/london_boroughs>London Datastore</a> Ordnance Survey Open Data - Contains public sector information licensed under the <a href=https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>Open Government Licence v3.0</a>'
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#f00', fill: false, weight: 1}}
|
||||||
|
/* minNativeZoom={17}*/
|
||||||
|
/><BuildingBaseLayerAllZoom theme="boroughs" /></>;
|
||||||
|
} else if (borough == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
// do not display anything
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#0f0', fill: false, weight: 1}} />
|
||||||
|
} else {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON data={boundaryGeojson} style={{color: '#0f0', fill: true}}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
19
app/src/frontend/map/layers/building-base-layer-all-zoom.tsx
Normal file
19
app/src/frontend/map/layers/building-base-layer-all-zoom.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { MapTheme } from '../../config/map-config';
|
||||||
|
import { MapTileset } from '../../config/tileserver-config';
|
||||||
|
|
||||||
|
import {getTileLayerUrl } from './get-tile-layer-url';
|
||||||
|
|
||||||
|
export function BuildingBaseLayerAllZoom({ theme }: {theme: MapTheme}) {
|
||||||
|
const tileset = `base_${theme}` as const;
|
||||||
|
|
||||||
|
return <TileLayer
|
||||||
|
key={theme} /* needed because TileLayer url is not mutable in react-leaflet v3 */
|
||||||
|
url={getTileLayerUrl(tileset)}
|
||||||
|
minZoom={1}
|
||||||
|
maxZoom={109}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
30
app/src/frontend/map/layers/conservation-boundary-layer.tsx
Normal file
30
app/src/frontend/map/layers/conservation-boundary-layer.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
|
||||||
|
export function ConservationAreaBoundaryLayer({}) {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { conservation } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/conservation_areas.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(conservation == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution='Conservation areas by <a href=http://www.bedfordpark.net/leo/planning/>Ian Hall</a> on <a href=https://creativecommons.org/licenses/by/4.0/legalcode>CC-BY 4.0 licence</a>, contains data under <a href=https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>the Open Government Licence v.3.0</a>'
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#cd7090', fill: true, weight: 3, opacity: 1, fillOpacity: 0.3}}
|
||||||
|
/>;
|
||||||
|
} else if (conservation == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
} else {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON data={boundaryGeojson} style={{color: '#fff', fill: true}}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
app/src/frontend/map/layers/creative-boundary-layer.tsx
Normal file
26
app/src/frontend/map/layers/creative-boundary-layer.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function CreativeBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { creative } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/creative_enterprise_zones.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(creative == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution="Creative Enterprise Zones data from <a href=https://apps.london.gov.uk/planning/?_gl=1*avicz4*_ga*MTg1MjY3MzMuMTY2NzcxMjIwMg..*_ga_PY4SWZN1RJ*MTY2NzcxMjI1NS4xLjAuMTY2NzcxMjI1NS42MC4wLjA>PLanning Datamap</a> licence: <a href=https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>Open Government Licence v3.0</a> The boundaries are based on Ordnance Survey mapping and the data is published under Ordnance Survey's 'presumption to publish'. Contains OS data © Crown copyright and database rights 2019."
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#f0f', fill: true, weight: 1, opacity: 0.6}}
|
||||||
|
/>;
|
||||||
|
} else if (creative == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
}
|
27
app/src/frontend/map/layers/flood-boundary-layer.tsx
Normal file
27
app/src/frontend/map/layers/flood-boundary-layer.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function FloodBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { flood } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/flood_zones_simplified.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(flood == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution='Flood zone from <a href=https://data.london.gov.uk/dataset/flood-risk-zones>London Datastore</a>: © Environment Agency copyright and/or database right 2017. All rights reserved. Some features of this map are based on digital spatial data from the Centre for Ecology & Hydrology, © NERC (CEH) © Crown copyright and database rights 2017 Ordnance Survey 100024198'
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#00f', fill: true, weight: 1, opacity: 0.6}}
|
||||||
|
/>;
|
||||||
|
} else if (flood == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
app/src/frontend/map/layers/historic-data-layer.tsx
Normal file
18
app/src/frontend/map/layers/historic-data-layer.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
import { LayerEnablementState } from '../../config/map-config';
|
||||||
|
import { BuildingBaseLayerAllZoom } from './building-base-layer-all-zoom';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function HistoricDataLayer({}) {
|
||||||
|
const { historicData } = useDisplayPreferences();
|
||||||
|
if(historicData == "enabled") {
|
||||||
|
return <><TileLayer
|
||||||
|
url="https://mapseries-tilesets.s3.amazonaws.com/london_1890s/{z}/{x}/{y}.png"
|
||||||
|
attribution='© CC BY 4.0 - Reproduced with the permission of the <a href="https://maps.nls.uk/">National Library of Scotland</a>'
|
||||||
|
/><BuildingBaseLayerAllZoom theme="night_outlines" /></>
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
app/src/frontend/map/layers/housing-boundary-layer.tsx
Normal file
26
app/src/frontend/map/layers/housing-boundary-layer.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function HousingBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { housing } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/housing_zones.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(housing == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution="Housing Zones from <a href=https://data.london.gov.uk/dataset/housing_zones>London Datastore</a>. The boundaries are based on Ordnance Survey mapping and the data is published under Ordnance Survey's 'presumption to publish'. Contains OS data © Crown copyright and database rights 2019. Greater London Authority - Contains public sector information licensed under the <a href=https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>Open Government Licence v3.0</a>'"
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#FF8000', fill: true, weight: 1, opacity: 0.6}}
|
||||||
|
/>;
|
||||||
|
} else if (housing == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
}
|
37
app/src/frontend/map/layers/parcel-boundary-layer.tsx
Normal file
37
app/src/frontend/map/layers/parcel-boundary-layer.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { LayerEnablementState } from '../../config/map-config';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function ParcelBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { parcel } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/parcels_city_of_london.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(parcel == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution='Parcel boundary from <a href=https://use-land-property-data.service.gov.uk/datasets/inspire/download>Index polygons spatial data (INSPIRE)</a> - <a href=www.nationalarchives.gov.uk/doc/open-government-licence/version/3/>Open Government Licence v3</a>'
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#ff0', fill: false, weight: 1}}
|
||||||
|
/* minNativeZoom={17}*/
|
||||||
|
/>;
|
||||||
|
} else if (parcel == "disabled") {
|
||||||
|
return <div></div>
|
||||||
|
// do not display anything
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#0f0', fill: false, weight: 1}} />
|
||||||
|
} else {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON data={boundaryGeojson} style={{color: '#0f0', fill: true}}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
26
app/src/frontend/map/layers/vista-boundary-layer.tsx
Normal file
26
app/src/frontend/map/layers/vista-boundary-layer.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||||
|
|
||||||
|
export function VistaBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
const { vista } = useDisplayPreferences();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/protected_vistas.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if(vista == "enabled") {
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON
|
||||||
|
attribution=' London Views Management Framework (LVMF) – Extended background vistas from <a href=https://data.london.gov.uk/dataset/london-views-management-framework-lvmf-extended-background-vistas>London Datastore</a>: <a href=https://creativecommons.org/licenses/by/4.0/legalcode>CC-BY-SA 4.0</a> by Greater London Authority (GLA)'
|
||||||
|
data={boundaryGeojson}
|
||||||
|
style={{color: '#0f0', fill: true, weight: 1, opacity: 0.6}}
|
||||||
|
/>;
|
||||||
|
} else {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,7 @@ export const Legend : FC<LegendProps> = ({
|
|||||||
<Logo variant="default" />
|
<Logo variant="default" />
|
||||||
{
|
{
|
||||||
mapColourScaleDefinitions.length > 1 ?
|
mapColourScaleDefinitions.length > 1 ?
|
||||||
<select className='style-select' onChange={e => onMapColourScale(e.target.value as BuildingMapTileset)}>
|
<select className='style-select' onChange={e => onMapColourScale(e.target.value as BuildingMapTileset)} value={mapColourScale}>
|
||||||
{
|
{
|
||||||
mapColourScaleDefinitions.map(def =>
|
mapColourScaleDefinitions.map(def =>
|
||||||
<option key={def.mapStyle} value={def.mapStyle}>{def.legend.title}</option>
|
<option key={def.mapStyle} value={def.mapStyle}>{def.legend.title}</option>
|
||||||
@ -81,7 +81,7 @@ export const Legend : FC<LegendProps> = ({
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.length === 0 ?
|
elements.length === 0 ?
|
||||||
<p className="data-intro">Coming soon…</p> :
|
( disclaimer ? <ul className={collapseList ? 'collapse data-legend' : 'data-legend'} ><p className='legend-disclaimer'>{disclaimer}</p></ul> : <p className="data-intro">Coming soon…</p>) :
|
||||||
<ul className={collapseList ? 'collapse data-legend' : 'data-legend'} >
|
<ul className={collapseList ? 'collapse data-legend' : 'data-legend'} >
|
||||||
{
|
{
|
||||||
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
||||||
|
82
app/src/frontend/map/map-button.css
Normal file
82
app/src/frontend/map/map-button.css
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
.night-theme {
|
||||||
|
filter: grayscale(100%) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-button {
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
float: right;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.map-button .btn {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 310px;
|
||||||
|
}
|
||||||
|
.map-button.night .btn {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #343a40;
|
||||||
|
border-color: #343a40;
|
||||||
|
}
|
||||||
|
.map-button.night .btn:hover {
|
||||||
|
color: #343a40;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
border-color: #343a40;
|
||||||
|
}
|
||||||
|
@media (max-width: 990px){
|
||||||
|
.map-button {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switcher {
|
||||||
|
top: 77px;
|
||||||
|
}
|
||||||
|
.theme-switcher .btn {
|
||||||
|
min-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-switcher {
|
||||||
|
top: 117px;
|
||||||
|
}
|
||||||
|
.data-switcher .btn {
|
||||||
|
min-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.borough-switcher {
|
||||||
|
top: 157px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.housing-switcher {
|
||||||
|
top: 197px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creative-switcher {
|
||||||
|
top: 237px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flood-switcher {
|
||||||
|
top: 277px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vista-switcher {
|
||||||
|
top: 317px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conservation-switcher {
|
||||||
|
top: 357px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historic-data-switcher {
|
||||||
|
top: 397px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcel-switcher {
|
||||||
|
top: 437px;
|
||||||
|
}
|
@ -6,13 +6,21 @@ import './map.css';
|
|||||||
|
|
||||||
import { apiGet } from '../apiHelpers';
|
import { apiGet } from '../apiHelpers';
|
||||||
import { HelpIcon } from '../components/icons';
|
import { HelpIcon } from '../components/icons';
|
||||||
import { categoryMapsConfig } from '../config/category-maps-config';
|
|
||||||
import { Category } from '../config/categories-config';
|
import { Category } from '../config/categories-config';
|
||||||
import { initialMapViewport, mapBackgroundColor, MapTheme } from '../config/map-config';
|
import { initialMapViewport, mapBackgroundColor, MapTheme, LayerEnablementState } from '../config/map-config';
|
||||||
|
|
||||||
import { Building } from '../models/building';
|
import { Building } from '../models/building';
|
||||||
|
|
||||||
import { CityBaseMapLayer } from './layers/city-base-map-layer';
|
import { CityBaseMapLayer } from './layers/city-base-map-layer';
|
||||||
import { CityBoundaryLayer } from './layers/city-boundary-layer';
|
import { CityBoundaryLayer } from './layers/city-boundary-layer';
|
||||||
|
import { BoroughBoundaryLayer } from './layers/borough-boundary-layer';
|
||||||
|
import { ParcelBoundaryLayer } from './layers/parcel-boundary-layer';
|
||||||
|
import { HistoricDataLayer } from './layers/historic-data-layer';
|
||||||
|
import { FloodBoundaryLayer } from './layers/flood-boundary-layer';
|
||||||
|
import { ConservationAreaBoundaryLayer } from './layers/conservation-boundary-layer';
|
||||||
|
import { VistaBoundaryLayer } from './layers/vista-boundary-layer';
|
||||||
|
import { HousingBoundaryLayer } from './layers/housing-boundary-layer';
|
||||||
|
import { CreativeBoundaryLayer } from './layers/creative-boundary-layer';
|
||||||
import { BuildingBaseLayer } from './layers/building-base-layer';
|
import { BuildingBaseLayer } from './layers/building-base-layer';
|
||||||
import { BuildingDataLayer } from './layers/building-data-layer';
|
import { BuildingDataLayer } from './layers/building-data-layer';
|
||||||
import { BuildingNumbersLayer } from './layers/building-numbers-layer';
|
import { BuildingNumbersLayer } from './layers/building-numbers-layer';
|
||||||
@ -21,30 +29,44 @@ import { BuildingHighlightLayer } from './layers/building-highlight-layer';
|
|||||||
import { Legend } from './legend';
|
import { Legend } from './legend';
|
||||||
import SearchBox from './search-box';
|
import SearchBox from './search-box';
|
||||||
import ThemeSwitcher from './theme-switcher';
|
import ThemeSwitcher from './theme-switcher';
|
||||||
|
import DataLayerSwitcher from './data-switcher';
|
||||||
|
import { BoroughSwitcher } from './borough-switcher';
|
||||||
|
import { ParcelSwitcher } from './parcel-switcher';
|
||||||
|
import { FloodSwitcher } from './flood-switcher';
|
||||||
|
import { ConservationAreaSwitcher } from './conservation-switcher';
|
||||||
|
import { HistoricDataSwitcher } from './historic-data-switcher';
|
||||||
|
import { VistaSwitcher } from './vista-switcher';
|
||||||
|
import { CreativeSwitcher } from './creative-switcher';
|
||||||
|
import { HousingSwitcher } from './housing-switcher';
|
||||||
import { BuildingMapTileset } from '../config/tileserver-config';
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
import { CategoryMapDefinition } from '../config/category-maps-config';
|
||||||
|
|
||||||
interface ColouringMapProps {
|
interface ColouringMapProps {
|
||||||
selectedBuildingId: number;
|
selectedBuildingId: number;
|
||||||
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||||
category: Category;
|
|
||||||
revisionId: string;
|
revisionId: string;
|
||||||
onBuildingAction: (building: Building) => void;
|
onBuildingAction: (building: Building) => void;
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
|
categoryMapDefinitions: CategoryMapDefinition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColouringMap : FC<ColouringMapProps> = ({
|
export const ColouringMap : FC<ColouringMapProps> = ({
|
||||||
category,
|
|
||||||
mode,
|
mode,
|
||||||
revisionId,
|
revisionId,
|
||||||
onBuildingAction,
|
onBuildingAction,
|
||||||
selectedBuildingId,
|
selectedBuildingId,
|
||||||
|
mapColourScale,
|
||||||
|
onMapColourScale,
|
||||||
|
categoryMapDefinitions,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
|
const { darkLightTheme, darkLightThemeSwitch } = useDisplayPreferences();
|
||||||
const [theme, setTheme] = useState<MapTheme>('night');
|
const [dataLayers, setDataLayers] = useState<LayerEnablementState>('disabled');
|
||||||
const [position, setPosition] = useState(initialMapViewport.position);
|
const [position, setPosition] = useState(initialMapViewport.position);
|
||||||
const [zoom, setZoom] = useState(initialMapViewport.zoom);
|
const [zoom, setZoom] = useState(initialMapViewport.zoom);
|
||||||
|
|
||||||
const [mapColourScale, setMapColourScale] = useState<BuildingMapTileset>();
|
|
||||||
|
|
||||||
const handleLocate = useCallback(
|
const handleLocate = useCallback(
|
||||||
(lat: number, lng: number, zoom: number) => {
|
(lat: number, lng: number, zoom: number) => {
|
||||||
@ -64,23 +86,15 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
|||||||
[onBuildingAction],
|
[onBuildingAction],
|
||||||
)
|
)
|
||||||
|
|
||||||
const themeSwitch = useCallback(
|
const layerSwitch = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newTheme = (theme === 'light')? 'night' : 'light';
|
const newDisplayState = (dataLayers === 'enabled')? 'disabled' : 'enabled';
|
||||||
setTheme(newTheme);
|
setDataLayers(newDisplayState);
|
||||||
},
|
},
|
||||||
[theme],
|
[dataLayers],
|
||||||
)
|
)
|
||||||
|
|
||||||
const categoryMapDefinitions = useMemo(() => categoryMapsConfig[category], [category]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(!categoryMapDefinitions.some(def => def.mapStyle === mapColourScale)) {
|
|
||||||
setMapColourScale(categoryMapDefinitions[0].mapStyle);
|
|
||||||
}
|
|
||||||
}, [categoryMapDefinitions, mapColourScale]);
|
|
||||||
|
|
||||||
const hasSelection = selectedBuildingId != undefined;
|
const hasSelection = selectedBuildingId != undefined;
|
||||||
const isEdit = ['edit', 'multi-edit'].includes(mode);
|
const isEdit = ['edit', 'multi-edit'].includes(mode);
|
||||||
|
|
||||||
@ -96,16 +110,23 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<ClickHandler onClick={handleClick} />
|
<ClickHandler onClick={handleClick} />
|
||||||
<MapBackgroundColor theme={theme} />
|
<MapBackgroundColor theme={darkLightTheme} />
|
||||||
<MapViewport position={position} zoom={zoom} />
|
<MapViewport position={position} zoom={zoom} />
|
||||||
|
|
||||||
<Pane
|
<Pane
|
||||||
key={theme}
|
key={darkLightTheme}
|
||||||
name={'cc-base-pane'}
|
name={'cc-base-pane'}
|
||||||
style={{zIndex: 50}}
|
style={{zIndex: 50}}
|
||||||
>
|
>
|
||||||
<CityBaseMapLayer theme={theme} />
|
<CityBaseMapLayer theme={darkLightTheme} />
|
||||||
<BuildingBaseLayer theme={theme} />
|
<BuildingBaseLayer theme={darkLightTheme} />
|
||||||
|
</Pane>
|
||||||
|
|
||||||
|
<Pane
|
||||||
|
name='cc-overlay-pane-shown-behind-buildings'
|
||||||
|
style={{zIndex: 199}}
|
||||||
|
>
|
||||||
|
<ConservationAreaBoundaryLayer/>
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -120,7 +141,14 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
|||||||
name='cc-overlay-pane'
|
name='cc-overlay-pane'
|
||||||
style={{zIndex: 300}}
|
style={{zIndex: 300}}
|
||||||
>
|
>
|
||||||
<CityBoundaryLayer />
|
<CityBoundaryLayer/>
|
||||||
|
<HistoricDataLayer/>
|
||||||
|
<BoroughBoundaryLayer/>
|
||||||
|
<ParcelBoundaryLayer/>
|
||||||
|
<FloodBoundaryLayer/>
|
||||||
|
<VistaBoundaryLayer/>
|
||||||
|
<HousingBoundaryLayer/>
|
||||||
|
<CreativeBoundaryLayer/>
|
||||||
<BuildingNumbersLayer revisionId={revisionId} />
|
<BuildingNumbersLayer revisionId={revisionId} />
|
||||||
{
|
{
|
||||||
selectedBuildingId &&
|
selectedBuildingId &&
|
||||||
@ -143,8 +171,24 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
|||||||
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={setMapColourScale}/>
|
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={onMapColourScale}/>
|
||||||
<ThemeSwitcher onSubmit={themeSwitch} currentTheme={theme} />
|
<ThemeSwitcher onSubmit={darkLightThemeSwitch} currentTheme={darkLightTheme} />
|
||||||
|
<DataLayerSwitcher onSubmit={layerSwitch} currentDisplay={dataLayers} />
|
||||||
|
{
|
||||||
|
(dataLayers == "enabled") ?
|
||||||
|
<>
|
||||||
|
<BoroughSwitcher/>
|
||||||
|
<ParcelSwitcher/>
|
||||||
|
<FloodSwitcher/>
|
||||||
|
<ConservationAreaSwitcher/>
|
||||||
|
<HistoricDataSwitcher/>
|
||||||
|
<VistaSwitcher />
|
||||||
|
<HousingSwitcher />
|
||||||
|
<CreativeSwitcher />
|
||||||
|
</>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
{/* TODO change remaining ones*/}
|
||||||
<SearchBox onLocate={handleLocate} />
|
<SearchBox onLocate={handleLocate} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
16
app/src/frontend/map/parcel-switcher.tsx
Normal file
16
app/src/frontend/map/parcel-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const ParcelSwitcher: React.FC<{}> = () => {
|
||||||
|
const { parcel, parcelSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`parcel-switcher map-button ${darkLightTheme}`} onSubmit={parcelSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(parcel === 'enabled')? 'Switch off Parcel (sample) overlay' : 'Switch on Parcel (sample) overlay'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -1,36 +0,0 @@
|
|||||||
.night-theme {
|
|
||||||
filter: grayscale(100%) invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-theme {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switcher {
|
|
||||||
z-index: 1000;
|
|
||||||
position: absolute;
|
|
||||||
top: 77px;
|
|
||||||
right: 10px;
|
|
||||||
float: right;
|
|
||||||
background: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.theme-switcher .btn {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.theme-switcher.night .btn {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #343a40;
|
|
||||||
border-color: #343a40;
|
|
||||||
}
|
|
||||||
.theme-switcher.night .btn:hover {
|
|
||||||
color: #343a40;
|
|
||||||
background-color: transparent;
|
|
||||||
background-image: none;
|
|
||||||
border-color: #343a40;
|
|
||||||
}
|
|
||||||
@media (max-width: 990px){
|
|
||||||
.theme-switcher {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import './theme-switcher.css';
|
import './map-button.css';
|
||||||
|
|
||||||
interface ThemeSwitcherProps {
|
interface ThemeSwitcherProps {
|
||||||
currentTheme: string;
|
currentTheme: string;
|
||||||
@ -8,7 +8,7 @@ interface ThemeSwitcherProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = (props) => (
|
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = (props) => (
|
||||||
<form className={`theme-switcher ${props.currentTheme}`} onSubmit={props.onSubmit}>
|
<form className={`theme-switcher map-button ${props.currentTheme}`} onSubmit={props.onSubmit}>
|
||||||
<button className="btn btn-outline btn-outline-dark"
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
type="submit">
|
type="submit">
|
||||||
Switch theme ({(props.currentTheme === 'light')? 'Light' : 'Night'})
|
Switch theme ({(props.currentTheme === 'light')? 'Light' : 'Night'})
|
||||||
|
16
app/src/frontend/map/vista-switcher.tsx
Normal file
16
app/src/frontend/map/vista-switcher.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './map-button.css';
|
||||||
|
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||||
|
|
||||||
|
export const VistaSwitcher: React.FC<{}> = () => {
|
||||||
|
const { vista, vistaSwitch, darkLightTheme } = useDisplayPreferences();
|
||||||
|
return (
|
||||||
|
<form className={`vista-switcher map-button ${darkLightTheme}`} onSubmit={vistaSwitch}>
|
||||||
|
<button className="btn btn-outline btn-outline-dark"
|
||||||
|
type="submit">
|
||||||
|
{(vista === 'enabled')? 'Switch off Protected Vistas' : 'Switch on Protected Vistas'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import serialize from 'serialize-javascript';
|
|||||||
import {
|
import {
|
||||||
getBuildingById,
|
getBuildingById,
|
||||||
getBuildingUPRNsById,
|
getBuildingUPRNsById,
|
||||||
|
getBuildingPlanningDataById,
|
||||||
getLatestRevisionId,
|
getLatestRevisionId,
|
||||||
getUserVerifiedAttributes
|
getUserVerifiedAttributes
|
||||||
} from './api/services/building/base';
|
} from './api/services/building/base';
|
||||||
@ -33,13 +34,14 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let [user, building, uprns, userVerified, latestRevisionId] = await Promise.all([
|
let [user, building, uprns, planningData, userVerified, latestRevisionId] = await Promise.all([
|
||||||
userId ? getUserById(userId) : undefined,
|
userId ? getUserById(userId) : undefined,
|
||||||
isBuilding ? getBuildingById(
|
isBuilding ? getBuildingById(
|
||||||
buildingId,
|
buildingId,
|
||||||
{ userDataOptions: userId ? { userId, userAttributes: true } : null }
|
{ userDataOptions: userId ? { userId, userAttributes: true } : null }
|
||||||
) : undefined,
|
) : undefined,
|
||||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||||
|
isBuilding ? getBuildingPlanningDataById(buildingId) : undefined,
|
||||||
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||||
getLatestRevisionId()
|
getLatestRevisionId()
|
||||||
]);
|
]);
|
||||||
@ -53,6 +55,9 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
|||||||
if (data.building != null) {
|
if (data.building != null) {
|
||||||
data.building.uprns = uprns;
|
data.building.uprns = uprns;
|
||||||
}
|
}
|
||||||
|
if (data.building != null) {
|
||||||
|
data.building.planning_data = planningData;
|
||||||
|
}
|
||||||
data.latestRevisionId = latestRevisionId;
|
data.latestRevisionId = latestRevisionId;
|
||||||
renderHTML(context, data, req, res);
|
renderHTML(context, data, req, res);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
|
@ -13,6 +13,17 @@ const LAYER_QUERIES = {
|
|||||||
geometry_id
|
geometry_id
|
||||||
FROM
|
FROM
|
||||||
buildings`,
|
buildings`,
|
||||||
|
base_night_outlines: `
|
||||||
|
SELECT
|
||||||
|
geometry_id
|
||||||
|
FROM
|
||||||
|
buildings`,
|
||||||
|
base_boroughs: `
|
||||||
|
SELECT
|
||||||
|
geometry_id,
|
||||||
|
name
|
||||||
|
FROM
|
||||||
|
external_data_borough_boundary`,
|
||||||
number_labels:`
|
number_labels:`
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id,
|
geometry_id,
|
||||||
@ -136,6 +147,25 @@ const LAYER_QUERIES = {
|
|||||||
WHERE
|
WHERE
|
||||||
community_public_ownership IS NOT NULL
|
community_public_ownership IS NOT NULL
|
||||||
`,
|
`,
|
||||||
|
planning_applications_status_all: `SELECT
|
||||||
|
buildings.geometry_id, building_properties.uprn, building_properties.building_id, planning_data.status AS status, planning_data.uprn
|
||||||
|
FROM building_properties
|
||||||
|
INNER JOIN planning_data ON building_properties.uprn = planning_data.uprn
|
||||||
|
INNER JOIN buildings ON building_properties.building_id = buildings.building_id`,
|
||||||
|
planning_applications_status_recent: `SELECT
|
||||||
|
buildings.geometry_id, building_properties.uprn, building_properties.building_id, planning_data.status AS status, planning_data.uprn,
|
||||||
|
1 AS days_since_decision_date,
|
||||||
|
1 AS days_since_registered_with_local_authority_date
|
||||||
|
FROM building_properties
|
||||||
|
INNER JOIN planning_data ON building_properties.uprn = planning_data.uprn
|
||||||
|
INNER JOIN buildings ON building_properties.building_id = buildings.building_id`,
|
||||||
|
planning_applications_status_very_recent: `SELECT
|
||||||
|
buildings.geometry_id, building_properties.uprn, building_properties.building_id, planning_data.status AS status, planning_data.uprn,
|
||||||
|
planning_data.days_since_decision_date_cached AS days_since_decision_date,
|
||||||
|
planning_data.days_since_registration_cached AS days_since_registered_with_local_authority_date
|
||||||
|
FROM building_properties
|
||||||
|
INNER JOIN planning_data ON building_properties.uprn = planning_data.uprn
|
||||||
|
INNER JOIN buildings ON building_properties.building_id = buildings.building_id`,
|
||||||
planning_combined: `
|
planning_combined: `
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id,
|
geometry_id,
|
||||||
@ -161,6 +191,13 @@ const LAYER_QUERIES = {
|
|||||||
OR planning_heritage_at_risk_url <> ''
|
OR planning_heritage_at_risk_url <> ''
|
||||||
OR planning_in_apa_url <> ''
|
OR planning_in_apa_url <> ''
|
||||||
`,
|
`,
|
||||||
|
empty_map: `
|
||||||
|
SELECT
|
||||||
|
geometry_id
|
||||||
|
FROM
|
||||||
|
buildings
|
||||||
|
WHERE
|
||||||
|
sust_dec IS NOT NULL`,
|
||||||
conservation_area: `
|
conservation_area: `
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id
|
geometry_id
|
||||||
@ -221,6 +258,29 @@ function getDataConfig(tileset: string): DataConfig {
|
|||||||
throw new Error('Invalid tileset requested');
|
throw new Error('Invalid tileset requested');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(tileset == 'base_boroughs') {
|
||||||
|
const query = `(
|
||||||
|
SELECT
|
||||||
|
d.*,
|
||||||
|
g.geometry_geom
|
||||||
|
FROM (
|
||||||
|
${table}
|
||||||
|
) AS d
|
||||||
|
JOIN
|
||||||
|
geometries AS g
|
||||||
|
ON d.geometry_id = g.geometry_id
|
||||||
|
JOIN
|
||||||
|
external_data_borough_boundary AS b
|
||||||
|
ON d.geometry_id = b.geometry_id
|
||||||
|
) AS data
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
geometry_field: GEOMETRY_FIELD,
|
||||||
|
table: query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const query = `(
|
const query = `(
|
||||||
SELECT
|
SELECT
|
||||||
d.*,
|
d.*,
|
||||||
|
@ -35,11 +35,11 @@ if(!allLayersCacheSwitch) {
|
|||||||
// cache age data and base building outlines for more zoom levels than other layers
|
// cache age data and base building outlines for more zoom levels than other layers
|
||||||
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
||||||
(tileset === 'date_year' && z <= 16) ||
|
(tileset === 'date_year' && z <= 16) ||
|
||||||
(['base_light', 'base_night'].includes(tileset) && z <= 17) ||
|
(['base_light', 'base_night', 'base_night_outlines', 'base_boroughs'].includes(tileset) && z <= 17) ||
|
||||||
z <= 13;
|
z <= 13;
|
||||||
} else {
|
} else {
|
||||||
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
||||||
['base_light', 'base_night'].includes(tileset) && z <= 17;
|
['base_light', 'base_night', 'base_night_outlines', 'base_boroughs'].includes(tileset) && z <= 17;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tileCache = new TileCache(
|
const tileCache = new TileCache(
|
||||||
@ -52,8 +52,8 @@ const tileCache = new TileCache(
|
|||||||
},
|
},
|
||||||
shouldCacheFn,
|
shouldCacheFn,
|
||||||
|
|
||||||
// don't clear base_light and base_night on bounding box cache clear
|
// don't clear on bounding box cache clear tilesets not affected by user-editable data
|
||||||
(tileset: string) => tileset !== 'base_light' && tileset !== 'base_night'
|
(tileset: string) => tileset !== 'base_light' && tileset !== 'base_night' && tileset !== 'base_night_outlines' && tileset !== 'base_burough' && tileset !== "planning_applications_status_recent" && tileset !== "planning_applications_status_very_recent" && tileset !== "planning_applications_status_all"
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderBuildingTile = (t: TileParams, d: any) => renderDataSourceTile(t, d, getDataConfig, getLayerVariables);
|
const renderBuildingTile = (t: TileParams, d: any) => renderDataSourceTile(t, d, getDataConfig, getLayerVariables);
|
||||||
@ -63,7 +63,7 @@ function cacheOrCreateBuildingTile(tileParams: TileParams, dataParams: any): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stitchOrRenderBuildingTile(tileParams: TileParams, dataParams: any): Promise<Tile> {
|
function stitchOrRenderBuildingTile(tileParams: TileParams, dataParams: any): Promise<Tile> {
|
||||||
if (tileParams.z <= STITCH_THRESHOLD) {
|
if (tileParams.z <= STITCH_THRESHOLD && tileParams.tileset != "base_boroughs") {
|
||||||
// stitch tile, using cache recursively
|
// stitch tile, using cache recursively
|
||||||
return stitchTile(tileParams, dataParams, cacheOrCreateBuildingTile);
|
return stitchTile(tileParams, dataParams, cacheOrCreateBuildingTile);
|
||||||
} else {
|
} else {
|
||||||
|
2
etl/planning_data/.gitignore
vendored
Normal file
2
etl/planning_data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.json
|
||||||
|
*.sql
|
37
etl/planning_data/README.MD
Normal file
37
etl/planning_data/README.MD
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
Following instructions assume that code is placed within `~/colouring-london/etl/planning_data/`
|
||||||
|
|
||||||
|
To install necessary dependencies use `cd ~/colouring-london/etl/planning_data/ && pip3 install -r requirements.txt`
|
||||||
|
|
||||||
|
Following scripts should be scheduled to run regularly to load livestream data into database.
|
||||||
|
|
||||||
|
```
|
||||||
|
# querying API to obtain data & loading data into Colouring database
|
||||||
|
python3 obtain_livestream_data_and_load_into_database.py
|
||||||
|
|
||||||
|
# removing tile cache for planning applications status layers - note that location of cache depends on your configuration
|
||||||
|
rm /srv/colouring-london/tilecache/planning_applications_status_all/* -rf
|
||||||
|
rm /srv/colouring-london/tilecache/planning_applications_status_recent/* -rf
|
||||||
|
rm /srv/colouring-london/tilecache/planning_applications_status_very_recent/* -rf
|
||||||
|
```
|
||||||
|
|
||||||
|
As loading into databases expects environment variables to be set, one option to actually schedule it in a cron is something like
|
||||||
|
|
||||||
|
```
|
||||||
|
export $(cat ~/scripts/.env | xargs) && /usr/bin/python3 ~/colouring-london/etl/planning_data/obtain_livestream_data_and_load_into_database.py
|
||||||
|
```
|
||||||
|
|
||||||
|
with
|
||||||
|
|
||||||
|
```
|
||||||
|
~/scripts/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
being in following format
|
||||||
|
|
||||||
|
```
|
||||||
|
PGHOST=localhost
|
||||||
|
PGDATABASE=colouringlondondb
|
||||||
|
PGUSER=cldbadmin
|
||||||
|
PGPASSWORD=actualpassword
|
||||||
|
PLANNNING_DATA_API_ALLOW_REQUEST_CODE=requestcode
|
||||||
|
```
|
103
etl/planning_data/address_data.py
Normal file
103
etl/planning_data/address_data.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
def planning_data_entry_to_address(element):
|
||||||
|
site_name = element["_source"].get("site_name")
|
||||||
|
site_number = element["_source"].get("site_number")
|
||||||
|
street_name = element["_source"].get("street_name") # seems often misused - say "31 COPTHALL ROAD EAST" site_name getting Ickenham street_name
|
||||||
|
secondary_street_name = element["_source"].get("secondary_street_name")
|
||||||
|
return generate_address(site_name, site_number, street_name, secondary_street_name)['result']
|
||||||
|
|
||||||
|
def generate_address(site_name, site_number, street_name, secondary_street_name):
|
||||||
|
"""
|
||||||
|
this function generates address from planning data that was provided
|
||||||
|
sadly it does not always works well and relies on many heursitics as data quality is limited
|
||||||
|
"""
|
||||||
|
|
||||||
|
if site_name != None:
|
||||||
|
site_name = site_name.strip()
|
||||||
|
if site_number != None:
|
||||||
|
site_number = site_number.strip()
|
||||||
|
if street_name != None:
|
||||||
|
street_name = street_name.strip()
|
||||||
|
if secondary_street_name != None:
|
||||||
|
secondary_street_name = secondary_street_name.strip()
|
||||||
|
|
||||||
|
if site_name == "":
|
||||||
|
site_name = None
|
||||||
|
if site_number == "":
|
||||||
|
site_number = None
|
||||||
|
if street_name == "":
|
||||||
|
street_name = None
|
||||||
|
if secondary_street_name == "":
|
||||||
|
secondary_street_name = None
|
||||||
|
data = {
|
||||||
|
'site_name': site_name,
|
||||||
|
'site_number': site_number,
|
||||||
|
'street_name': street_name,
|
||||||
|
'secondary_street_name': secondary_street_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if site_name == site_number == street_name == secondary_street_name == None:
|
||||||
|
return {'result': None, 'data': data}
|
||||||
|
|
||||||
|
if secondary_street_name != None:
|
||||||
|
if street_name == None:
|
||||||
|
print('"secondary_street_name != None, street_name == None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, "???????")
|
||||||
|
else:
|
||||||
|
street_name += " - with secondary road name: " + secondary_street_name
|
||||||
|
|
||||||
|
if site_number != None and street_name != None:
|
||||||
|
address = site_number + " " + street_name
|
||||||
|
if site_name != None:
|
||||||
|
print('"site_name != None and site_number != None and street_name != None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, address)
|
||||||
|
|
||||||
|
return {'result': address, 'data': data}
|
||||||
|
|
||||||
|
if site_name != None:
|
||||||
|
if street_name != None:
|
||||||
|
try:
|
||||||
|
if site_number == None and int(site_name):
|
||||||
|
return {'result': site_name + " " + street_name, 'data': data}
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if street_name in site_name:
|
||||||
|
site_name_without_street_name = site_name.replace(street_name, "").strip()
|
||||||
|
try:
|
||||||
|
house_number = int(site_name_without_street_name)
|
||||||
|
# so it appears to be case like
|
||||||
|
# site_name: 5 Warwick Road
|
||||||
|
# street_name: Warwick Road
|
||||||
|
# no other info provided
|
||||||
|
# in such case just returning site_name will work fine...
|
||||||
|
return {'result': site_name, 'data': data}
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
print('"site_name != None and street_name != None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, site_name)
|
||||||
|
if site_number != None:
|
||||||
|
print('"site_name != None and site_number != None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, site_name)
|
||||||
|
return {'result': site_name, 'data': data}
|
||||||
|
else:
|
||||||
|
if street_name != None:
|
||||||
|
if site_number != None:
|
||||||
|
return {'result': site_number + " " + street_name, 'data': data}
|
||||||
|
if street_name != None and site_number == None:
|
||||||
|
print('"street_name != None or site_number == None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, None)
|
||||||
|
return {'result': None, 'data': data}
|
||||||
|
if street_name == None and site_number != None:
|
||||||
|
print('"street_name == None or site_number != None"')
|
||||||
|
show_data(site_name, site_number, street_name, secondary_street_name, None)
|
||||||
|
return {'result': None, 'data': data}
|
||||||
|
return {'result': None, 'data': data}
|
||||||
|
|
||||||
|
|
||||||
|
def show_data(site_name, site_number, street_name, secondary_street_name, address):
|
||||||
|
print("site_name:", site_name)
|
||||||
|
print("site_number:", site_number)
|
||||||
|
print("street_name:", street_name)
|
||||||
|
print("secondary_street_name:", secondary_street_name)
|
||||||
|
print("address generated based on this data:", address)
|
||||||
|
print()
|
||||||
|
print()
|
@ -0,0 +1,310 @@
|
|||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import psycopg2
|
||||||
|
import address_data
|
||||||
|
|
||||||
|
def main():
|
||||||
|
connection = get_connection()
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("TRUNCATE planning_data")
|
||||||
|
|
||||||
|
downloaded = 0
|
||||||
|
last_sort = None
|
||||||
|
search_after = []
|
||||||
|
while True:
|
||||||
|
data = query(search_after).json()
|
||||||
|
load_data_into_database(cursor, data)
|
||||||
|
for entry in data['hits']['hits']:
|
||||||
|
downloaded += 1
|
||||||
|
last_sort = entry['sort']
|
||||||
|
print("downloaded", downloaded, "last_sort", last_sort, "previous", search_after)
|
||||||
|
if search_after == last_sort:
|
||||||
|
break
|
||||||
|
search_after = last_sort
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def load_data_into_database(cursor, data):
|
||||||
|
if "timed_out" not in data:
|
||||||
|
print(json.dumps(data, indent=4))
|
||||||
|
print("timed_out field missing in provided data")
|
||||||
|
else:
|
||||||
|
if data['timed_out']:
|
||||||
|
raise Exception("query getting livestream data has failed")
|
||||||
|
for entry in data['hits']['hits']:
|
||||||
|
try:
|
||||||
|
description = None
|
||||||
|
if entry['_source']['description'] != None:
|
||||||
|
description = entry['_source']['description'].strip()
|
||||||
|
application_id = entry['_source']['lpa_app_no']
|
||||||
|
decision_date = parse_date_string_into_date_object(entry['_source']['decision_date'])
|
||||||
|
last_synced_date = parse_date_string_into_date_object(entry['_source']['last_synced'])
|
||||||
|
uprn = entry['_source']['uprn']
|
||||||
|
status_before_aliasing = entry['_source']['status']
|
||||||
|
status_info = process_status(status_before_aliasing, decision_date)
|
||||||
|
status = status_info["status"]
|
||||||
|
status_explanation_note = status_info["status_explanation_note"]
|
||||||
|
planning_url = obtain_entry_link(entry['_source']['url_planning_app'], application_id)
|
||||||
|
if uprn == None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
uprn = int(uprn)
|
||||||
|
except ValueError as e:
|
||||||
|
print(e)
|
||||||
|
continue
|
||||||
|
entry = {
|
||||||
|
"description": description,
|
||||||
|
"decision_date": decision_date,
|
||||||
|
"last_synced_date": last_synced_date,
|
||||||
|
"application_id": application_id,
|
||||||
|
"application_url": planning_url,
|
||||||
|
"registered_with_local_authority_date": parse_date_string_into_date_object(entry['_source']['valid_date']),
|
||||||
|
"uprn": uprn,
|
||||||
|
"status": status,
|
||||||
|
"status_before_aliasing": status_before_aliasing,
|
||||||
|
"status_explanation_note": status_explanation_note,
|
||||||
|
"data_source": "Greater London Authority's Planning London DataHub",
|
||||||
|
"data_source_link": None,
|
||||||
|
"address": address_data.planning_data_entry_to_address(entry),
|
||||||
|
}
|
||||||
|
if entry["address"] != None:
|
||||||
|
maximum_address_length = 300
|
||||||
|
if len(entry["address"]) > maximum_address_length:
|
||||||
|
print("address is too long, shortening", entry["address"])
|
||||||
|
entry["address"] = entry["address"][0:maximum_address_length]
|
||||||
|
if date_in_future(entry["registered_with_local_authority_date"]):
|
||||||
|
print("registered_with_local_authority_date is treated as invalid:", entry["registered_with_local_authority_date"])
|
||||||
|
# Brent-87_0946 has "valid_date": "23/04/9187"
|
||||||
|
entry["registered_with_local_authority_date"] = None
|
||||||
|
|
||||||
|
if date_in_future(entry["decision_date"]):
|
||||||
|
print("decision_date is treated as invalid:", entry["decision_date"])
|
||||||
|
entry["decision_date"] = None
|
||||||
|
|
||||||
|
if date_in_future(entry["last_synced_date"]):
|
||||||
|
print("last_synced_date is treated as invalid:", entry["last_synced_date"])
|
||||||
|
entry["last_synced_date"] = None
|
||||||
|
|
||||||
|
if "Hackney" in entry["application_id"]:
|
||||||
|
if entry["application_url"] != None:
|
||||||
|
if "https://" not in entry["application_url"]:
|
||||||
|
entry["application_url"] = "https://developmentandhousing.hackney.gov.uk" + entry["application_url"]
|
||||||
|
insert_entry(cursor, entry)
|
||||||
|
except TypeError as e:
|
||||||
|
print()
|
||||||
|
print()
|
||||||
|
print()
|
||||||
|
print(e)
|
||||||
|
print()
|
||||||
|
show_dictionary(entry)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def date_in_future(date):
|
||||||
|
if date == None:
|
||||||
|
return False
|
||||||
|
return date > datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def query(search_after):
|
||||||
|
headers = {
|
||||||
|
'X-API-AllowRequest': os.environ['PLANNNING_DATA_API_ALLOW_REQUEST_CODE'],
|
||||||
|
# Already added when you pass json= but not when you pass data=
|
||||||
|
# 'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
json_data = {
|
||||||
|
'size': 10000,
|
||||||
|
'sort': [
|
||||||
|
{
|
||||||
|
'last_updated': {
|
||||||
|
'order': 'desc',
|
||||||
|
'unmapped_type': 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'stored_fields': [
|
||||||
|
'*',
|
||||||
|
],
|
||||||
|
'_source': {
|
||||||
|
'excludes': [],
|
||||||
|
},
|
||||||
|
'query': {
|
||||||
|
'bool': {
|
||||||
|
'must': [
|
||||||
|
{
|
||||||
|
'range': {
|
||||||
|
'valid_date': {
|
||||||
|
'gte': '01/01/1021',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if search_after != []:
|
||||||
|
json_data['search_after'] = search_after
|
||||||
|
|
||||||
|
print(json_data)
|
||||||
|
return requests.post('https://planningdata.london.gov.uk/api-guest/applications/_search', headers=headers, json=json_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=os.environ['PGHOST'],
|
||||||
|
dbname=os.environ['PGDATABASE'],
|
||||||
|
user=os.environ['PGUSER'],
|
||||||
|
password=os.environ['PGPASSWORD']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filepath():
|
||||||
|
return os.path.dirname(os.path.realpath(__file__)) + os.sep + "data.json"
|
||||||
|
|
||||||
|
|
||||||
|
def insert_entry(cursor, e):
|
||||||
|
try:
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
application_url = None
|
||||||
|
if e["application_url"] != None:
|
||||||
|
application_url = e["application_url"]
|
||||||
|
cursor.execute('''INSERT INTO
|
||||||
|
planning_data (planning_application_id, planning_application_link, description, registered_with_local_authority_date, days_since_registration_cached, decision_date, days_since_decision_date_cached, last_synced_date, status, status_before_aliasing, status_explanation_note, data_source, data_source_link, address, uprn)
|
||||||
|
VALUES
|
||||||
|
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
''', (
|
||||||
|
e["application_id"],
|
||||||
|
application_url, e["description"],
|
||||||
|
date_object_into_date_string(e["registered_with_local_authority_date"]),
|
||||||
|
days_since(e["registered_with_local_authority_date"], now),
|
||||||
|
date_object_into_date_string(e["decision_date"]),
|
||||||
|
days_since(e["decision_date"], now),
|
||||||
|
date_object_into_date_string(e["last_synced_date"]),
|
||||||
|
e["status"],
|
||||||
|
e["status_before_aliasing"],
|
||||||
|
e["status_explanation_note"],
|
||||||
|
e["data_source"],
|
||||||
|
e["data_source_link"],
|
||||||
|
e["address"],
|
||||||
|
e["uprn"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except psycopg2.errors.Error as error:
|
||||||
|
show_dictionary(e)
|
||||||
|
raise error
|
||||||
|
|
||||||
|
|
||||||
|
def show_dictionary(data):
|
||||||
|
for key in data.keys():
|
||||||
|
print(key, "=", data[key])
|
||||||
|
|
||||||
|
|
||||||
|
def days_since(date, now):
|
||||||
|
if(date == None):
|
||||||
|
return None
|
||||||
|
return (now - date).days
|
||||||
|
|
||||||
|
|
||||||
|
def date_object_into_date_string(date):
|
||||||
|
if(date == None):
|
||||||
|
return None
|
||||||
|
return datetime.datetime.strftime(date, "%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date_string_into_date_object(incoming):
|
||||||
|
if incoming == None:
|
||||||
|
return None
|
||||||
|
date = None
|
||||||
|
try:
|
||||||
|
date = datetime.datetime.strptime(incoming, "%d/%m/%Y") # '21/07/2022'
|
||||||
|
except ValueError:
|
||||||
|
date = datetime.datetime.strptime(incoming, "%Y-%m-%dT%H:%M:%S.%fZ") # '2022-08-08T20:07:22.238Z'
|
||||||
|
return date
|
||||||
|
|
||||||
|
|
||||||
|
def obtain_entry_link(provided_link, application_id):
|
||||||
|
if provided_link != None:
|
||||||
|
if "Ealing" in application_id:
|
||||||
|
if ";" == provided_link[-1]:
|
||||||
|
return provided_link[:-1]
|
||||||
|
return provided_link
|
||||||
|
if "Hackney" in application_id:
|
||||||
|
# https://cl-staging.uksouth.cloudapp.azure.com/view/planning/1377846
|
||||||
|
# Planning application ID: Hackney-2021_2491
|
||||||
|
# https://developmentandhousing.hackney.gov.uk/planning/index.html?fa=getApplication&reference=2021/2491
|
||||||
|
ref_for_link = application_id.replace("Hackney-", "").replace("_", "/")
|
||||||
|
return "https://developmentandhousing.hackney.gov.uk/planning/index.html?fa=getApplication&reference=" + ref_for_link
|
||||||
|
if "Lambeth" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://planning.lambeth.gov.uk/online-applications/refineSearch.do?action=refine"
|
||||||
|
if "Barnet" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://publicaccess.barnet.gov.uk/online-applications/"
|
||||||
|
if "Kingston" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://publicaccess.kingston.gov.uk/online-applications/"
|
||||||
|
if "Sutton" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://publicaccess.sutton.gov.uk/online-applications/"
|
||||||
|
if "Croydon" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://publicaccess3.croydon.gov.uk/online-applications/"
|
||||||
|
if "Bromley" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://searchapplications.bromley.gov.uk/online-applications/"
|
||||||
|
if "Bexley" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://pa.bexley.gov.uk/online-applications/search.do?action=simple&searchType=Application"
|
||||||
|
if "Newham" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://pa.newham.gov.uk/online-applications/"
|
||||||
|
if "Westminster" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://idoxpa.westminster.gov.uk/online-applications/"
|
||||||
|
if "Enfield" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://planningandbuildingcontrol.enfield.gov.uk/online-applications/"
|
||||||
|
if "Southwark" in application_id:
|
||||||
|
# sadly, specific links seems impossible
|
||||||
|
return "https://planning.southwark.gov.uk/online-applications/"
|
||||||
|
if "Hammersmith" in application_id:
|
||||||
|
return "https://public-access.lbhf.gov.uk/online-applications/search.do?action=simple&searchType=Application"
|
||||||
|
if "City_of_London" in application_id:
|
||||||
|
return "https://www.planning2.cityoflondon.gov.uk/online-applications/"
|
||||||
|
return None
|
||||||
|
# Richmond is simply broken
|
||||||
|
|
||||||
|
|
||||||
|
def process_status(status, decision_date):
|
||||||
|
status_length_limit = 50 # see migrations/034.planning_livestream_data.up.sql
|
||||||
|
if status in ["Application Under Consideration", "Application Received"]:
|
||||||
|
if decision_date == None:
|
||||||
|
status = "Submitted"
|
||||||
|
if status in ["Refused", "Refusal", "Refusal (P)", "Application Invalid", "Insufficient Fee", "Dismissed"]:
|
||||||
|
status = "Rejected"
|
||||||
|
if status == "Appeal Received":
|
||||||
|
status = "Appeal In Progress"
|
||||||
|
if status in ["Completed", "Allowed", "Approval"]:
|
||||||
|
status = "Approved"
|
||||||
|
if status in [None, "NOT_MAPPED"]:
|
||||||
|
status = "Unknown"
|
||||||
|
if status in ["Lapsed"]:
|
||||||
|
status = "Withdrawn"
|
||||||
|
if len(status) > status_length_limit:
|
||||||
|
print("Status was too long and was skipped:", status)
|
||||||
|
return {"status": "Processing failed", "status_explanation_note": "status was unusally long and it was imposible to save it"}
|
||||||
|
if (status in ["Submitted", "Approved", "Rejected", "Appeal In Progress", "Withdrawn", "Unknown"]):
|
||||||
|
return {"status": status, "status_explanation_note": None}
|
||||||
|
if status in ["No Objection to Proposal (OBS only)", "Objection Raised to Proposal (OBS only)"]:
|
||||||
|
return {"status": "Approved", "status_explanation_note": "preapproved application, local authority is unable to reject it"}
|
||||||
|
print("Unexpected status " + status)
|
||||||
|
if status not in ["Not Required", "SECS", "Comment Issued", "ALL DECISIONS ISSUED", "Closed", "Declined to Determine"]:
|
||||||
|
print("New unexpected status " + status)
|
||||||
|
return {"status": status, "status_explanation_note": None}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
3
etl/planning_data/requirements.txt
Normal file
3
etl/planning_data/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Python packages for planning data import
|
||||||
|
psycopg2==2.8.6
|
||||||
|
requests==2.27.1
|
@ -70,6 +70,9 @@ This is the main table, containing almost all data collected by Colouring London
|
|||||||
- `sust_dec`: DEC rating
|
- `sust_dec`: DEC rating
|
||||||
- `sust_retrofit_date`: year of last significant retrofit
|
- `sust_retrofit_date`: year of last significant retrofit
|
||||||
- `planning_portal_link`: link to an entry on https://www.planningportal.co.uk/
|
- `planning_portal_link`: link to an entry on https://www.planningportal.co.uk/
|
||||||
|
- `planning_crowdsourced_site_completion_status`: status of completion of costruction at given location
|
||||||
|
- `planning_crowdsourced_site_completion_year`: year of completion of costruction at given location
|
||||||
|
- `planning_crowdsourced_planning_id`: id of planning application for a given location
|
||||||
- `planning_list_id`: National Heritage List for England ID
|
- `planning_list_id`: National Heritage List for England ID
|
||||||
- `planning_in_conservation_area_id`: conservation area ID
|
- `planning_in_conservation_area_id`: conservation area ID
|
||||||
- `planning_in_conservation_area_url`: conservation area appraisal link
|
- `planning_in_conservation_area_url`: conservation area appraisal link
|
||||||
|
1
migrations/034.planning_livestream_data.down.sql
Normal file
1
migrations/034.planning_livestream_data.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS planning_data;
|
21
migrations/034.planning_livestream_data.up.sql
Normal file
21
migrations/034.planning_livestream_data.up.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS planning_data (
|
||||||
|
-- internal unique id
|
||||||
|
planning_entry_id serial PRIMARY KEY,
|
||||||
|
|
||||||
|
-- assigned by planning authority
|
||||||
|
planning_application_id VARCHAR(50),
|
||||||
|
planning_application_link VARCHAR(260),
|
||||||
|
description VARCHAR,
|
||||||
|
registered_with_local_authority_date date,
|
||||||
|
days_since_registration_cached smallint,
|
||||||
|
decision_date date,
|
||||||
|
days_since_decision_date_cached smallint,
|
||||||
|
last_synced_date date,
|
||||||
|
status VARCHAR(50),
|
||||||
|
status_before_aliasing VARCHAR(50),
|
||||||
|
status_explanation_note VARCHAR(250),
|
||||||
|
data_source VARCHAR(70),
|
||||||
|
data_source_link VARCHAR(150),
|
||||||
|
address VARCHAR(300),
|
||||||
|
uprn bigint
|
||||||
|
);
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE buildings DROP COLUMN IF EXISTS planning_crowdsourced_site_completion_status;
|
||||||
|
ALTER TABLE buildings DROP COLUMN IF EXISTS planning_crowdsourced_site_completion_year;
|
||||||
|
ALTER TABLE buildings DROP COLUMN IF EXISTS planning_crowdsourced_planning_id;
|
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS planning_crowdsourced_site_completion_status boolean;
|
||||||
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS planning_crowdsourced_site_completion_year smallint;
|
||||||
|
ALTER TABLE buildings ADD COLUMN IF NOT EXISTS planning_crowdsourced_planning_id VARCHAR DEFAULT '';
|
1
migrations/036.borough_borders_and_labels.down.sql
Normal file
1
migrations/036.borough_borders_and_labels.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS external_data_borough_boundary;
|
84
migrations/036.borough_borders_and_labels.up.sql
Normal file
84
migrations/036.borough_borders_and_labels.up.sql
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user