Merge branch 'master' into colouring-core

This commit is contained in:
Mike Simpson 2023-02-03 11:28:45 +00:00
commit 7aebd77f10
105 changed files with 16052 additions and 729 deletions

View File

@ -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] &lt; 366</Filter>
<PolygonSymbolizer fill="#a040a0"/>
<LineSymbolizer stroke="#a040a0" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Approved" and [days_since_decision_date] &lt; 366</Filter>
<PolygonSymbolizer fill="#16cf15"/>
<LineSymbolizer stroke="#16cf15" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Appeal In Progress" and [days_since_decision_date] &lt; 366</Filter>
<PolygonSymbolizer fill="#fff200"/>
<LineSymbolizer stroke="#fff200" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Rejected" and [days_since_decision_date] &lt; 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] &lt; 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] &lt; 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] &lt;= 30</Filter>
<PolygonSymbolizer fill="#a040a0"/>
<LineSymbolizer stroke="#a040a0" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Approved" and [days_since_decision_date] &lt;= 30</Filter>
<PolygonSymbolizer fill="#16cf15"/>
<LineSymbolizer stroke="#16cf15" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Appeal In Progress" and [days_since_decision_date] &lt;= 30</Filter>
<PolygonSymbolizer fill="#fff200"/>
<LineSymbolizer stroke="#fff200" stroke-width="1.75" />
</Rule>
<Rule>
<Filter>[status] = "Rejected" and [days_since_decision_date] &lt;= 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] &lt;= 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] &lt;= 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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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,
},

View File

@ -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,

View File

@ -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')

View File

@ -56,4 +56,5 @@ export * from './edit';
export * from './history';
export * from './query';
export * from './uprn';
export * from './planningData';
export * from './verify';

View 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;
}
}

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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}
/>;
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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}
/>

View File

@ -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>
</>
);

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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 {

View File

@ -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}

View File

@ -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;

View File

@ -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}
/>

View File

@ -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 authoritys 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 authoritys 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;

View 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;

View File

@ -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"

View File

@ -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);

View File

@ -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'

View File

@ -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'

View File

@ -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;

View File

@ -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'}) => (

View File

@ -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>

View File

@ -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?'
},

View File

@ -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: '5099' },
@ -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: '5099' },
{ color: '#fc4e2a', text: '2049' },
{ color: '#fd8d3c', text: '1019' },
{ color: '#feb24c', text: '39' },
{ 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: [
{

View File

@ -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,
};

View File

@ -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: {

View File

@ -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',
};

View File

@ -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;

View 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);
};

View 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

View File

@ -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"
},
],
[

View File

@ -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}
/>
</>
);

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 <></>
}
}

View 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 <></>
}
}

View 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}
/>;
}

View File

@ -14,6 +14,6 @@ export function BuildingBaseLayer({ theme }: {theme: MapTheme}) {
url={getTileLayerUrl(tileset)}
minZoom={14}
maxZoom={19}
detectRetina={true}
detectRetina={false}
/>;
}

View File

@ -11,6 +11,6 @@ export function BuildingDataLayer({tileset, revisionId} : { tileset: BuildingMap
url={getTileLayerUrl(tileset, {rev: revisionId})}
minZoom={9}
maxZoom={19}
detectRetina={true}
detectRetina={false}
/>;
}

View File

@ -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}
/>;
}

View File

@ -9,6 +9,6 @@ export function BuildingNumbersLayer({revisionId}: {revisionId: string}) {
url={getTileLayerUrl('number_labels', {rev: revisionId})}
minZoom={17}
maxZoom={19}
detectRetina={true}
detectRetina={false}
/>;
}

View File

@ -31,7 +31,7 @@ export function CityBaseMapLayer({ theme }: { theme: MapTheme }) {
attribution={attribution}
maxNativeZoom={18}
maxZoom={19}
detectRetina={true}
detectRetina={false}
className={theme_class}
/>;
}

View 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}}/>;
}
}

View 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>
}
}

View 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>
}
}

View 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='&copy; 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;
}
}

View 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>
}
}

View 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>
}
}

View 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 <></>
}
}

View File

@ -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>

View 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;
}
*/

View File

@ -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 {

View File

@ -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} />
</>
}

View 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>
);
}

View File

@ -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">

View File

@ -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;
}
}

View File

@ -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'})

View 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>
);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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.*,

View File

@ -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
View File

@ -0,0 +1,2 @@
*.json
*.sql

View 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
```

View 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()

View File

@ -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()

View File

@ -0,0 +1,3 @@
# Python packages for planning data import
psycopg2==2.8.6
requests==2.27.1

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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',

View File

@ -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';

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS planning_data;

View 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
);

View File

@ -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