Merge branch 'master' into colouring-core
This commit is contained in:
commit
7aebd77f10
@ -5,8 +5,6 @@
|
||||
</Rule>
|
||||
<Rule>
|
||||
<LineSymbolizer stroke="#222222aa" stroke-width="0.5" />
|
||||
<MaxScaleDenominator>17000</MaxScaleDenominator>
|
||||
<MinScaleDenominator>0</MinScaleDenominator>
|
||||
</Rule>
|
||||
</Style>
|
||||
<Style name="base_night">
|
||||
@ -19,6 +17,61 @@
|
||||
<MinScaleDenominator>0</MinScaleDenominator>
|
||||
</Rule>
|
||||
</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">
|
||||
<Rule>
|
||||
<TextSymbolizer
|
||||
@ -313,6 +366,102 @@
|
||||
<PolygonSymbolizer fill="#73ebaf" />
|
||||
</Rule>
|
||||
</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="#7a84a0"/>
|
||||
<LineSymbolizer stroke="#7a84a0" 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="#7a84a0"/>
|
||||
<LineSymbolizer stroke="#7a84a0" 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="#7a84a0"/>
|
||||
<LineSymbolizer stroke="#7a84a0" 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">
|
||||
<Rule>
|
||||
<Filter>[planning_in_conservation_area] = true</Filter>
|
||||
@ -597,7 +746,7 @@
|
||||
<Style name="landuse">
|
||||
<Rule>
|
||||
<Filter>[current_landuse_order] = "Agriculture And Fisheries"</Filter>
|
||||
<PolygonSymbolizer fill="#fa667d" />
|
||||
<PolygonSymbolizer fill="#73ccd1" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[current_landuse_order] = "Minerals"</Filter>
|
||||
@ -637,7 +786,7 @@
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[current_landuse_order] = "Community Services"</Filter>
|
||||
<PolygonSymbolizer fill="#73ccd1" />
|
||||
<PolygonSymbolizer fill="#fa667d" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<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,
|
||||
verify: true,
|
||||
},
|
||||
work_on_site_is_completed_on_year: {
|
||||
planning_crowdsourced_site_completion_status: {
|
||||
edit: true,
|
||||
verify: true,
|
||||
},
|
||||
planning_planning_application_id_crowdsourced: {
|
||||
planning_crowdsourced_site_completion_year: {
|
||||
edit: true,
|
||||
verify: true,
|
||||
},
|
||||
planning_crowdsourced_planning_id: {
|
||||
edit: 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) => {
|
||||
if(!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in'});
|
||||
@ -202,6 +219,7 @@ export default {
|
||||
getBuildingById,
|
||||
updateBuildingById,
|
||||
getBuildingUPRNsById,
|
||||
getBuildingPlanningDataById,
|
||||
getUserVerifiedAttributes,
|
||||
verifyBuildingAttributes,
|
||||
getBuildingEditHistoryById,
|
||||
|
@ -26,6 +26,7 @@ router.route('/:building_id.json')
|
||||
// GET building UPRNs
|
||||
router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById);
|
||||
|
||||
router.get('/:building_id/planning_data.json', buildingController.getBuildingPlanningDataById);
|
||||
|
||||
// POST verify building attribute
|
||||
router.route('/:building_id/verify.json')
|
||||
|
@ -56,4 +56,5 @@ export * from './edit';
|
||||
export * from './history';
|
||||
export * from './query';
|
||||
export * from './uprn';
|
||||
export * from './planningData';
|
||||
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;
|
||||
}
|
||||
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}/uprns.json`)
|
||||
apiGet(`/api/buildings/${buildingId}/uprns.json`),
|
||||
apiGet(`/api/buildings/${buildingId}/planning_data.json`)
|
||||
]);
|
||||
|
||||
building.uprns = buildingUprns.uprns;
|
||||
building.planning_data = planningData.data;
|
||||
building = Object.assign(building, {...building.user_attributes});
|
||||
delete building.user_attributes;
|
||||
|
||||
|
@ -6,6 +6,7 @@ import './app.css';
|
||||
|
||||
import { AuthRoute, PrivateRoute } from './route';
|
||||
import { AuthProvider } from './auth-context';
|
||||
import { DisplayPreferencesProvider } from './displayPreferences-context';
|
||||
import { Header } from './header';
|
||||
import { MapApp } from './map-app';
|
||||
import { Building, UserVerified } from './models/building';
|
||||
@ -53,6 +54,7 @@ export const App: React.FC<AppProps> = props => {
|
||||
const mapAppPaths = ['/', '/:mode(view|edit|multi-edit)/:category/:building(\\d+)?/(history)?'];
|
||||
|
||||
return (
|
||||
<DisplayPreferencesProvider>
|
||||
<AuthProvider preloadedUser={props.user}>
|
||||
<Switch>
|
||||
<Route exact path={mapAppPaths}>
|
||||
@ -89,5 +91,6 @@ export const App: React.FC<AppProps> = props => {
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</AuthProvider>
|
||||
</DisplayPreferencesProvider>
|
||||
);
|
||||
};
|
||||
|
@ -58,6 +58,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
||||
const [userError, setUserError] = useState<string>(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
const login = useCallback(async (data: UserLoginData, cb: (err) => void = noop) => {
|
||||
if(isAuthenticated) {
|
||||
return;
|
||||
@ -199,7 +200,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
||||
logout,
|
||||
signup,
|
||||
generateApiKey,
|
||||
deleteAccount
|
||||
deleteAccount,
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
|
@ -4,6 +4,7 @@ import { useAuth } from '../auth-context';
|
||||
import { categoriesConfig, Category } from '../config/categories-config';
|
||||
import { categoryUiConfig } from '../config/category-ui-config';
|
||||
import { Building, UserVerified } from '../models/building';
|
||||
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||
|
||||
import BuildingNotFound from './building-not-found';
|
||||
|
||||
@ -14,6 +15,8 @@ interface BuildingViewProps {
|
||||
user_verified?: any;
|
||||
onBuildingUpdate: (buildingId: number, updatedData: Building) => 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}
|
||||
inactive={inactive}
|
||||
user={user}
|
||||
mapColourScale={props.mapColourScale}
|
||||
onMapColourScale={props.onMapColourScale}
|
||||
/>;
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
/* padding: 0.1em; */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@ -18,5 +17,6 @@
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
margin: 0;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
.section-header .h2 {
|
||||
display: inline-block;
|
||||
flex-basis: 150px;
|
||||
flex-basis: 200px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
margin: 0.75rem 0 0.5em 0.1em;
|
||||
@ -33,7 +33,8 @@
|
||||
|
||||
.section-header .section-header-actions {
|
||||
display: inline-block;
|
||||
flex-basis: 400px;
|
||||
flex-basis: 220px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
|
@ -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()} target="_blank">{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 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>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;
|
@ -1,9 +1,10 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import DataTitle from './data-title';
|
||||
import { BaseDataEntryProps } from './data-entry';
|
||||
import { DataTitleCopyable } from './data-title';
|
||||
|
||||
|
||||
interface UPRNsDataEntryProps {
|
||||
interface UPRNsDataEntryProps extends BaseDataEntryProps {
|
||||
title: string;
|
||||
tooltip: string;
|
||||
value: {
|
||||
@ -19,7 +20,8 @@ const UPRNsDataEntry: React.FC<UPRNsDataEntryProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DataTitle
|
||||
<DataTitleCopyable
|
||||
slug={props.slug}
|
||||
title={props.title}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
|
@ -29,7 +29,7 @@ const UserOpinionEntry: React.FunctionComponent<UserOpinionEntryProps> = (props)
|
||||
checked={!!props.userValue}
|
||||
disabled={props.mode === 'view'}
|
||||
onChange={e => props.onChange(props.slug, e.target.checked)}
|
||||
/> Yes
|
||||
/> Yes (tick here to add (or remove) your opinion, to this you need to be in the edit mode)
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import ErrorBox from '../components/error-box';
|
||||
import InfoBox from '../components/info-box';
|
||||
import { compareObjects } from '../helpers';
|
||||
import { Building, BuildingEdits, BuildingUserAttributes, UserVerified } from '../models/building';
|
||||
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||
import { User } from '../models/user';
|
||||
|
||||
import ContainerHeader from './container-header';
|
||||
@ -34,6 +35,9 @@ interface DataContainerProps {
|
||||
user_verified?: any;
|
||||
onBuildingUpdate: (buildingId: number, updatedData: Building) => void;
|
||||
onUserVerifiedUpdate: (buildingId: number, updatedData: UserVerified) => void;
|
||||
|
||||
mapColourScale: BuildingMapTileset;
|
||||
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||
}
|
||||
|
||||
interface DataContainerState {
|
||||
@ -43,6 +47,8 @@ interface DataContainerState {
|
||||
currentBuildingId: number;
|
||||
currentBuildingRevisionId: number;
|
||||
buildingEdits: BuildingEdits;
|
||||
mapColourScale: BuildingMapTileset;
|
||||
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||
}
|
||||
|
||||
export type DataContainerType = React.ComponentType<DataContainerProps>;
|
||||
@ -66,7 +72,9 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
||||
keys_to_copy: {},
|
||||
buildingEdits: {},
|
||||
currentBuildingId: undefined,
|
||||
currentBuildingRevisionId: undefined
|
||||
currentBuildingRevisionId: undefined,
|
||||
mapColourScale: undefined,
|
||||
onMapColourScale: undefined
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
@ -108,7 +116,9 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
||||
keys_to_copy: categoryKeys,
|
||||
buildingEdits: {},
|
||||
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}
|
||||
onSaveChange={undefined}
|
||||
user_verified={[]}
|
||||
mapColourScale={undefined}
|
||||
onMapColourScale={undefined}
|
||||
/>
|
||||
</Fragment> :
|
||||
this.props.building != undefined ?
|
||||
@ -413,6 +425,8 @@ const withCopyEdit: (wc: React.ComponentType<CategoryViewProps>) => DataContaine
|
||||
onSaveChange={this.handleSaveChange}
|
||||
user_verified={this.props.user_verified}
|
||||
user={this.props.user}
|
||||
mapColourScale={this.props.mapColourScale}
|
||||
onMapColourScale={this.props.onMapColourScale}
|
||||
/>
|
||||
</form> :
|
||||
<InfoBox msg="Select a building to view data"></InfoBox>
|
||||
|
@ -1,27 +1,52 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import '../../map/map-button.css';
|
||||
import { dataFields } from '../../config/data-fields-config';
|
||||
import { MultiDataEntry } from '../data-components/multi-data-entry/multi-data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import { DynamicsBuildingPane, DynamicsDataEntry } from './dynamics/dynamics-data-entry';
|
||||
import { FieldRow } from '../data-components/field-row';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Category } from '../../config/categories-config';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import TextboxDataEntry from '../data-components/textbox-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import YearDataEntry from '../data-components/year-data-entry';
|
||||
import withCopyEdit from '../data-container';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import InfoBox from '../../components/info-box';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
import { LogicalDataEntry } from '../data-components/logical-data-entry/logical-data-entry';
|
||||
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||
|
||||
const HistoricalStatusOptions = [
|
||||
'The current footprint matches/almost exactly matches the historical map beneath, and/or is known to have been built before the map was made',
|
||||
'The building core is the same as the historical map but has had multiple additions/changes',
|
||||
'The building no longer exists',
|
||||
];
|
||||
|
||||
/**
|
||||
* Age view/edit section
|
||||
*/
|
||||
const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const building = props.building;
|
||||
const thisYear = (new Date()).getFullYear();
|
||||
const currentBuildingConstructionYear = building.date_year || undefined;
|
||||
|
||||
const ageLinkUrl = `/${props.mode}/${Category.Age}/${props.building.building_id}`;
|
||||
|
||||
const { historicData, historicDataSwitchOnClick, darkLightTheme } = useDisplayPreferences();
|
||||
|
||||
if (props.building.date_source == "Expert knowledge of building" ||
|
||||
props.building.date_source == "Expert estimate from image" ||
|
||||
props.building.date_source == null
|
||||
){
|
||||
return (
|
||||
<Fragment>
|
||||
<DataEntryGroup name="Building Age" collapsed={true} >
|
||||
<YearDataEntry
|
||||
year={props.building.date_year}
|
||||
upper={props.building.date_upper}
|
||||
@ -88,11 +113,148 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
user_verified_as={props.user_verified.date_source}
|
||||
verified_count={props.building.verified.date_source}
|
||||
/>
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<DataEntry
|
||||
title="Cladding Date"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Date of Significant Extensions"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Date of Significant Retrofits"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Lifespan and Site History" collapsed={true} >
|
||||
<button className={`map-switcher-inline ${historicData}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={historicDataSwitchOnClick}>
|
||||
{(historicData === 'enabled')?'Click here to hide historical maps':'Click here to show historical maps'}
|
||||
</button>
|
||||
<DataEntryGroup collapsed={false} name="Constructions and demolitions on this site" showCount={false}>
|
||||
<DynamicsBuildingPane>
|
||||
<label>Current building (age data <Link to={ageLinkUrl}>editable here</Link>)</label>
|
||||
<FieldRow>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_constructed.title}
|
||||
value={currentBuildingConstructionYear}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_demolished.title}
|
||||
value={undefined}
|
||||
placeholder='---'
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div style={{flex: '0 1 27%'}}>
|
||||
<DataEntry
|
||||
slug=''
|
||||
title='Lifespan to date'
|
||||
value={ (thisYear - currentBuildingConstructionYear) + ''}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</DynamicsBuildingPane>
|
||||
{
|
||||
currentBuildingConstructionYear == undefined ?
|
||||
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox> :
|
||||
|
||||
<>
|
||||
<LogicalDataEntry
|
||||
slug='dynamics_has_demolished_buildings'
|
||||
title={dataFields.dynamics_has_demolished_buildings.title}
|
||||
value={building.dynamics_has_demolished_buildings}
|
||||
disallowFalse={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
disallowNull={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
|
||||
onChange={props.onSaveChange}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
{
|
||||
building.dynamics_has_demolished_buildings &&
|
||||
<>
|
||||
<DynamicsDataEntry
|
||||
|
||||
/*
|
||||
Will clear the edits and new record data upon navigating to another building.
|
||||
Should get a better way to do this, plus a way to actually keep unsaved edits.
|
||||
*/
|
||||
key={building.building_id}
|
||||
|
||||
value={building.demolished_buildings}
|
||||
editableEntries={true}
|
||||
slug='demolished_buildings'
|
||||
title={dataFields.demolished_buildings.title}
|
||||
mode={props.mode}
|
||||
onChange={props.onChange}
|
||||
onSaveAdd={props.onSaveAdd}
|
||||
hasEdits={props.edited}
|
||||
maxYear={currentBuildingConstructionYear}
|
||||
minYear={50}
|
||||
/>
|
||||
{
|
||||
props.mode === 'view' &&
|
||||
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DataEntryGroup>
|
||||
<InfoBox>
|
||||
This section is under development in collaboration with the historic environment sector.
|
||||
Please let us know your suggestions on the <a href="https://discuss.colouring.london/t/dynamics-category-discussion/107">discussion forum</a>! (external link - save your edits first)
|
||||
</InfoBox>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Survival and Loss tracked using Historical Maps" collapsed={true} >
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<button className={`map-switcher-inline ${historicData}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={historicDataSwitchOnClick}>
|
||||
{(historicData === 'enabled')?'Click here to hide historical maps':'Click here to show historical maps'}
|
||||
</button>
|
||||
<SelectDataEntry
|
||||
title={dataFields.historical_status.title}
|
||||
slug="historical_status"
|
||||
value={""}
|
||||
tooltip={dataFields.historical_status.tooltip}
|
||||
options={HistoricalStatusOptions}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Historical land use change"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Fragment>
|
||||
<DataEntryGroup name="Building Age" collapsed={true} >
|
||||
<YearDataEntry
|
||||
year={props.building.date_year}
|
||||
upper={props.building.date_upper}
|
||||
@ -139,7 +301,6 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
user_verified_as={props.user_verified.facade_year}
|
||||
verified_count={props.building.verified.facade_year}
|
||||
/>
|
||||
|
||||
<SelectDataEntry
|
||||
title={dataFields.date_source.title}
|
||||
slug="date_source"
|
||||
@ -179,6 +340,184 @@ const AgeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
user_verified_as={props.user_verified.date_link}
|
||||
verified_count={props.building.verified.date_link}
|
||||
/>
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<DataEntry
|
||||
title="Cladding Date"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<Verification
|
||||
slug="date_link"
|
||||
allow_verify={props.user !== undefined && props.building.date_link !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("date_link")}
|
||||
user_verified_as={props.user_verified.date_link}
|
||||
verified_count={props.building.verified.date_link}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Source"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Date of Significant Extensions"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<Verification
|
||||
slug="date_link"
|
||||
allow_verify={props.user !== undefined && props.building.date_link !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("date_link")}
|
||||
user_verified_as={props.user_verified.date_link}
|
||||
verified_count={props.building.verified.date_link}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Source"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Date of Significant Retrofits"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<Verification
|
||||
slug="date_link"
|
||||
allow_verify={props.user !== undefined && props.building.date_link !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("date_link")}
|
||||
user_verified_as={props.user_verified.date_link}
|
||||
verified_count={props.building.verified.date_link}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Source"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Lifespan and Site History" collapsed={true} >
|
||||
<button className={`map-switcher-inline ${historicData} btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={historicDataSwitchOnClick}>
|
||||
{(historicData === 'enabled')?'Click here to hide historical maps':'Click here to show historical maps'}
|
||||
</button>
|
||||
<DataEntryGroup collapsed={false} name="Constructions and demolitions on this site" showCount={false}>
|
||||
<DynamicsBuildingPane>
|
||||
<label>Current building (age data <Link to={ageLinkUrl}>editable here</Link>)</label>
|
||||
<FieldRow>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_constructed.title}
|
||||
value={currentBuildingConstructionYear}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_demolished.title}
|
||||
value={undefined}
|
||||
placeholder='---'
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div style={{flex: '0 1 27%'}}>
|
||||
<DataEntry
|
||||
slug=''
|
||||
title='Lifespan to date'
|
||||
value={ (thisYear - currentBuildingConstructionYear) + ''}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</DynamicsBuildingPane>
|
||||
{
|
||||
currentBuildingConstructionYear == undefined ?
|
||||
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox> :
|
||||
|
||||
<>
|
||||
<LogicalDataEntry
|
||||
slug='dynamics_has_demolished_buildings'
|
||||
title={dataFields.dynamics_has_demolished_buildings.title}
|
||||
value={building.dynamics_has_demolished_buildings}
|
||||
disallowFalse={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
disallowNull={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
|
||||
onChange={props.onSaveChange}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
{
|
||||
building.dynamics_has_demolished_buildings &&
|
||||
<>
|
||||
<DynamicsDataEntry
|
||||
|
||||
/*
|
||||
Will clear the edits and new record data upon navigating to another building.
|
||||
Should get a better way to do this, plus a way to actually keep unsaved edits.
|
||||
*/
|
||||
key={building.building_id}
|
||||
|
||||
value={building.demolished_buildings}
|
||||
editableEntries={true}
|
||||
slug='demolished_buildings'
|
||||
title={dataFields.demolished_buildings.title}
|
||||
mode={props.mode}
|
||||
onChange={props.onChange}
|
||||
onSaveAdd={props.onSaveAdd}
|
||||
hasEdits={props.edited}
|
||||
maxYear={currentBuildingConstructionYear}
|
||||
minYear={50}
|
||||
/>
|
||||
{
|
||||
props.mode === 'view' &&
|
||||
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DataEntryGroup>
|
||||
<InfoBox>
|
||||
This section is under development in collaboration with the historic environment sector.
|
||||
Please let us know your suggestions on the <a href="https://discuss.colouring.london/t/dynamics-category-discussion/107">discussion forum</a>! (external link - save your edits first)
|
||||
</InfoBox>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Survival and Loss tracked using Historical Maps" collapsed={true} >
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<button className={`map-switcher-inline ${historicData} btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={historicDataSwitchOnClick}>
|
||||
{(historicData === 'enabled')?'Click here to hide historical maps':'Click here to show historical maps'}
|
||||
</button>
|
||||
<SelectDataEntry
|
||||
title={dataFields.historical_status.title}
|
||||
slug="historical_status"
|
||||
value={""}
|
||||
tooltip={dataFields.historical_status.tooltip}
|
||||
options={HistoricalStatusOptions}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Historical land use change"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Building, BuildingAttributes } from '../../models/building';
|
||||
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||
|
||||
interface CopyProps {
|
||||
copying: boolean;
|
||||
@ -38,6 +39,9 @@ interface CategoryViewProps {
|
||||
|
||||
user_verified: any;
|
||||
user?: any;
|
||||
|
||||
mapColourScale: BuildingMapTileset;
|
||||
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import '../../map/map-button.css';
|
||||
import withCopyEdit from '../data-container';
|
||||
import UserOpinionEntry from '../data-components/user-opinion-data-entry';
|
||||
import { MultiSelectDataEntry } from '../data-components/multi-select-data-entry';
|
||||
@ -13,11 +14,29 @@ import './community.css';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import { MultiDataEntry } from '../data-components/multi-data-entry/multi-data-entry';
|
||||
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||
|
||||
/**
|
||||
* Community view/edit section
|
||||
*/
|
||||
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 { darkLightTheme } = useDisplayPreferences();
|
||||
const worthKeepingReasonsNonEmpty = Object.values(props.building.community_type_worth_keeping_reasons ?? {}).some(x => x);
|
||||
return <>
|
||||
<InfoBox type='warning'>
|
||||
@ -38,6 +57,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
copy={props.copy}
|
||||
|
||||
/>
|
||||
<button className={`map-switcher-inline ${props.mapColourScale == "likes" ? "enabled-state" : "disabled-state"} btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToLikesMapStyle}>
|
||||
{'Click here to switch map key to this info'}
|
||||
</button>
|
||||
<LogicalDataEntry
|
||||
slug='community_type_worth_keeping'
|
||||
title={buildingUserFields.community_type_worth_keeping.title}
|
||||
@ -81,7 +103,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
|
||||
<button className={`map-switcher-inline ${props.mapColourScale == "community_local_significance_total" ? "enabled-state" : "disabled-state"} btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToLocalSignificanceMapStyle}>
|
||||
{'Click here to switch map key to this info'}
|
||||
</button>
|
||||
<UserOpinionEntry
|
||||
slug='community_expected_planning_application'
|
||||
title={buildingUserFields.community_expected_planning_application.title}
|
||||
@ -92,6 +116,10 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
<button className={`map-switcher-inline ${props.mapColourScale == "community_expected_planning_application_total" ? "enabled-state" : "disabled-state"} btn btn-outline btn-outline-dark ${darkLightTheme}`} 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>
|
||||
|
||||
<InfoBox>Can you help add information on community use of buildings?</InfoBox>
|
||||
@ -149,6 +177,9 @@ const CommunityView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
<button className={`map-switcher-inline ${props.mapColourScale == "community_in_public_ownership" ? "enabled-state" : "disabled-state"} btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToPublicOwnershipMapStyle}>
|
||||
{'Click here to switch map key to this info'}
|
||||
</button>
|
||||
<Verification
|
||||
slug="community_public_ownership"
|
||||
allow_verify={props.user !== undefined && props.building.community_public_ownership !== null && !props.edited}
|
||||
|
@ -1,140 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import InfoBox from '../../../components/info-box';
|
||||
|
||||
import { Category } from '../../../config/categories-config';
|
||||
import { dataFields } from '../../../config/data-fields-config';
|
||||
|
||||
import DataEntry from '../../data-components/data-entry';
|
||||
import { DataEntryGroup } from '../../data-components/data-entry-group';
|
||||
import { DynamicsBuildingPane, DynamicsDataEntry } from './dynamics-data-entry';
|
||||
import { FieldRow } from '../../data-components/field-row';
|
||||
import NumericDataEntry from '../../data-components/numeric-data-entry';
|
||||
import withCopyEdit from '../../data-container';
|
||||
|
||||
import { CategoryViewProps } from '../category-view-props';
|
||||
import { LogicalDataEntry } from '../../data-components/logical-data-entry/logical-data-entry';
|
||||
|
||||
/**
|
||||
* Dynamics view/edit section
|
||||
*/
|
||||
const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
const building = props.building;
|
||||
const thisYear = (new Date()).getFullYear();
|
||||
const currentBuildingConstructionYear = building.date_year || undefined;
|
||||
|
||||
const ageLinkUrl = `/${props.mode}/${Category.Age}/${props.building.building_id}`;
|
||||
|
||||
return (<>
|
||||
<DataEntryGroup collapsed={false} name="Constructions and demolitions on this site" showCount={false}>
|
||||
<DynamicsBuildingPane>
|
||||
<label>Current building (age data <Link to={ageLinkUrl}>editable here</Link>)</label>
|
||||
<FieldRow>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_constructed.title}
|
||||
value={currentBuildingConstructionYear}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_demolished.title}
|
||||
value={undefined}
|
||||
placeholder='---'
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div style={{flex: '0 1 27%'}}>
|
||||
<DataEntry
|
||||
slug=''
|
||||
title='Lifespan to date'
|
||||
value={ (thisYear - currentBuildingConstructionYear) + ''}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</DynamicsBuildingPane>
|
||||
{
|
||||
currentBuildingConstructionYear == undefined ?
|
||||
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox> :
|
||||
|
||||
<>
|
||||
<LogicalDataEntry
|
||||
slug='dynamics_has_demolished_buildings'
|
||||
title={dataFields.dynamics_has_demolished_buildings.title}
|
||||
value={building.dynamics_has_demolished_buildings}
|
||||
disallowFalse={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
disallowNull={(building.demolished_buildings?.length ?? 0) > 0}
|
||||
|
||||
onChange={props.onSaveChange}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
/>
|
||||
{
|
||||
building.dynamics_has_demolished_buildings &&
|
||||
<>
|
||||
<DynamicsDataEntry
|
||||
|
||||
/*
|
||||
Will clear the edits and new record data upon navigating to another building.
|
||||
Should get a better way to do this, plus a way to actually keep unsaved edits.
|
||||
*/
|
||||
key={building.building_id}
|
||||
|
||||
value={building.demolished_buildings}
|
||||
editableEntries={true}
|
||||
slug='demolished_buildings'
|
||||
title={dataFields.demolished_buildings.title}
|
||||
mode={props.mode}
|
||||
onChange={props.onChange}
|
||||
onSaveAdd={props.onSaveAdd}
|
||||
hasEdits={props.edited}
|
||||
maxYear={currentBuildingConstructionYear}
|
||||
minYear={50}
|
||||
/>
|
||||
{
|
||||
props.mode === 'view' &&
|
||||
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</DataEntryGroup>
|
||||
|
||||
<DataEntryGroup name="Future planned data collection" collapsed={false} showCount={false}>
|
||||
<DataEntry
|
||||
title="Historical land use change"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (raster) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (vector) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<InfoBox>
|
||||
This section is under development in collaboration with the historic environment sector.
|
||||
Please let us know your suggestions on the <a href="https://discuss.colouring.london/t/dynamics-category-discussion/107">discussion forum</a>! (external link - save your edits first)
|
||||
</InfoBox>
|
||||
</>)
|
||||
};
|
||||
|
||||
const DynamicsContainer = withCopyEdit(DynamicsView);
|
||||
|
||||
export default DynamicsContainer;
|
@ -137,6 +137,7 @@ const LocationView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
/>
|
||||
<UPRNsDataEntry
|
||||
title={dataFields.uprns.title}
|
||||
slug="ref_uprns"
|
||||
value={props.building.uprns}
|
||||
tooltip={dataFields.uprns.tooltip}
|
||||
/>
|
||||
|
@ -1,137 +1,185 @@
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import '../../map/map-button.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
import InfoBox from '../../components/info-box';
|
||||
import { dataFields } from '../../config/data-fields-config';
|
||||
import CheckboxDataEntry from '../data-components/checkbox-data-entry';
|
||||
import NumericDataEntryWithFormattedLink from '../data-components/numeric-data-entry-with-formatted-link';
|
||||
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 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 SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import PlanningDataOfficialDataEntry from '../data-components/planning-data-entry';
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
import { Category } from '../../config/categories-config';
|
||||
import { useDisplayPreferences } from '../../displayPreferences-context';
|
||||
import { processParam } from '../../../api/parameters';
|
||||
|
||||
/**
|
||||
* Planning view/edit section
|
||||
*/
|
||||
const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
const currentTimestamp = new Date().valueOf();
|
||||
const milisecondsInYear = 1000 * 60 * 60 * 24 * 365;
|
||||
|
||||
// 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 switchToAllPlanningApplicationsMapStyle = (e) => {
|
||||
e.preventDefault();
|
||||
props.onMapColourScale('planning_applications_status_all')
|
||||
}
|
||||
const { flood, floodSwitchOnClick, housing, housingSwitchOnClick, creative, creativeSwitchOnClick, vista, vistaSwitchOnClick, parcel, parcelSwitchOnClick, conservation, conservationSwitchOnClick, darkLightTheme } = useDisplayPreferences();
|
||||
const communityLinkUrl = `/${props.mode}/${Category.Community}/${props.building.building_id}`;
|
||||
return (
|
||||
<Fragment>
|
||||
<InfoBox type='warning'>
|
||||
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="Planning application information" collapsed={true} >
|
||||
<DataEntryGroup name="Current/active applications (official data)" collapsed={false} >
|
||||
<InfoBox>
|
||||
This section provides data on active applications. We define these as applications with any activity in the last year.
|
||||
<br />
|
||||
To comment on an application follow the application link if provided, or visit the relevant local authority's planning page.
|
||||
</InfoBox>
|
||||
<DataEntryGroup name="Planning application information">
|
||||
<CheckboxDataEntry
|
||||
title="Is a planning application live for this site?"
|
||||
slug="planning_live_application"
|
||||
value={null}
|
||||
disabled={false}
|
||||
{props.building.planning_data ?
|
||||
<PlanningDataOfficialDataEntry
|
||||
shownData={props.building.planning_data.filter(item => isArchived(item) == false)}
|
||||
messageOnMissingData={
|
||||
props.building.planning_data.length > 0 ?
|
||||
"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."
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
: <></>
|
||||
}
|
||||
</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."
|
||||
}
|
||||
/>
|
||||
<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:
|
||||
: <></>
|
||||
}
|
||||
</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 no-applicable-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToExpectedApplicationMapStyle}>
|
||||
{'Click here to view possible locations of future applications'}
|
||||
</button>
|
||||
:
|
||||
<button className={`map-switcher-inline no-applicable-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToAllPlanningApplicationsMapStyle}>
|
||||
{'Click to see planning applications'}
|
||||
</button>
|
||||
}
|
||||
<UserOpinionEntry
|
||||
slug='community_expected_planning_application'
|
||||
title={buildingUserFields.community_expected_planning_application.title}
|
||||
userValue={props.building.community_expected_planning_application}
|
||||
|
||||
<CheckboxDataEntry
|
||||
title={dataFields.planning_demolition_complete.title}
|
||||
slug="planning_demolition_complete"
|
||||
value={props.building.planning_demolition_complete}
|
||||
onChange={props.onSaveChange}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
disabled={false}
|
||||
/>
|
||||
<DataEntry
|
||||
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}
|
||||
/>
|
||||
*/}
|
||||
<InfoBox type='warning'>
|
||||
Further improvements to this feature are currently being made.
|
||||
</InfoBox>
|
||||
</DataEntryGroup>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Planning zones" collapsed={true} >
|
||||
<InfoBox type='success'>
|
||||
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.
|
||||
<InfoBox>
|
||||
To view planning zone data for London click the buttons below. 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>
|
||||
<div className={`alert alert-dark`} role="alert" style={{ fontSize: 13, backgroundColor: "#f6f8f9" }}>
|
||||
<i>
|
||||
Data in this section comes from <a href="https://www.london.gov.uk/programmes-strategies/planning/digital-planning/planning-london-datahub">the Greater London Authority's Planning London Datahub</a>. Please check the original GLA source when using for planning purposes.
|
||||
<br />
|
||||
Specific sources are mentioned in the footer of map for currently enabled layers.
|
||||
</i>
|
||||
</div>
|
||||
<LogicalDataEntry
|
||||
title="Is the building inside a flood zone?"
|
||||
title="Is the building inside a Flood Zone?"
|
||||
slug="planning_live_application"
|
||||
value={null}
|
||||
disabled={true}
|
||||
tooltip={"the GLA official description: \"All areas with more than a 1 in 1,000 annual probability of either river or sea flooding.\""}
|
||||
/>
|
||||
{/*
|
||||
<form className={`layer-switcher-inline`}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
Click to see the data mapped
|
||||
<button className={`map-switcher-inline ${flood}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={floodSwitchOnClick}>
|
||||
{(flood === 'enabled')? 'Click to hide Flood Zones' : 'Click to see Flood Zones mapped'}
|
||||
</button>
|
||||
</form>
|
||||
*/}
|
||||
<LogicalDataEntry
|
||||
title="Is the building in a strategic development zone for housing?"
|
||||
title="Is the building in a Housing Zone?"
|
||||
slug="planning_live_application"
|
||||
value={null}
|
||||
disabled={true}
|
||||
tooltip={"the GLA official description: \"Housing zones are areas funded by the Mayor and government to attract developers and relevant partners to build new homes.\""}
|
||||
/>
|
||||
{/*
|
||||
<form className={`layer-switcher-inline`}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
Click to see the data mapped
|
||||
<button className={`map-switcher-inline ${housing}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={housingSwitchOnClick}>
|
||||
{(housing === 'enabled')? 'Click to hide Housing Zones' : 'Click to see Housing Zones mapped'}
|
||||
</button>
|
||||
</form>
|
||||
*/}
|
||||
<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"
|
||||
value={null}
|
||||
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.\""}
|
||||
/>
|
||||
{/*
|
||||
<form className={`layer-switcher-inline`}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
Click to see the data mapped
|
||||
<button className={`map-switcher-inline ${creative}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={creativeSwitchOnClick}>
|
||||
{(creative === 'enabled')? 'Click to hide Creative Enterprise Zones' : 'Click to see Creative Enterprise Zones'}
|
||||
</button>
|
||||
</form>
|
||||
*/}
|
||||
<LogicalDataEntry
|
||||
title="Is the building within a protected sightline?"
|
||||
title="Is the building within a Protected Vista?"
|
||||
slug="planning_live_application"
|
||||
value={null}
|
||||
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.\""}
|
||||
/>
|
||||
{/*
|
||||
<form className={`layer-switcher-inline`}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
Click to see the data mapped
|
||||
<button className={`map-switcher-inline ${vista}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={vistaSwitchOnClick}>
|
||||
{(vista === 'enabled')? 'Click to hide Protected Vistas' : 'Click to see Protected Vistas'}
|
||||
</button>
|
||||
</form>
|
||||
*/}
|
||||
{/*
|
||||
<DataEntry
|
||||
title={dataFields.planning_glher_url.title}
|
||||
@ -153,10 +201,26 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
/>
|
||||
*/}
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Building protection" collapsed={true} >
|
||||
<InfoBox type='success'>
|
||||
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.
|
||||
<DataEntryGroup name="Heritage assets and building protection" collapsed={true} >
|
||||
<InfoBox>
|
||||
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>
|
||||
<div className={`alert alert-dark`} role="alert" style={{ fontSize: 13, backgroundColor: "#f6f8f9" }}>
|
||||
<i><div><u>Disclaimer</u>: Data for designated heritage assets has been accessed from <a href="https://historicengland.org.uk/listing/the-list/">the National Heritage List for England</a>. Source information for Conservation Area data can be accessed <a href="http://www.bedfordpark.net/leo/planning/">here</a>. Please note all data should be double checked against official sources where used for planning purposes'.</div></i>
|
||||
</div>
|
||||
{
|
||||
props.mapColourScale != "planning_combined" ?
|
||||
<button className={`map-switcher-inline no-applicable-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToBuildingProtectionMapStyle}>
|
||||
{'Click to see individual protected buildings mapped'}
|
||||
</button>
|
||||
:
|
||||
<button className={`map-switcher-inline no-applicable-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={switchToAllPlanningApplicationsMapStyle}>
|
||||
{'Click to see planning applications'}
|
||||
</button>
|
||||
}
|
||||
<button className={`map-switcher-inline ${conservation}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={conservationSwitchOnClick}>
|
||||
{(conservation === 'enabled')? 'Click to hide Conservation Areas' : 'Click to see Conservation Areas'}
|
||||
</button>
|
||||
<NumericDataEntryWithFormattedLink
|
||||
title={dataFields.planning_list_id.title}
|
||||
slug="planning_list_id"
|
||||
@ -164,7 +228,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
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" } }
|
||||
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
||||
/>
|
||||
@ -224,7 +288,7 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
placeholder="If yes, add ID here"
|
||||
placeholder="add ID here"
|
||||
linkTargetFunction={(id: String) => { return "https://whc.unesco.org/en/list/" + id } }
|
||||
linkDescriptionFunction={(id: String) => { return "ID Link" } }
|
||||
/>
|
||||
@ -266,12 +330,12 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
placeholder="Please add Conservation Area identifier"
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_in_conservation_area_url"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_conservation_area_url !== null && !props.edited}
|
||||
slug="planning_in_conservation_area_id"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_conservation_area_id !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_conservation_area_url")}
|
||||
user_verified_as={props.user_verified.planning_in_conservation_area_url}
|
||||
verified_count={props.building.verified.planning_in_conservation_area_url}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_conservation_area_id")}
|
||||
user_verified_as={props.user_verified.planning_in_conservation_area_id}
|
||||
verified_count={props.building.verified.planning_in_conservation_area_id}
|
||||
/>
|
||||
*/}
|
||||
<DataEntry
|
||||
@ -284,6 +348,16 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
isUrl={true}
|
||||
placeholder="Please add CA appraisal link here"
|
||||
/>
|
||||
{props.building.planning_in_conservation_area_url === "" ? "Our CA map records this building as not being within a CA. To help us verify this, please click ‘verify’ or, if info is incorrect, please add the local authority’s CA appraisal link." : "" }
|
||||
{props.building.planning_in_conservation_area_url === "identified as listed: please replace with links" ? "Our CA map records this building as being within a CA. To help us verify this information please add the local authority’s CA appraisal link and then click ‘verify’." : "" }
|
||||
<Verification
|
||||
slug="planning_in_conservation_area_url"
|
||||
allow_verify={props.user !== undefined && props.building.planning_in_conservation_area_url !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_in_conservation_area_url")}
|
||||
user_verified_as={props.user_verified.planning_in_conservation_area_url}
|
||||
verified_count={props.building.verified.planning_in_conservation_area_url}
|
||||
/>
|
||||
{/*
|
||||
<DataEntry
|
||||
title={dataFields.planning_conservation_area_name.title}
|
||||
@ -339,10 +413,94 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
verified_count={props.building.verified.planning_in_apa_url}
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<DataEntryGroup name="Forthcoming data (sections to be activated)" collapsed={true} >
|
||||
<DataEntryGroup name="Active application info (crowdsourced)" collapsed={true} >
|
||||
{/* will be titled "Other active application info (crowdsourced data)" once active" */}
|
||||
<InfoBox type='warning'>
|
||||
This category is not yet activated - Until this section is activated please report inaccuracies or problems on the <a href=" https://github.com/colouring-cities/colouring-london/discussions/categories/planning-section-comments">Discussion Forum</a>.
|
||||
</InfoBox>
|
||||
{/* that is placeholder display, will be replaced by actual code */}
|
||||
<div className="data-title">
|
||||
<div className="data-title-text">
|
||||
<ul>
|
||||
<li>Year of completion if known</li>
|
||||
<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>
|
||||
<li>If any of the active planning applications are not mapped onto the correct site, please tick here</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
/*
|
||||
<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}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
disabled={true}
|
||||
/>
|
||||
<Verification
|
||||
slug="planning_crowdsourced_site_completion_year"
|
||||
allow_verify={false}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("planning_crowdsourced_site_completion_year")}
|
||||
user_verified_as={props.user_verified.planning_crowdsourced_site_completion_year}
|
||||
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='success'>
|
||||
<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 ${parcel}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} 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? "}
|
||||
@ -367,9 +525,12 @@ const PlanningView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
user_verified_as={props.user_verified.community_public_ownership}
|
||||
verified_count={props.building.verified.community_public_ownership}
|
||||
/>
|
||||
*/
|
||||
}
|
||||
</DataEntryGroup>
|
||||
</DataEntryGroup>
|
||||
</Fragment>
|
||||
);
|
||||
)};
|
||||
const PlanningContainer = withCopyEdit(PlanningView);
|
||||
|
||||
export default PlanningContainer;
|
||||
|
105
app/src/frontend/building/data-containers/resilience.tsx
Normal file
105
app/src/frontend/building/data-containers/resilience.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import InfoBox from '../../components/info-box';
|
||||
|
||||
import { Category } from '../../config/categories-config';
|
||||
import { dataFields } from '../../config/data-fields-config';
|
||||
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
import { DynamicsBuildingPane, DynamicsDataEntry } from './dynamics/dynamics-data-entry';
|
||||
import { FieldRow } from '../data-components/field-row';
|
||||
import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
import { LogicalDataEntry } from '../data-components/logical-data-entry/logical-data-entry';
|
||||
|
||||
/**
|
||||
* Dynamics view/edit section
|
||||
*/
|
||||
const ResilienceView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
|
||||
|
||||
return (<>
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<DataEntry
|
||||
title="Building age"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Typical typology lifespan"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Typology adaptability rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Physical adaptability rating - within plot"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Landuse adaptability rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Structural material lifespan rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Protection from demolition rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Flood risk rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Surface geology type"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Average community value rating for typology"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Other rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Total resilience rating"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</>)
|
||||
};
|
||||
|
||||
const ResilienceContainer = withCopyEdit(ResilienceView);
|
||||
|
||||
export default ResilienceContainer;
|
@ -15,7 +15,7 @@ import { CategoryViewProps } from './category-view-props';
|
||||
*/
|
||||
const SizeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<DataEntryGroup name="Storeys">
|
||||
<DataEntryGroup name="Floors">
|
||||
<NumericDataEntry
|
||||
title={dataFields.size_storeys_core.title}
|
||||
slug="size_storeys_core"
|
||||
|
@ -13,7 +13,7 @@ import { CategoryViewProps } from './category-view-props';
|
||||
*/
|
||||
const StreetscapeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<InfoBox msg="This is what we're planning to collect on the building's context" />
|
||||
<InfoBox type='warning' msg="This is what we're planning to collect on the building's context" />
|
||||
<ul className="data-list">
|
||||
<li>Gardens</li>
|
||||
<li>Trees</li>
|
||||
@ -49,13 +49,7 @@ const StreetscapeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Land ownership parcel link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Land ownership type"
|
||||
title="Does the building have a garden?"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
@ -66,12 +60,30 @@ const StreetscapeView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Pavement width"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Street network geometry link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Distance from Public Green Space"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Distance from front door to nearest tree"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
const StreetscapeContainer = withCopyEdit(StreetscapeView);
|
||||
|
@ -6,6 +6,7 @@ import NumericDataEntry from '../data-components/numeric-data-entry';
|
||||
import SelectDataEntry from '../data-components/select-data-entry';
|
||||
import Verification from '../data-components/verification';
|
||||
import withCopyEdit from '../data-container';
|
||||
import InfoBox from '../../components/info-box';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
@ -94,26 +95,31 @@ const SustainabilityView: React.FunctionComponent<CategoryViewProps> = (props) =
|
||||
user_verified_as={props.user_verified.sust_retrofit_date}
|
||||
verified_count={props.building.verified.sust_retrofit_date}
|
||||
/>
|
||||
|
||||
<NumericDataEntry
|
||||
title={dataFields.sust_life_expectancy.title}
|
||||
slug="sust_life_expectancy"
|
||||
value={props.building.sust_life_expectancy}
|
||||
step={1}
|
||||
min={1}
|
||||
disabled={true}
|
||||
mode={props.mode}
|
||||
copy={props.copy}
|
||||
onChange={props.onChange}
|
||||
<InfoBox>
|
||||
This section is under development.
|
||||
</InfoBox>
|
||||
<DataEntry
|
||||
title="Date of Significant Retrofits"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<Verification
|
||||
slug="date_link"
|
||||
allow_verify={props.user !== undefined && props.building.date_link !== null && !props.edited}
|
||||
onVerify={props.onVerify}
|
||||
user_verified={props.user_verified.hasOwnProperty("date_link")}
|
||||
user_verified_as={props.user_verified.date_link}
|
||||
verified_count={props.building.verified.date_link}
|
||||
/>
|
||||
<DataEntry
|
||||
title="Repairability rating for type"
|
||||
title="Source"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Adaptability within plot rating"
|
||||
title="Green Walls / Green Roof / Shading"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
|
@ -90,7 +90,7 @@ const TypeView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Dynamic tissue type classificaiton"
|
||||
title="Dynamic tissue type classification"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
|
@ -150,6 +150,9 @@
|
||||
position: static;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.data-section a {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.data-list dd {
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.5;
|
||||
|
@ -2,7 +2,9 @@ import React from 'react';
|
||||
|
||||
interface InfoBoxProps {
|
||||
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'}) => (
|
||||
|
@ -43,7 +43,7 @@ const LogoGrid: React.FunctionComponent = () => (
|
||||
<div className="row">
|
||||
<div className="cell background-sustainability"></div>
|
||||
<div className="cell background-planning"></div>
|
||||
<div className="cell background-dynamics"></div>
|
||||
<div className="cell background-resilience"></div>
|
||||
<div className="cell background-community"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@ export enum Category {
|
||||
Team = 'team',
|
||||
Planning = 'planning',
|
||||
Sustainability = 'sustainability',
|
||||
Dynamics = 'dynamics',
|
||||
Resilience = 'resilience',
|
||||
Community = 'community',
|
||||
}
|
||||
|
||||
@ -26,14 +26,14 @@ export const categoriesOrder: Category[] = [
|
||||
Category.Location,
|
||||
Category.LandUse,
|
||||
Category.Type,
|
||||
Category.Age,
|
||||
Category.Size,
|
||||
Category.Construction,
|
||||
Category.Age,
|
||||
Category.Streetscape,
|
||||
Category.Team,
|
||||
Category.Planning,
|
||||
Category.Sustainability,
|
||||
Category.Dynamics,
|
||||
Category.Resilience,
|
||||
Category.Community,
|
||||
];
|
||||
|
||||
@ -48,13 +48,13 @@ interface CategoryDefinition {
|
||||
export const categoriesConfig: {[key in Category]: CategoryDefinition} = {
|
||||
[Category.Age]: {
|
||||
slug: 'age',
|
||||
name: 'Age',
|
||||
name: 'Age & History',
|
||||
aboutUrl: 'https://pages.colouring.london/age',
|
||||
intro: 'Building age data can support energy analysis and help predict long-term change.',
|
||||
},
|
||||
[Category.Size]: {
|
||||
slug: 'size',
|
||||
name: 'Size',
|
||||
name: 'Size & Form',
|
||||
aboutUrl: 'https://pages.colouring.london/shapeandsize',
|
||||
intro: 'How big are buildings?',
|
||||
},
|
||||
@ -84,19 +84,19 @@ export const categoriesConfig: {[key in Category]: CategoryDefinition} = {
|
||||
},
|
||||
[Category.Planning]: {
|
||||
slug: 'planning',
|
||||
name: 'Planning',
|
||||
name: 'Planning Controls',
|
||||
aboutUrl: 'https://pages.colouring.london/planning',
|
||||
intro: 'Planning controls relating to protection and reuse.',
|
||||
},
|
||||
[Category.Sustainability]: {
|
||||
slug: 'sustainability',
|
||||
name: 'Sustainability',
|
||||
name: 'Energy Performance',
|
||||
aboutUrl: 'https://pages.colouring.london/sustainability',
|
||||
intro: 'Are buildings energy efficient?',
|
||||
},
|
||||
[Category.Type]: {
|
||||
slug: 'type',
|
||||
name: 'Type',
|
||||
name: 'Typology',
|
||||
aboutUrl: 'https://pages.colouring.london/buildingtypology',
|
||||
intro: 'How were buildings previously used?',
|
||||
},
|
||||
@ -113,9 +113,9 @@ export const categoriesConfig: {[key in Category]: CategoryDefinition} = {
|
||||
aboutUrl: 'https://pages.colouring.london/greenery',
|
||||
intro: "What's the building's context? Coming soon…",
|
||||
},
|
||||
[Category.Dynamics]: {
|
||||
slug: 'dynamics',
|
||||
name: 'Dynamics',
|
||||
[Category.Resilience]: {
|
||||
slug: 'resilience',
|
||||
name: 'Resilience',
|
||||
aboutUrl: 'https://pages.colouring.london/dynamics',
|
||||
intro: 'How has the site of this building changed over time?'
|
||||
},
|
||||
|
@ -142,8 +142,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
||||
{
|
||||
mapStyle: 'community_expected_planning_application_total',
|
||||
legend: {
|
||||
title: 'Expected planning application',
|
||||
description: 'Sites identified by users as likely to be subject to planning application over the next six months',
|
||||
title: 'Expected planning applications',
|
||||
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' },
|
||||
@ -167,23 +167,89 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
||||
}
|
||||
}
|
||||
],
|
||||
[Category.Planning]: [{
|
||||
[Category.Planning]: [
|
||||
{
|
||||
// this database commad allows to see statistics about decision dates per year
|
||||
// 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;
|
||||
// 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;
|
||||
mapStyle: 'planning_applications_status_all',
|
||||
legend: {
|
||||
title: 'All planning applications available from GLA (official data)',
|
||||
disclaimer: 'The map shows official data available from the GLA Planning London Datahub. What you are looking at is mainly applications from 2019 onwards.',
|
||||
elements: [
|
||||
{ color: '#a040a0', text: 'Submitted, awaiting decision' },
|
||||
{ color: '#fff200', text: 'Appeal In Progress' },
|
||||
{ color: '#16cf15', text: 'Approved' },
|
||||
{ color: '#e31d23', text: 'Rejected' },
|
||||
{ color: '#7a84a0', text: 'Withdrawn' },
|
||||
{ color: '#eacad0', text: 'Other' },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
mapStyle: 'planning_applications_status_recent',
|
||||
legend: {
|
||||
title: 'The last 12 months - planning applications submissions/decisions (official data)',
|
||||
disclaimer: 'The map shows applications where the submission or decision data falls within the 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: '#7a84a0', 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: '#7a84a0', 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',
|
||||
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.',
|
||||
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 December 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'},
|
||||
{ color: '#858ed4', text: 'Locally Listed'},
|
||||
]
|
||||
},
|
||||
}],
|
||||
}
|
||||
],
|
||||
[Category.Sustainability]: [{
|
||||
mapStyle: 'sust_dec',
|
||||
legend: {
|
||||
@ -203,7 +269,7 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
||||
[Category.Type]: [{
|
||||
mapStyle: 'building_attachment_form',
|
||||
legend: {
|
||||
title: 'Type',
|
||||
title: 'Adjacency/Configuration',
|
||||
elements: [
|
||||
{ color: "#f2a2b9", text: "Detached" },
|
||||
{ color: "#ab8fb0", text: "Semi-Detached" },
|
||||
@ -223,12 +289,12 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
||||
{ color: '#7025a6', text: 'Residential (verified)' },
|
||||
{ color: '#ff8c00', text: 'Retail' },
|
||||
{ color: '#f5f58f', text: 'Industry & Business' },
|
||||
{ color: '#73ccd1', text: 'Community Services' },
|
||||
{ color: '#fa667d', text: 'Community Services' },
|
||||
{ color: '#ffbfbf', text: 'Recreation & Leisure' },
|
||||
{ color: '#b3de69', text: 'Transport' },
|
||||
{ color: '#cccccc', text: 'Utilities & Infrastructure' },
|
||||
{ color: '#898944', text: 'Defence' },
|
||||
{ color: '#fa667d', text: 'Agriculture' },
|
||||
{ color: '#73ccd1', text: 'Agriculture' },
|
||||
{ color: '#45cce3', text: 'Minerals' },
|
||||
{ color: '#ffffff', text: 'Vacant & Derelict' },
|
||||
{ color: '#6c6f8e', text: 'Unclassified, presumed non-residential' }
|
||||
@ -242,10 +308,10 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
|
||||
elements: []
|
||||
},
|
||||
}],
|
||||
[Category.Dynamics]: [{
|
||||
[Category.Resilience]: [{
|
||||
mapStyle: 'dynamics_demolished_count',
|
||||
legend: {
|
||||
title: 'Dynamics',
|
||||
title: 'Resilience',
|
||||
description: 'Demolished buildings on the same site',
|
||||
elements: [
|
||||
{
|
||||
|
@ -3,7 +3,7 @@ import { Category } from './categories-config';
|
||||
import AgeContainer from '../building/data-containers/age';
|
||||
import CommunityContainer from '../building/data-containers/community';
|
||||
import ConstructionContainer from '../building/data-containers/construction';
|
||||
import DynamicsContainer from '../building/data-containers/dynamics/dynamics';
|
||||
import ResilienceContainer from '../building/data-containers/resilience';
|
||||
import LocationContainer from '../building/data-containers/location';
|
||||
import PlanningContainer from '../building/data-containers/planning';
|
||||
import SizeContainer from '../building/data-containers/size';
|
||||
@ -26,7 +26,7 @@ export const categoryUiConfig: {[key in Category]: DataContainerType} = {
|
||||
[Category.Team]: TeamContainer,
|
||||
[Category.Planning]: PlanningContainer,
|
||||
[Category.Sustainability]: SustainabilityContainer,
|
||||
[Category.Dynamics]: DynamicsContainer,
|
||||
[Category.Resilience]: ResilienceContainer,
|
||||
[Category.Community]: CommunityContainer,
|
||||
};
|
||||
|
||||
|
@ -50,6 +50,8 @@ export interface DataFieldDefinition {
|
||||
*
|
||||
* Making it semantically correct is useful but not necessary.
|
||||
* E.g. for building attachment form, you could use "Detached" as example
|
||||
*
|
||||
* This field is later processed by AttributesBasedOnExample
|
||||
*/
|
||||
example: any;
|
||||
|
||||
@ -109,7 +111,7 @@ export const buildingUserFields = {
|
||||
community_expected_planning_application: {
|
||||
perUser: true,
|
||||
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
|
||||
}
|
||||
};
|
||||
@ -118,7 +120,7 @@ export const buildingUserFields = {
|
||||
export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
location_name: {
|
||||
category: Category.Location,
|
||||
title: "Building information (link)",
|
||||
title: "Building Name (Information link)",
|
||||
tooltip: "Link to a website with information on the building, not needed for most.",
|
||||
example: "https://en.wikipedia.org/wiki/Palace_of_Westminster",
|
||||
},
|
||||
@ -154,8 +156,8 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
},
|
||||
ref_toid: {
|
||||
category: Category.Location,
|
||||
title: "TOID",
|
||||
tooltip: "Ordnance Survey Topography Layer ID (to be filled automatically)",
|
||||
title: "Building Footprint ID",
|
||||
tooltip: "Ordnance Survey Topography Layer ID (TOID) [<a href='https://www.ordnancesurvey.co.uk/business-government/products/open-toid'>link</a>]",
|
||||
example: "",
|
||||
},
|
||||
|
||||
@ -165,11 +167,21 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
*/
|
||||
uprns: {
|
||||
category: Category.Location,
|
||||
title: "UPRNs",
|
||||
title: "Unique Property Reference Number(s) (UPRN)",
|
||||
tooltip: "Unique Property Reference Numbers (to be filled automatically)",
|
||||
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: {
|
||||
category: Category.Location,
|
||||
title: "OSM ID",
|
||||
@ -280,7 +292,7 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
},
|
||||
facade_year: {
|
||||
category: Category.Age,
|
||||
title: "Facade year",
|
||||
title: "Date of Front of Building",
|
||||
tooltip: "Best estimate",
|
||||
example: 1900,
|
||||
},
|
||||
@ -321,20 +333,20 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
size_storeys_core: {
|
||||
category: Category.Size,
|
||||
title: "Core storeys",
|
||||
tooltip: "How many storeys between the pavement and start of roof?",
|
||||
title: "Core Number of Floors",
|
||||
tooltip: "How many floors are there between the pavement and start of roof?",
|
||||
example: 10,
|
||||
},
|
||||
size_storeys_attic: {
|
||||
category: Category.Size,
|
||||
title: "Attic storeys",
|
||||
tooltip: "How many storeys above start of roof?",
|
||||
title: "Number of Floors within Roof Space",
|
||||
tooltip: "How many floors above start of roof?",
|
||||
example: 1,
|
||||
},
|
||||
size_storeys_basement: {
|
||||
category: Category.Size,
|
||||
title: "Basement storeys",
|
||||
tooltip: "How many storeys below pavement level?",
|
||||
title: "Number of Floors beneath Ground Level",
|
||||
tooltip: "How many floors below pavement level?",
|
||||
example: 1,
|
||||
},
|
||||
size_height_apex: {
|
||||
@ -411,20 +423,20 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
sust_breeam_rating: {
|
||||
category: Category.Sustainability,
|
||||
title: "BREEAM Rating",
|
||||
tooltip: "(Building Research Establishment Environmental Assessment Method) May not be present for many buildings",
|
||||
title: "Official Environmental Quality Rating",
|
||||
tooltip: "Building Research Establishment Environmental Assessment Method (BREEAM) May not be present for many buildings",
|
||||
example: "",
|
||||
},
|
||||
sust_dec: {
|
||||
category: Category.Sustainability,
|
||||
title: "DEC Rating",
|
||||
tooltip: "(Display Energy Certificate) Any public building should have (and display) a DEC. Showing how the energy use for that building compares to other buildings with same use",
|
||||
title: "Non-domestic Building Energy Rating",
|
||||
tooltip: "Display Energy Certificate (DEC) Any public building should have (and display) a DEC. Showing how the energy use for that building compares to other buildings with same use",
|
||||
example: "G",
|
||||
},
|
||||
sust_aggregate_estimate_epc: {
|
||||
category: Category.Sustainability,
|
||||
title: "EPC Rating",
|
||||
tooltip: "(Energy Performance Certificate) Any premises sold or rented is required to have an EPC to show how energy efficient it is. Only buildings rate grade E or higher maybe rented",
|
||||
title: "Domestic Building Energy Rating",
|
||||
tooltip: "Energy Performance Certificate (EPC) Any premises sold or rented is required to have an EPC to show how energy efficient it is. Only buildings rate grade E or higher maybe rented",
|
||||
example: "",
|
||||
},
|
||||
sust_retrofit_date: {
|
||||
@ -440,18 +452,50 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
//tooltip: ,
|
||||
},
|
||||
|
||||
historical_status: {
|
||||
category: Category.Age,
|
||||
title: "Historical Status",
|
||||
tooltip: "Survival and Loss tracked using Historical Maps",
|
||||
},
|
||||
|
||||
edit_history: {
|
||||
category: Category.Planning,
|
||||
title: "PLANNING DATA",
|
||||
tooltip: "PLANNING DATA",
|
||||
example: [{}],
|
||||
},
|
||||
|
||||
|
||||
planning_portal_link: {
|
||||
category: Category.Planning,
|
||||
title: "Planning portal link",
|
||||
title: "Local authority planning application link",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_in_conservation_area_url: {
|
||||
category: Category.Planning,
|
||||
title: "Is the building in a <a href=\"https://historicengland.org.uk/listing/what-is-designation/local/conservation-areas/\" target=\"_blank\">Conservation Area</a>?",
|
||||
title: "Is the building in a conservation area?",
|
||||
example: "",
|
||||
//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: {
|
||||
category: Category.Planning,
|
||||
title: "Conservation Area identifier",
|
||||
@ -466,49 +510,49 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
},
|
||||
planning_list_id: {
|
||||
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 a national heritage register, please add the ID:",
|
||||
example: "121436",
|
||||
//tooltip: ,
|
||||
tooltip: "e.g. National Heritage List for England (NHLE)",
|
||||
},
|
||||
planning_list_grade: {
|
||||
category: Category.Planning,
|
||||
title: "What is its grade?",
|
||||
title: "What is its rating?",
|
||||
example: "II",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_heritage_at_risk_url: {
|
||||
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 a heritage at risk register, please add the ID:",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_world_list_id: {
|
||||
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",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_glher_url: {
|
||||
category: Category.Planning,
|
||||
title: "Is it recorded on the <a href=\"https://historicengland.org.uk/advice/technical-advice/information-management/hers/\" target=\"_blank\">Historic Environment Record</a>?",
|
||||
title: "Is it recorded on any historic environment records?",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_in_apa_url: {
|
||||
category: Category.Planning,
|
||||
title: "Is it in an <a href=\"https://historicengland.org.uk/services-skills/our-planning-services/greater-london-archaeology-advisory-service/greater-london-archaeological-priority-areas/\" target=\"_blank\">Archaeological Priority Area</a>?",
|
||||
title: "Is it in an area if archaeological priority?",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_local_list_url: {
|
||||
category: Category.Planning,
|
||||
title: "Is it a <a href=\"https://historicengland.org.uk/advice/hpg/has/locallylistedhas/\" target=\"_blank\">Locally Listed Heritage Asset</a>?",
|
||||
title: "Is it a locally listed heritage asset?",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
planning_historic_area_assessment_url: {
|
||||
category: Category.Planning,
|
||||
title: "Does it have an <a href=\"https://historicengland.org.uk/images-books/publications/understanding-place-historic-area-assessments/\" target=\"_blank\">Historic Area Assessment</a>?",
|
||||
title: "Does it have any other kind of historic area assessment?",
|
||||
example: "",
|
||||
//tooltip: ,
|
||||
},
|
||||
@ -575,13 +619,13 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
},
|
||||
|
||||
dynamics_has_demolished_buildings: {
|
||||
category: Category.Dynamics,
|
||||
category: Category.Resilience,
|
||||
title: 'Were any other buildings ever built on this site?',
|
||||
example: true,
|
||||
},
|
||||
|
||||
demolished_buildings: {
|
||||
category: Category.Dynamics,
|
||||
category: Category.Resilience,
|
||||
title: 'Past (demolished) buildings on this site',
|
||||
items: {
|
||||
year_constructed: {
|
||||
|
@ -11,9 +11,13 @@ export const initialMapViewport: MapViewport = {
|
||||
zoom: config.initialZoomLevel,
|
||||
};
|
||||
|
||||
export type MapTheme = 'light' | 'night';
|
||||
export type MapTheme = 'light' | 'night' | 'night_outlines' | 'boroughs';
|
||||
|
||||
export type LayerEnablementState = 'enabled' | 'disabled';
|
||||
|
||||
export const mapBackgroundColor: Record<MapTheme, string> = {
|
||||
light: '#F0EEEB',
|
||||
night: '#162639'
|
||||
night: '#162639',
|
||||
night_outlines: '#162639',
|
||||
boroughs: '#ff0000',
|
||||
};
|
||||
|
@ -11,6 +11,9 @@ export type BuildingMapTileset = 'date_year' |
|
||||
'community_local_significance_total' |
|
||||
'community_expected_planning_application_total' |
|
||||
'community_in_public_ownership' |
|
||||
'planning_applications_status_all' |
|
||||
'planning_applications_status_recent' |
|
||||
'planning_applications_status_very_recent' |
|
||||
'planning_combined' |
|
||||
'sust_dec' |
|
||||
'building_attachment_form' |
|
||||
@ -18,6 +21,6 @@ export type BuildingMapTileset = 'date_year' |
|
||||
'dynamics_demolished_count' |
|
||||
'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;
|
||||
|
372
app/src/frontend/displayPreferences-context.tsx
Normal file
372
app/src/frontend/displayPreferences-context.tsx
Normal file
@ -0,0 +1,372 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { LayerEnablementState, MapTheme } from './config/map-config';
|
||||
|
||||
interface DisplayPreferencesContextState {
|
||||
showOverlayList: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
resetLayersAndHideTheirList: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
|
||||
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>;
|
||||
|
||||
showLayerSelection: LayerEnablementState;
|
||||
showLayerSelectionSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
showLayerSelectionSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const stub = (): never => {
|
||||
throw new Error('DisplayPreferencesProvider not set up');
|
||||
};
|
||||
|
||||
export const DisplayPreferencesContext = createContext<DisplayPreferencesContextState>({
|
||||
showOverlayList: stub,
|
||||
resetLayersAndHideTheirList: stub,
|
||||
|
||||
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,
|
||||
|
||||
showLayerSelection: undefined,
|
||||
showLayerSelectionSwitch: stub,
|
||||
showLayerSelectionSwitchOnClick: undefined,
|
||||
});
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
|
||||
const defaultVista = 'disabled'
|
||||
const defaultFlood = 'disabled'
|
||||
const defaultCreative = 'disabled'
|
||||
const defaultHousing = 'disabled'
|
||||
const defaultBorough = 'enabled'
|
||||
const defaultParcel = 'disabled'
|
||||
const defaultConservation = 'disabled'
|
||||
const defaultHistoricData = 'disabled'
|
||||
const defaultShowLayerSelection = 'disabled'
|
||||
const [vista, setVista] = useState<LayerEnablementState>(defaultVista);
|
||||
const [flood, setFlood] = useState<LayerEnablementState>(defaultFlood);
|
||||
const [creative, setCreative] = useState<LayerEnablementState>(defaultCreative);
|
||||
const [housing, setHousing] = useState<LayerEnablementState>(defaultHousing);
|
||||
const [borough, setBorough] = useState<LayerEnablementState>(defaultBorough);
|
||||
const [parcel, setParcel] = useState<LayerEnablementState>(defaultParcel);
|
||||
const [conservation, setConservation] = useState<LayerEnablementState>(defaultConservation);
|
||||
const [historicData, setHistoricData] = useState<LayerEnablementState>(defaultHistoricData);
|
||||
const [darkLightTheme, setDarkLightTheme] = useState<MapTheme>('night');
|
||||
const [showLayerSelection, setShowLayerSelection] = useState<LayerEnablementState>(defaultShowLayerSelection);
|
||||
|
||||
const showOverlayList = useCallback(
|
||||
(e) => {
|
||||
setShowLayerSelection('enabled')
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const resetLayersAndHideTheirList = useCallback(
|
||||
(e) => {
|
||||
setVista(defaultVista);
|
||||
setFlood(defaultFlood);
|
||||
setCreative(defaultCreative);
|
||||
setHousing(defaultHousing);
|
||||
setBorough(defaultBorough)
|
||||
setParcel(defaultParcel);
|
||||
setConservation(defaultConservation);
|
||||
setHistoricData(defaultHistoricData);
|
||||
setShowLayerSelection(defaultShowLayerSelection); // reset layers + hiding this panel is integrated into one action
|
||||
//setDarkLightTheme('night'); // reset only layers
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
function anyLayerModifiedState() {
|
||||
if(vista != defaultVista) {
|
||||
return true;
|
||||
}
|
||||
if(flood != defaultFlood) {
|
||||
return true;
|
||||
}
|
||||
if(creative != defaultCreative) {
|
||||
return true;
|
||||
}
|
||||
if(housing != defaultHousing) {
|
||||
return true;
|
||||
}
|
||||
if(borough != defaultBorough) {
|
||||
return true;
|
||||
}
|
||||
if(parcel != defaultParcel) {
|
||||
return true;
|
||||
}
|
||||
if(conservation != defaultConservation) {
|
||||
return true;
|
||||
}
|
||||
if(historicData != defaultHistoricData) {
|
||||
return true;
|
||||
}
|
||||
//darkLightTheme not handled here
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const showLayerSelectionSwitch = useCallback(
|
||||
(e) => {
|
||||
flipShowLayerSelection(e)
|
||||
},
|
||||
[showLayerSelection],
|
||||
)
|
||||
const showLayerSelectionSwitchOnClick = (e) => {
|
||||
flipShowLayerSelection(e)
|
||||
}
|
||||
function flipShowLayerSelection(e) {
|
||||
e.preventDefault();
|
||||
const newShowLayerSelection = (showLayerSelection === 'enabled')? 'disabled' : 'enabled';
|
||||
setShowLayerSelection(newShowLayerSelection);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<DisplayPreferencesContext.Provider value={{
|
||||
showOverlayList,
|
||||
resetLayersAndHideTheirList,
|
||||
|
||||
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,
|
||||
|
||||
showLayerSelection,
|
||||
showLayerSelectionSwitch,
|
||||
showLayerSelectionSwitchOnClick
|
||||
}}>
|
||||
{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
|
@ -55,16 +55,26 @@ function getCurrentMenuLinks(username: string): MenuLink[][] {
|
||||
to: "/data-extracts.html",
|
||||
text: "Download data"
|
||||
},
|
||||
{
|
||||
to: "https://github.com/colouring-cities/manual/wiki",
|
||||
text: "Open Manual - Wiki",
|
||||
external: true
|
||||
},
|
||||
{
|
||||
to: config.githubURL,
|
||||
text: "Access open code",
|
||||
text: "Open code",
|
||||
external: true
|
||||
},
|
||||
{
|
||||
to: "https://github.com/colouring-cities/manual/wiki",
|
||||
text: "Colouring Cities Open Manual/Wiki",
|
||||
disabled: false,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
to: "/showcase.html",
|
||||
text: "View Data Showcase",
|
||||
text: "Case Study Showcase",
|
||||
disabled: true,
|
||||
note: "Coming soon"
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -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 loadable from '@loadable/component';
|
||||
|
||||
@ -18,7 +18,8 @@ import Welcome from './pages/welcome';
|
||||
import { PrivateRoute } from './route';
|
||||
import { useLastNotEmpty } from './hooks/use-last-not-empty';
|
||||
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 { useAuth } from './auth-context';
|
||||
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 => {
|
||||
const { user } = useAuth();
|
||||
const [categoryUrlParam] = useUrlCategoryParam();
|
||||
@ -121,6 +131,11 @@ export const MapApp: React.FC<MapAppProps> = props => {
|
||||
}
|
||||
}, [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 (
|
||||
<>
|
||||
<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 ?? {}}
|
||||
onBuildingUpdate={handleBuildingUpdate}
|
||||
onUserVerifiedUpdate={handleUserVerifiedUpdate}
|
||||
mapColourScale={mapColourScale}
|
||||
onMapColourScale={setMapColourScale}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
@ -158,9 +175,11 @@ export const MapApp: React.FC<MapAppProps> = props => {
|
||||
<ColouringMap
|
||||
selectedBuildingId={selectedBuildingId}
|
||||
mode={mode || 'basic'}
|
||||
category={displayCategory}
|
||||
revisionId={revisionId}
|
||||
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 ${borough}-state ${darkLightTheme}`} onSubmit={boroughSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(borough === 'enabled')? 'Borough Boundaries on' : 'Borough Boundaries off'}
|
||||
</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 ${conservation}-state ${darkLightTheme}`} onSubmit={conservationSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(conservation === 'enabled')? 'Conservation Areas on' : 'Conservation Areas off'}
|
||||
</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 ${creative}-state ${darkLightTheme}`} onSubmit={creativeSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(creative === 'enabled')? 'Enterprise Zones on' : 'Creative Enterprise Zones off'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
29
app/src/frontend/map/data-switcher.tsx
Normal file
29
app/src/frontend/map/data-switcher.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import './map-button.css';
|
||||
import { useDisplayPreferences } from '../displayPreferences-context';
|
||||
|
||||
interface DataLayerSwitcherProps {
|
||||
}
|
||||
|
||||
const DataLayerSwitcher: React.FC<DataLayerSwitcherProps> = (props) => {
|
||||
const { showLayerSelection, showOverlayList, resetLayersAndHideTheirList, darkLightTheme } = useDisplayPreferences();
|
||||
const handleSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
if (showLayerSelection === 'enabled') {
|
||||
resetLayersAndHideTheirList(evt)
|
||||
} else {
|
||||
showOverlayList(evt)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<form className={`data-switcher map-button ${darkLightTheme}`} onSubmit={handleSubmit}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(showLayerSelection === 'enabled')? 'Clear 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 ${flood}-state ${darkLightTheme}`} onSubmit={floodSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(flood === 'enabled')? 'Flood Zones on' : 'Flood Zones of'}
|
||||
</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 ${historicData}-state ${darkLightTheme}`} onSubmit={historicDataSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(historicData === 'enabled')? 'The OS 1890s Historical Map on' : 'The OS 1890s Historical Map off'}
|
||||
</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 ${housing}-state ${darkLightTheme}`} onSubmit={housingSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(housing === 'enabled')? 'Housing Zones on' : 'Housing Zones off'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
28
app/src/frontend/map/layers/borough-boundary-layer.tsx
Normal file
28
app/src/frontend/map/layers/borough-boundary-layer.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
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}}
|
||||
/>;
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
17
app/src/frontend/map/layers/borough-label-layer.tsx
Normal file
17
app/src/frontend/map/layers/borough-label-layer.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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 BoroughLabelLayer({}) {
|
||||
const { borough } = useDisplayPreferences();
|
||||
|
||||
if(borough == "enabled") {
|
||||
return <BuildingBaseLayerAllZoom theme="boroughs" />;
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
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={false}
|
||||
/>;
|
||||
}
|
@ -14,6 +14,6 @@ export function BuildingBaseLayer({ theme }: {theme: MapTheme}) {
|
||||
url={getTileLayerUrl(tileset)}
|
||||
minZoom={14}
|
||||
maxZoom={19}
|
||||
detectRetina={true}
|
||||
detectRetina={false}
|
||||
/>;
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ export function BuildingDataLayer({tileset, revisionId} : { tileset: BuildingMap
|
||||
url={getTileLayerUrl(tileset, {rev: revisionId})}
|
||||
minZoom={9}
|
||||
maxZoom={19}
|
||||
detectRetina={true}
|
||||
detectRetina={false}
|
||||
/>;
|
||||
}
|
@ -11,6 +11,6 @@ export function BuildingHighlightLayer({selectedBuildingId, baseTileset}: {selec
|
||||
url={getTileLayerUrl('highlight', {highlight: `${selectedBuildingId}`, base: baseTileset})}
|
||||
minZoom={13}
|
||||
maxZoom={19}
|
||||
detectRetina={true}
|
||||
detectRetina={false}
|
||||
/>;
|
||||
}
|
||||
|
@ -9,6 +9,6 @@ export function BuildingNumbersLayer({revisionId}: {revisionId: string}) {
|
||||
url={getTileLayerUrl('number_labels', {rev: revisionId})}
|
||||
minZoom={17}
|
||||
maxZoom={19}
|
||||
detectRetina={true}
|
||||
detectRetina={false}
|
||||
/>;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export function CityBaseMapLayer({ theme }: { theme: MapTheme }) {
|
||||
attribution={attribution}
|
||||
maxNativeZoom={18}
|
||||
maxZoom={19}
|
||||
detectRetina={true}
|
||||
detectRetina={false}
|
||||
className={theme_class}
|
||||
/>;
|
||||
}
|
||||
|
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. The 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>
|
||||
}
|
||||
}
|
29
app/src/frontend/map/layers/parcel-boundary-layer.tsx
Normal file
29
app/src/frontend/map/layers/parcel-boundary-layer.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
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 {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
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 the 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" />
|
||||
{
|
||||
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 =>
|
||||
<option key={def.mapStyle} value={def.mapStyle}>{def.legend.title}</option>
|
||||
@ -81,7 +81,7 @@ export const Legend : FC<LegendProps> = ({
|
||||
}
|
||||
{
|
||||
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'} >
|
||||
{
|
||||
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
||||
|
147
app/src/frontend/map/map-button.css
Normal file
147
app/src/frontend/map/map-button.css
Normal file
@ -0,0 +1,147 @@
|
||||
.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;
|
||||
color: #191b1d;
|
||||
}
|
||||
.map-button .btn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.map-button.night .btn {
|
||||
color: #fff;
|
||||
background-color: #343a40;
|
||||
border-color: #343a40;
|
||||
}
|
||||
.map-button.night .btn:hover {
|
||||
color: #d4d7da;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border-color: #343a40;
|
||||
}
|
||||
.map-switcher-inline.night.no-applicable-state {
|
||||
color: #191b1d;
|
||||
}
|
||||
.map-switcher-inline.disabled-state,
|
||||
.map-button.disabled-state,
|
||||
.map-button.disabled-state .btn{
|
||||
background-color: #df7474;
|
||||
}
|
||||
.map-switcher-inline.night.disabled-state,
|
||||
.map-button.night.disabled-state,
|
||||
.map-button.night.disabled-state .btn{
|
||||
background-color: #b03f3f;
|
||||
}
|
||||
.map-button.enabled-state {
|
||||
color: #75e775;
|
||||
background-color: #75e775;
|
||||
}
|
||||
.map-switcher-inline.enabled-state,
|
||||
.map-button.enabled-state .btn{
|
||||
background-color: #75e775;
|
||||
}
|
||||
.map-button.night.enabled-state {
|
||||
color: #448844;
|
||||
background-color: #448844;
|
||||
}
|
||||
.map-switcher-inline.night.enabled-state,
|
||||
.map-button.night.enabled-state .btn{
|
||||
background-color: #448844;
|
||||
}
|
||||
|
||||
.map-switcher-inline.night.no-applicable-state,
|
||||
.map-button.enabled .btn:hover,
|
||||
.map-button.night.enabled .btn:hover {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
.map-switcher-inline {
|
||||
border-radius: 4px;
|
||||
/*background: #FFC0CB;*/
|
||||
margin: 12px;
|
||||
min-width: 400px;
|
||||
color: #343a40;
|
||||
}
|
||||
.map-switcher-inline.night {
|
||||
color: #d4d7da;
|
||||
}
|
||||
/*
|
||||
.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;
|
||||
}
|
||||
*/
|
@ -28,19 +28,6 @@
|
||||
.leaflet-grab {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.map-notice {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 1000;
|
||||
padding: 0.5rem 0.75rem;
|
||||
width: 250px;
|
||||
background: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 0px 1px 1px #222;
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 990px){
|
||||
/* Only show the "Click a building ..." notice for larger screens */
|
||||
.map-notice {
|
||||
|
@ -5,14 +5,21 @@ import 'leaflet/dist/leaflet.css';
|
||||
import './map.css';
|
||||
|
||||
import { apiGet } from '../apiHelpers';
|
||||
import { HelpIcon } from '../components/icons';
|
||||
import { categoryMapsConfig } from '../config/category-maps-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 { CityBaseMapLayer } from './layers/city-base-map-layer';
|
||||
import { CityBoundaryLayer } from './layers/city-boundary-layer';
|
||||
import { BoroughBoundaryLayer } from './layers/borough-boundary-layer';
|
||||
import { BoroughLabelLayer } from './layers/borough-label-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 { BuildingDataLayer } from './layers/building-data-layer';
|
||||
import { BuildingNumbersLayer } from './layers/building-numbers-layer';
|
||||
@ -21,30 +28,43 @@ import { BuildingHighlightLayer } from './layers/building-highlight-layer';
|
||||
import { Legend } from './legend';
|
||||
import SearchBox from './search-box';
|
||||
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 { useDisplayPreferences } from '../displayPreferences-context';
|
||||
import { CategoryMapDefinition } from '../config/category-maps-config';
|
||||
|
||||
interface ColouringMapProps {
|
||||
selectedBuildingId: number;
|
||||
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||
category: Category;
|
||||
revisionId: string;
|
||||
onBuildingAction: (building: Building) => void;
|
||||
mapColourScale: BuildingMapTileset;
|
||||
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||
categoryMapDefinitions: CategoryMapDefinition[]
|
||||
}
|
||||
|
||||
export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
category,
|
||||
mode,
|
||||
revisionId,
|
||||
onBuildingAction,
|
||||
selectedBuildingId,
|
||||
mapColourScale,
|
||||
onMapColourScale,
|
||||
categoryMapDefinitions,
|
||||
children
|
||||
}) => {
|
||||
|
||||
const [theme, setTheme] = useState<MapTheme>('night');
|
||||
const { darkLightTheme, darkLightThemeSwitch, showLayerSelection } = useDisplayPreferences();
|
||||
const [position, setPosition] = useState(initialMapViewport.position);
|
||||
const [zoom, setZoom] = useState(initialMapViewport.zoom);
|
||||
|
||||
const [mapColourScale, setMapColourScale] = useState<BuildingMapTileset>();
|
||||
|
||||
const handleLocate = useCallback(
|
||||
(lat: number, lng: number, zoom: number) => {
|
||||
@ -64,26 +84,6 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
[onBuildingAction],
|
||||
)
|
||||
|
||||
const themeSwitch = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const newTheme = (theme === 'light')? 'night' : 'light';
|
||||
setTheme(newTheme);
|
||||
},
|
||||
[theme],
|
||||
)
|
||||
|
||||
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 isEdit = ['edit', 'multi-edit'].includes(mode);
|
||||
|
||||
return (
|
||||
<div className="map-container">
|
||||
<MapContainer
|
||||
@ -96,16 +96,23 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
attributionControl={false}
|
||||
>
|
||||
<ClickHandler onClick={handleClick} />
|
||||
<MapBackgroundColor theme={theme} />
|
||||
<MapBackgroundColor theme={darkLightTheme} />
|
||||
<MapViewport position={position} zoom={zoom} />
|
||||
|
||||
<Pane
|
||||
key={theme}
|
||||
key={darkLightTheme}
|
||||
name={'cc-base-pane'}
|
||||
style={{zIndex: 50}}
|
||||
>
|
||||
<CityBaseMapLayer theme={theme} />
|
||||
<BuildingBaseLayer theme={theme} />
|
||||
<CityBaseMapLayer theme={darkLightTheme} />
|
||||
<BuildingBaseLayer theme={darkLightTheme} />
|
||||
</Pane>
|
||||
|
||||
<Pane
|
||||
name='cc-overlay-pane-shown-behind-buildings'
|
||||
style={{zIndex: 199}}
|
||||
>
|
||||
<ConservationAreaBoundaryLayer/>
|
||||
</Pane>
|
||||
|
||||
{
|
||||
@ -120,7 +127,14 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
name='cc-overlay-pane'
|
||||
style={{zIndex: 300}}
|
||||
>
|
||||
<CityBoundaryLayer />
|
||||
<CityBoundaryLayer/>
|
||||
<HistoricDataLayer/>
|
||||
<BoroughBoundaryLayer/>
|
||||
<ParcelBoundaryLayer/>
|
||||
<FloodBoundaryLayer/>
|
||||
<VistaBoundaryLayer/>
|
||||
<HousingBoundaryLayer/>
|
||||
<CreativeBoundaryLayer/>
|
||||
<BuildingNumbersLayer revisionId={revisionId} />
|
||||
{
|
||||
selectedBuildingId &&
|
||||
@ -130,6 +144,12 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
/>
|
||||
}
|
||||
</Pane>
|
||||
<Pane
|
||||
name='cc-label-overlay-pane'
|
||||
style={{zIndex: 1000}}
|
||||
>
|
||||
<BoroughLabelLayer/>
|
||||
</Pane>
|
||||
|
||||
<ZoomControl position="topright" />
|
||||
<AttributionControl prefix=""/>
|
||||
@ -137,14 +157,24 @@ export const ColouringMap : FC<ColouringMapProps> = ({
|
||||
{
|
||||
mode !== 'basic' &&
|
||||
<>
|
||||
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={onMapColourScale}/>
|
||||
<ThemeSwitcher onSubmit={darkLightThemeSwitch} currentTheme={darkLightTheme} />
|
||||
<DataLayerSwitcher />
|
||||
{
|
||||
!hasSelection &&
|
||||
<div className="map-notice">
|
||||
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||
</div>
|
||||
(showLayerSelection == "enabled") ?
|
||||
<>
|
||||
<BoroughSwitcher/>
|
||||
<ParcelSwitcher/>
|
||||
<FloodSwitcher/>
|
||||
<ConservationAreaSwitcher/>
|
||||
<HistoricDataSwitcher/>
|
||||
<VistaSwitcher />
|
||||
<HousingSwitcher />
|
||||
<CreativeSwitcher />
|
||||
</>
|
||||
: <></>
|
||||
}
|
||||
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={setMapColourScale}/>
|
||||
<ThemeSwitcher onSubmit={themeSwitch} currentTheme={theme} />
|
||||
{/* TODO change remaining ones*/}
|
||||
<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 ${parcel}-state ${darkLightTheme}`} onSubmit={parcelSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(parcel === 'enabled')? 'Parcel overlay (sample) on' : 'Parcel overlay (sample) off'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -103,10 +103,7 @@ class SearchBox extends Component<SearchBoxProps, SearchBoxState> {
|
||||
apiGet(`/api/search?q=${this.state.q}`)
|
||||
.then((data) => {
|
||||
if (data && data.results){
|
||||
this.setState({
|
||||
results: data.results,
|
||||
fetching: false
|
||||
});
|
||||
this.props.onLocate(data.results[0].geometry.coordinates[1], data.results[0].geometry.coordinates[0], data.results[0].attributes.zoom)
|
||||
} else {
|
||||
console.error(data);
|
||||
|
||||
@ -157,31 +154,7 @@ class SearchBox extends Component<SearchBoxProps, SearchBoxState> {
|
||||
);
|
||||
}
|
||||
|
||||
const resultsList = this.state.results.length?
|
||||
<ul className="search-box-results">
|
||||
{
|
||||
this.state.results.map((result) => {
|
||||
const label = result.attributes.label;
|
||||
const lng = result.geometry.coordinates[0];
|
||||
const lat = result.geometry.coordinates[1];
|
||||
const zoom = result.attributes.zoom;
|
||||
const href = `?lng=${lng}&lat=${lat}&zoom=${zoom}`;
|
||||
return (
|
||||
<li key={result.attributes.label}>
|
||||
<a
|
||||
className="search-box-result"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.props.onLocate(lat, lng, zoom);
|
||||
}}
|
||||
href={href}
|
||||
>{`${label.substring(0, 4)} ${label.substring(4, 7)}`}</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
: null;
|
||||
const resultsList = null;
|
||||
return (
|
||||
<div className="search-box" onKeyDown={this.handleKeyPress}>
|
||||
<div className="search-box-pane">
|
||||
|
@ -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 './theme-switcher.css';
|
||||
import './map-button.css';
|
||||
|
||||
interface ThemeSwitcherProps {
|
||||
currentTheme: string;
|
||||
@ -8,7 +8,7 @@ interface ThemeSwitcherProps {
|
||||
}
|
||||
|
||||
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"
|
||||
type="submit">
|
||||
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 ${vista}-state ${darkLightTheme}`} onSubmit={vistaSwitch}>
|
||||
<button className="btn btn-outline btn-outline-dark"
|
||||
type="submit">
|
||||
{(vista === 'enabled')? 'Protected Vistas on' : 'Protected Vistas off'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -4,10 +4,12 @@ import { Link } from 'react-router-dom';
|
||||
import { CCConfig } from '../../cc-config';
|
||||
let config: CCConfig = require('../../cc-config.json')
|
||||
|
||||
import Categories from '../building/categories';
|
||||
import './welcome.css';
|
||||
|
||||
const Welcome = () => (
|
||||
<div className="section-body welcome">
|
||||
<Categories mode="view"/>
|
||||
<h1 className="h2">Welcome to Colouring {config.cityName}!</h1>
|
||||
<p>
|
||||
|
||||
|
@ -39,7 +39,7 @@ export const AuthRoute: React.FC<RouteProps> = ({ component: Component, children
|
||||
if(isAuthenticated) {
|
||||
let state = props.location.state as any;
|
||||
let from = '/my-account.html';
|
||||
if (typeof state == 'object' && 'from' in state){
|
||||
if (typeof state == 'object' && state !== null && 'from' in state) {
|
||||
from = state.from;
|
||||
}
|
||||
|
||||
|
@ -66,13 +66,13 @@
|
||||
background-color: #f77d11;
|
||||
}
|
||||
.background-age {
|
||||
background-color: #ff6161;
|
||||
background-color: #ab8fb0;
|
||||
}
|
||||
.background-size {
|
||||
background-color: #f2a2b9;
|
||||
background-color: #ff6161;
|
||||
}
|
||||
.background-construction {
|
||||
background-color: #ab8fb0;
|
||||
background-color: #f2a2b9;
|
||||
}
|
||||
.background-streetscape {
|
||||
background-color: #718899;
|
||||
@ -86,7 +86,7 @@
|
||||
.background-sustainability {
|
||||
background-color: #6bb1e3;
|
||||
}
|
||||
.background-dynamics {
|
||||
.background-resilience {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
.background-community {
|
||||
|
@ -7,6 +7,7 @@ import serialize from 'serialize-javascript';
|
||||
import {
|
||||
getBuildingById,
|
||||
getBuildingUPRNsById,
|
||||
getBuildingPlanningDataById,
|
||||
getLatestRevisionId,
|
||||
getUserVerifiedAttributes
|
||||
} from './api/services/building/base';
|
||||
@ -36,13 +37,14 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
||||
}
|
||||
|
||||
try {
|
||||
let [user, building, uprns, userVerified, latestRevisionId] = await Promise.all([
|
||||
let [user, building, uprns, planningData, userVerified, latestRevisionId] = await Promise.all([
|
||||
userId ? getUserById(userId) : undefined,
|
||||
isBuilding ? getBuildingById(
|
||||
buildingId,
|
||||
{ userDataOptions: userId ? { userId, userAttributes: true } : null }
|
||||
) : undefined,
|
||||
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
|
||||
isBuilding ? getBuildingPlanningDataById(buildingId) : undefined,
|
||||
(isBuilding && userId) ? getUserVerifiedAttributes(buildingId, userId) : {},
|
||||
getLatestRevisionId()
|
||||
]);
|
||||
@ -56,6 +58,9 @@ const frontendRoute = asyncController(async (req: express.Request, res: express.
|
||||
if (data.building != null) {
|
||||
data.building.uprns = uprns;
|
||||
}
|
||||
if (data.building != null) {
|
||||
data.building.planning_data = planningData;
|
||||
}
|
||||
data.latestRevisionId = latestRevisionId;
|
||||
renderHTML(context, data, req, res);
|
||||
} catch(error) {
|
||||
|
@ -13,6 +13,17 @@ const LAYER_QUERIES = {
|
||||
geometry_id
|
||||
FROM
|
||||
buildings`,
|
||||
base_night_outlines: `
|
||||
SELECT
|
||||
geometry_id
|
||||
FROM
|
||||
buildings`,
|
||||
base_boroughs: `
|
||||
SELECT
|
||||
geometry_id,
|
||||
name
|
||||
FROM
|
||||
external_data_borough_boundary`,
|
||||
number_labels:`
|
||||
SELECT
|
||||
geometry_id,
|
||||
@ -136,6 +147,25 @@ const LAYER_QUERIES = {
|
||||
WHERE
|
||||
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,
|
||||
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_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: `
|
||||
SELECT
|
||||
geometry_id,
|
||||
@ -221,6 +251,29 @@ function getDataConfig(tileset: string): DataConfig {
|
||||
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 = `(
|
||||
SELECT
|
||||
d.*,
|
||||
|
@ -32,14 +32,10 @@ let shouldCacheFn: (t: TileParams) => boolean;
|
||||
if(!allLayersCacheSwitch) {
|
||||
shouldCacheFn = t => false;
|
||||
} else if(dataLayersCacheSwitch) {
|
||||
// cache age data and base building outlines for more zoom levels than other layers
|
||||
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
||||
(tileset === 'date_year' && z <= 16) ||
|
||||
(['base_light', 'base_night'].includes(tileset) && z <= 17) ||
|
||||
z <= 13;
|
||||
shouldCacheFn = ({ tileset, z }: TileParams) => z <= 18;
|
||||
} else {
|
||||
shouldCacheFn = ({ tileset, z }: TileParams) =>
|
||||
['base_light', 'base_night'].includes(tileset) && z <= 17;
|
||||
['base_light', 'base_night', 'base_night_outlines', 'base_boroughs'].includes(tileset) && z <= 18;
|
||||
}
|
||||
|
||||
const tileCache = new TileCache(
|
||||
@ -52,8 +48,8 @@ const tileCache = new TileCache(
|
||||
},
|
||||
shouldCacheFn,
|
||||
|
||||
// don't clear base_light and base_night on bounding box cache clear
|
||||
(tileset: string) => tileset !== 'base_light' && tileset !== 'base_night'
|
||||
// don't clear on bounding box cache clear tilesets not affected by user-editable data
|
||||
(tileset: string) => tileset !== 'base_light' && tileset !== 'base_night' && tileset !== 'base_night_outlines' && tileset !== 'base_borough' && 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);
|
||||
@ -63,7 +59,7 @@ function cacheOrCreateBuildingTile(tileParams: TileParams, dataParams: any): Pro
|
||||
}
|
||||
|
||||
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
|
||||
return stitchTile(tileParams, dataParams, cacheOrCreateBuildingTile);
|
||||
} 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,311 @@
|
||||
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']
|
||||
application_id_with_borough_identifier = entry['_source']['id']
|
||||
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": "the Greater London Authority's Planning London Datahub",
|
||||
"data_source_link": "https://www.london.gov.uk/programmes-strategies/planning/digital-planning/planning-london-datahub",
|
||||
"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 application_id_with_borough_identifier:
|
||||
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_retrofit_date`: year of last significant retrofit
|
||||
- `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_in_conservation_area_id`: conservation area ID
|
||||
- `planning_in_conservation_area_url`: conservation area appraisal link
|
||||
|
@ -1,15 +1,15 @@
|
||||
-- Remove sustainability fields, update in paralell with adding new fields
|
||||
-- Remove sustainability fields, update in parallel with adding new fields
|
||||
-- BREEAM rating
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_breeam_rating;
|
||||
-- BREEAM date
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_breeam_date;
|
||||
|
||||
-- DEC (display energy certifcate, only applies to non domestic buildings)
|
||||
-- DEC (display energy certificate, only applies to non domestic buildings)
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec;
|
||||
-- DEC date
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec_date;
|
||||
|
||||
--DEC certifcate lmk key, this would be lmkkey, no online lookup but can scrape through API. Numeric (25)
|
||||
--DEC certificate lmk key, this would be lmkkey, no online lookup but can scrape through API. Numeric (25)
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_dec_lmkey;
|
||||
|
||||
-- Aggregate EPC rating (Estimated) for a building, derived from inidividual certificates
|
||||
|
@ -1,4 +1,4 @@
|
||||
-- Remove sustainability fields, update in paralell with adding new fields
|
||||
-- Remove sustainability fields, update in parallel with adding new fields
|
||||
-- Last significant retrofit date YYYY
|
||||
-- Need to add a constraint to sust_retrofit_date
|
||||
-- Renewal technologies
|
||||
@ -6,7 +6,7 @@
|
||||
-- Values:
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_renewables_tech;
|
||||
|
||||
--Has a building had a major renovation without extenstion (captured in form)
|
||||
--Has a building had a major renovation without extension (captured in form)
|
||||
--Boolean yes/no - links to the the DATE
|
||||
ALTER TABLE buildings DROP COLUMN IF EXISTS sust_retrofitted;
|
||||
|
||||
|
@ -20,7 +20,7 @@ ALTER TABLE buildings
|
||||
ALTER TABLE buildings
|
||||
ADD COLUMN IF NOT EXISTS sust_breeam_date smallint;
|
||||
|
||||
-- DEC (display energy certifcate, only applies to non domestic buildings)
|
||||
-- DEC (display energy certificate, only applies to non domestic buildings)
|
||||
-- A - G
|
||||
CREATE TYPE sust_dec
|
||||
AS ENUM ('A',
|
||||
|
@ -1,10 +1,10 @@
|
||||
-- Remove sustainability fields, update in paralell with adding new fields
|
||||
-- Remove sustainability fields, update in parallel with adding new fields
|
||||
-- Last significant retrofit date YYYY
|
||||
-- Need to add a constraint to sust_retrofit_date
|
||||
ALTER TABLE buildings
|
||||
ADD CONSTRAINT sust_retrofit_date_end CHECK (sust_retrofit_date <= DATE_PART('year', CURRENT_DATE));
|
||||
|
||||
--Has a building had a major renovation without extenstion (captured in form)
|
||||
--Has a building had a major renovation without extension (captured in form)
|
||||
--Boolean yes/no - links to the the DATE
|
||||
ALTER TABLE buildings
|
||||
ADD COLUMN IF NOT EXISTS sust_retrofitted boolean DEFAULT 'n';
|
||||
|
@ -1,4 +1,4 @@
|
||||
--Landuse is hierachical. Highest level is Order (ie. Residential) then Group (ie Residential-Dwelling) then Class (ie Residential-Dwelling-Detached house)
|
||||
--Landuse is hierarchical. Highest level is Order (ie. Residential) then Group (ie Residential-Dwelling) then Class (ie Residential-Dwelling-Detached house)
|
||||
--Interface will collected most detailed (class) but visualise highest level (order)
|
||||
--Landuse is a table as #358
|
||||
--Land use class, group and order will be stored in a new table
|
||||
|
@ -1,5 +1,5 @@
|
||||
-- Create land use and fields
|
||||
--Landuse is hierachical. Highest level is Order (Residential) then Group (Residential-Dwelling) then Class (Residential-Dwelling-Detached house)
|
||||
--Landuse is hierarchical. Highest level is Order (Residential) then Group (Residential-Dwelling) then Class (Residential-Dwelling-Detached house)
|
||||
--Some ETL work required to get this together refer to analysis repo
|
||||
--Prerequesite is to have first run bulk_data_sources migrations
|
||||
--Then create table landuse_order for the app, this is used as foreign key for current and original landuse_order
|
||||
@ -36,11 +36,11 @@ FROM reference_tables.landuse_classifications a
|
||||
WHERE a.level = 'class'
|
||||
AND a.is_used;
|
||||
|
||||
--Landuse is hierachical. Highest level is Order (Residential) then Group (Residential-Dwelling) then Class (Residential-Dwelling-Detached house)
|
||||
--Landuse is hierarchical. Highest level is Order (Residential) then Group (Residential-Dwelling) then Class (Residential-Dwelling-Detached house)
|
||||
--Interface will collected most detailed (class) but visualise highest level (order)
|
||||
--Landuse is a table as #358
|
||||
--Prerequisite run bulk_sources migration first
|
||||
-- Land use is table with 3 levels of hierachy (highest to lowest). order > group > class
|
||||
-- Land use is table with 3 levels of hierarchy (highest to lowest). order > group > class
|
||||
|
||||
|
||||
-- Land use order, singular. Client and db constrained with foreign key
|
||||
|
@ -3,7 +3,7 @@
|
||||
-- -- Ownership type, enumerate type from:
|
||||
-- ALTER TABLE buildings DROP COLUMN IF EXISTS ownership_type;
|
||||
|
||||
-- -- Ownerhsip perception, would you describe this as a community asset?
|
||||
-- -- Ownership perception, would you describe this as a community asset?
|
||||
-- -- Boolean yes / no
|
||||
-- ALTER TABLE buildings DROP COLUMN IF EXISTS ownership_perception;
|
||||
|
||||
|
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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user