Rewrite map with hooks, add map colour toggle
Map and Legend components rewritten using hooks. Also, each category can now have multiple available colour scales. These can be switched using a select dropdown in the legend.
This commit is contained in:
parent
d3a17f2e5f
commit
a4d1afab81
@ -446,6 +446,68 @@
|
|||||||
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
||||||
</Rule>
|
</Rule>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style name="community_local_significance_total">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] >= 100</Filter>
|
||||||
|
<PolygonSymbolizer fill="#bd0026" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] >= 50 and [community_local_significance_total] < 100</Filter>
|
||||||
|
<PolygonSymbolizer fill="#e31a1c" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] >= 20 and [community_local_significance_total] < 50</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fc4e2a" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] >= 10 and [community_local_significance_total] < 20</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fd8d3c" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] >= 3 and [community_local_significance_total] < 10</Filter>
|
||||||
|
<PolygonSymbolizer fill="#feb24c" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] = 2</Filter>
|
||||||
|
<PolygonSymbolizer fill="#fed976" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[community_local_significance_total] = 1</Filter>
|
||||||
|
<PolygonSymbolizer fill="#ffe8a9" />
|
||||||
|
</Rule>
|
||||||
|
|
||||||
|
<Rule>
|
||||||
|
<MaxScaleDenominator>17061</MaxScaleDenominator>
|
||||||
|
<MinScaleDenominator>4264</MinScaleDenominator>
|
||||||
|
<LineSymbolizer stroke="#888" stroke-width="1.0"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<MaxScaleDenominator>4264</MaxScaleDenominator>
|
||||||
|
<MinScaleDenominator>0</MinScaleDenominator>
|
||||||
|
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
|
<Style name="community_in_public_ownership">
|
||||||
|
<Rule>
|
||||||
|
<Filter>[in_public_ownership] = true</Filter>
|
||||||
|
<PolygonSymbolizer fill="#1166ff" />
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<Filter>[in_public_ownership] = false</Filter>
|
||||||
|
<PolygonSymbolizer fill="#ffaaa0" />
|
||||||
|
</Rule>
|
||||||
|
|
||||||
|
<Rule>
|
||||||
|
<MaxScaleDenominator>17061</MaxScaleDenominator>
|
||||||
|
<MinScaleDenominator>4264</MinScaleDenominator>
|
||||||
|
<LineSymbolizer stroke="#888" stroke-width="1.0"/>
|
||||||
|
</Rule>
|
||||||
|
<Rule>
|
||||||
|
<MaxScaleDenominator>4264</MaxScaleDenominator>
|
||||||
|
<MinScaleDenominator>0</MinScaleDenominator>
|
||||||
|
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
||||||
|
</Rule>
|
||||||
|
</Style>
|
||||||
<Style name="landuse">
|
<Style name="landuse">
|
||||||
<Rule>
|
<Rule>
|
||||||
<Filter>[current_landuse_order] = "Agriculture And Fisheries"</Filter>
|
<Filter>[current_landuse_order] = "Agriculture And Fisheries"</Filter>
|
||||||
|
@ -23,8 +23,8 @@ export interface CategoryMapDefinition {
|
|||||||
|
|
||||||
export const defaultMapCategory = Category.Age;
|
export const defaultMapCategory = Category.Age;
|
||||||
|
|
||||||
export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} = {
|
||||||
[Category.Age]: {
|
[Category.Age]: [{
|
||||||
mapStyle: 'date_year',
|
mapStyle: 'date_year',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Age',
|
title: 'Age',
|
||||||
@ -46,8 +46,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: '#d0c291', text: '<1700' },
|
{ color: '#d0c291', text: '<1700' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Size]: {
|
[Category.Size]: [{
|
||||||
mapStyle: 'size_height',
|
mapStyle: 'size_height',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Height to apex',
|
title: 'Height to apex',
|
||||||
@ -62,15 +62,15 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: '#980043', text: '≥152'}
|
{ color: '#980043', text: '≥152'}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Team]: {
|
[Category.Team]: [{
|
||||||
mapStyle: undefined,
|
mapStyle: undefined,
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Team',
|
title: 'Team',
|
||||||
elements: []
|
elements: []
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Construction]: {
|
[Category.Construction]: [{
|
||||||
mapStyle: 'construction_core_material',
|
mapStyle: 'construction_core_material',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Construction',
|
title: 'Construction',
|
||||||
@ -85,8 +85,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: "#c48a85", text: "Other Man-Made Material" }
|
{ color: "#c48a85", text: "Other Man-Made Material" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Location]: {
|
[Category.Location]: [{
|
||||||
mapStyle: 'location',
|
mapStyle: 'location',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Location',
|
title: 'Location',
|
||||||
@ -99,23 +99,52 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: '#bae4bc', text: '<20%' }
|
{ color: '#bae4bc', text: '<20%' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Community]: {
|
[Category.Community]: [
|
||||||
mapStyle: 'likes',
|
{
|
||||||
legend: {
|
mapStyle: 'likes',
|
||||||
title: 'Like Me',
|
legend: {
|
||||||
elements: [
|
title: 'Like Me',
|
||||||
{ color: '#bd0026', text: '👍👍👍👍 100+' },
|
elements: [
|
||||||
{ color: '#e31a1c', text: '👍👍👍 50–99' },
|
{ color: '#bd0026', text: '👍👍👍👍 100+' },
|
||||||
{ color: '#fc4e2a', text: '👍👍 20–49' },
|
{ color: '#e31a1c', text: '👍👍👍 50–99' },
|
||||||
{ color: '#fd8d3c', text: '👍👍 10–19' },
|
{ color: '#fc4e2a', text: '👍👍 20–49' },
|
||||||
{ color: '#feb24c', text: '👍 3–9' },
|
{ color: '#fd8d3c', text: '👍👍 10–19' },
|
||||||
{ color: '#fed976', text: '👍 2' },
|
{ color: '#feb24c', text: '👍 3–9' },
|
||||||
{ color: '#ffe8a9', text: '👍 1'}
|
{ color: '#fed976', text: '👍 2' },
|
||||||
]
|
{ color: '#ffe8a9', text: '👍 1'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'community_local_significance_total',
|
||||||
|
legend: {
|
||||||
|
title: 'Local Significance',
|
||||||
|
description: 'People who think the building should be locally listed',
|
||||||
|
elements: [
|
||||||
|
{ color: '#bd0026', text: '100+' },
|
||||||
|
{ color: '#e31a1c', text: '50–99' },
|
||||||
|
{ color: '#fc4e2a', text: '20–49' },
|
||||||
|
{ color: '#fd8d3c', text: '10–19' },
|
||||||
|
{ color: '#feb24c', text: '3–9' },
|
||||||
|
{ color: '#fed976', text: '2' },
|
||||||
|
{ color: '#ffe8a9', text: '1'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mapStyle: 'community_in_public_ownership',
|
||||||
|
legend: {
|
||||||
|
title: 'Public Ownership',
|
||||||
|
description: 'Is the building in some form of public/community ownership',
|
||||||
|
elements: [
|
||||||
|
{color: '#1166ff', text: 'Yes'},
|
||||||
|
{color: '#ffaaa0', text: 'No'}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
[Category.Planning]: {
|
[Category.Planning]: [{
|
||||||
mapStyle: 'planning_combined',
|
mapStyle: 'planning_combined',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Statutory protections',
|
title: 'Statutory protections',
|
||||||
@ -128,8 +157,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: '#858ed4', text: 'Locally listed'},
|
{ color: '#858ed4', text: 'Locally listed'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Sustainability]: {
|
[Category.Sustainability]: [{
|
||||||
mapStyle: 'sust_dec',
|
mapStyle: 'sust_dec',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Sustainability',
|
title: 'Sustainability',
|
||||||
@ -144,8 +173,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: "#e31d23", text: 'G' },
|
{ color: "#e31d23", text: 'G' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Type]: {
|
[Category.Type]: [{
|
||||||
mapStyle: 'building_attachment_form',
|
mapStyle: 'building_attachment_form',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Type',
|
title: 'Type',
|
||||||
@ -156,8 +185,8 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: "#226291", text: "Mid-Terrace" }
|
{ color: "#226291", text: "Mid-Terrace" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.LandUse]: {
|
[Category.LandUse]: [{
|
||||||
mapStyle: 'landuse',
|
mapStyle: 'landuse',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Land Use',
|
title: 'Land Use',
|
||||||
@ -177,15 +206,15 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
{ color: '#ffffff', text: 'Vacant & Derelict' }
|
{ color: '#ffffff', text: 'Vacant & Derelict' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Streetscape]: {
|
[Category.Streetscape]: [{
|
||||||
mapStyle: undefined,
|
mapStyle: undefined,
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Streetscape',
|
title: 'Streetscape',
|
||||||
elements: []
|
elements: []
|
||||||
},
|
},
|
||||||
},
|
}],
|
||||||
[Category.Dynamics]: {
|
[Category.Dynamics]: [{
|
||||||
mapStyle: 'dynamics_demolished_count',
|
mapStyle: 'dynamics_demolished_count',
|
||||||
legend: {
|
legend: {
|
||||||
title: 'Dynamics',
|
title: 'Dynamics',
|
||||||
@ -218,6 +247,6 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,8 @@ export type BuildingMapTileset = 'date_year' |
|
|||||||
'construction_core_material' |
|
'construction_core_material' |
|
||||||
'location' |
|
'location' |
|
||||||
'likes' |
|
'likes' |
|
||||||
|
'community_local_significance_total' |
|
||||||
|
'community_in_public_ownership' |
|
||||||
'planning_combined' |
|
'planning_combined' |
|
||||||
'sust_dec' |
|
'sust_dec' |
|
||||||
'building_attachment_form' |
|
'building_attachment_form' |
|
||||||
|
@ -35,7 +35,7 @@ import { sendBuildingUpdate } from './api-data/building-update';
|
|||||||
* to all modules that import leaflet or react-leaflet.
|
* to all modules that import leaflet or react-leaflet.
|
||||||
*/
|
*/
|
||||||
const ColouringMap = loadable(
|
const ColouringMap = loadable(
|
||||||
() => import('./map/map'),
|
async () => (await import('./map/map')).ColouringMap,
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -56,6 +56,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-legend .style-select {
|
||||||
|
background-color: inherit;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
width: auto;
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.map-legend .h4,
|
.map-legend .h4,
|
||||||
.map-legend p,
|
.map-legend p,
|
||||||
.data-legend {
|
.data-legend {
|
||||||
|
@ -1,116 +1,115 @@
|
|||||||
import React from 'react';
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import './legend.css';
|
import './legend.css';
|
||||||
|
|
||||||
import { DownIcon, UpIcon } from '../components/icons';
|
import { DownIcon, UpIcon } from '../components/icons';
|
||||||
import { Logo } from '../components/logo';
|
import { Logo } from '../components/logo';
|
||||||
import { LegendConfig } from '../config/category-maps-config';
|
import { CategoryMapDefinition, LegendConfig } from '../config/category-maps-config';
|
||||||
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
|
|
||||||
interface LegendProps {
|
interface LegendProps {
|
||||||
legendConfig: LegendConfig;
|
mapColourScaleDefinitions: CategoryMapDefinition[];
|
||||||
|
mapColourScale: BuildingMapTileset;
|
||||||
|
onMapColourScale: (x: BuildingMapTileset) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegendState {
|
export const Legend : FC<LegendProps> = ({
|
||||||
collapseList: boolean;
|
mapColourScaleDefinitions,
|
||||||
}
|
mapColourScale,
|
||||||
|
onMapColourScale
|
||||||
|
}) => {
|
||||||
|
const [collapseList, setCollapseList] = useState(false);
|
||||||
|
|
||||||
class Legend extends React.Component<LegendProps, LegendState> {
|
const handleToggle = useCallback(() => {
|
||||||
constructor(props) {
|
setCollapseList(!collapseList);
|
||||||
super(props);
|
}, [collapseList]);
|
||||||
this.state = {collapseList: false};
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.onResize= this.onResize.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const onResize = useCallback(({target}) => {
|
||||||
|
setCollapseList((target.outerHeight < 670 || target.outerWidth < 768))
|
||||||
|
}, []);
|
||||||
|
|
||||||
handleClick() {
|
useEffect(() => {
|
||||||
this.setState(state => ({
|
window.addEventListener('resize', onResize);
|
||||||
collapseList: !state.collapseList
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(window?.outerHeight) {
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('resize', this.onResize);
|
|
||||||
if (window && window.outerHeight) {
|
|
||||||
// if we're in the browser, pass in as though from event to initialise
|
// if we're in the browser, pass in as though from event to initialise
|
||||||
this.onResize({target: window});
|
onResize({target: window});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
}
|
||||||
|
}, [onResize]);
|
||||||
|
|
||||||
|
const legendConfig = mapColourScaleDefinitions.find(def => def.mapStyle === mapColourScale)?.legend;
|
||||||
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
const {
|
||||||
window.removeEventListener('resize', this.onResize);
|
title = undefined,
|
||||||
}
|
elements = [],
|
||||||
|
description = undefined,
|
||||||
|
disclaimer = undefined
|
||||||
|
} = legendConfig ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
onResize(e) {
|
<div className="map-legend">
|
||||||
this.setState({collapseList: (e.target.outerHeight < 670 || e.target.outerWidth < 768)}); // magic number needs to be consistent with CSS expander-button media query
|
<Logo variant="default" />
|
||||||
}
|
{
|
||||||
|
mapColourScaleDefinitions.length > 1 ?
|
||||||
render() {
|
<select className='style-select' onChange={e => onMapColourScale(e.target.value as BuildingMapTileset)}>
|
||||||
const {
|
{
|
||||||
title = undefined,
|
mapColourScaleDefinitions.map(def =>
|
||||||
elements = [],
|
<option key={def.mapStyle} value={def.mapStyle}>{def.legend.title}</option>
|
||||||
description = undefined,
|
)
|
||||||
disclaimer = undefined
|
}
|
||||||
} = this.props.legendConfig ?? {};
|
</select> :
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="map-legend">
|
|
||||||
<Logo variant="default" />
|
|
||||||
{
|
|
||||||
title && <h4 className="h4">{title}</h4>
|
title && <h4 className="h4">{title}</h4>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.length > 0 &&
|
elements.length > 0 &&
|
||||||
<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={this.handleClick} >
|
<button className="expander-button btn btn-outline-secondary btn-sm" type="button" onClick={handleToggle} >
|
||||||
{
|
{
|
||||||
this.state.collapseList ?
|
collapseList ?
|
||||||
<UpIcon /> :
|
<UpIcon /> :
|
||||||
<DownIcon />
|
<DownIcon />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
description && <p>{description}</p>
|
description && <p>{description}</p>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.length === 0 ?
|
elements.length === 0 ?
|
||||||
<p className="data-intro">Coming soon…</p> :
|
<p className="data-intro">Coming soon…</p> :
|
||||||
<ul className={this.state.collapseList ? 'collapse data-legend' : 'data-legend'} >
|
<ul className={collapseList ? 'collapse data-legend' : 'data-legend'} >
|
||||||
{
|
{
|
||||||
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
disclaimer && <p className='legend-disclaimer'>{disclaimer}</p>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
elements.map((item) => {
|
elements.map((item) => {
|
||||||
let key: string,
|
let key: string,
|
||||||
content: React.ReactElement;
|
content: React.ReactElement;
|
||||||
|
|
||||||
if('subtitle' in item) {
|
|
||||||
key = item.subtitle;
|
|
||||||
content = <h6>{item.subtitle}</h6>;
|
|
||||||
} else {
|
|
||||||
key = `${item.text}-${item.color}`;
|
|
||||||
content = <>
|
|
||||||
<div className="key" style={ { background: item.color, border: item.border } } />
|
|
||||||
{ item.text }
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<li key={key}>
|
|
||||||
{content}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if('subtitle' in item) {
|
||||||
|
key = item.subtitle;
|
||||||
|
content = <h6>{item.subtitle}</h6>;
|
||||||
|
} else {
|
||||||
|
key = `${item.text}-${item.color}`;
|
||||||
|
content = <>
|
||||||
|
<div className="key" style={ { background: item.color, border: item.border } } />
|
||||||
|
{ item.text }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
{content}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Legend;
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment, useEffect } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { AttributionControl, MapContainer, ZoomControl, useMapEvent, Pane, useMap } from 'react-leaflet';
|
import { AttributionControl, MapContainer, ZoomControl, useMapEvent, Pane, useMap } from 'react-leaflet';
|
||||||
|
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
@ -18,9 +18,10 @@ import { BuildingDataLayer } from './layers/building-data-layer';
|
|||||||
import { BuildingNumbersLayer } from './layers/building-numbers-layer';
|
import { BuildingNumbersLayer } from './layers/building-numbers-layer';
|
||||||
import { BuildingHighlightLayer } from './layers/building-highlight-layer';
|
import { BuildingHighlightLayer } from './layers/building-highlight-layer';
|
||||||
|
|
||||||
import Legend from './legend';
|
import { Legend } from './legend';
|
||||||
import SearchBox from './search-box';
|
import SearchBox from './search-box';
|
||||||
import ThemeSwitcher from './theme-switcher';
|
import ThemeSwitcher from './theme-switcher';
|
||||||
|
import { BuildingMapTileset } from '../config/tileserver-config';
|
||||||
|
|
||||||
interface ColouringMapProps {
|
interface ColouringMapProps {
|
||||||
selectedBuildingId: number;
|
selectedBuildingId: number;
|
||||||
@ -30,124 +31,125 @@ interface ColouringMapProps {
|
|||||||
onBuildingAction: (building: Building) => void;
|
onBuildingAction: (building: Building) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColouringMapState {
|
export const ColouringMap : FC<ColouringMapProps> = ({
|
||||||
theme: MapTheme;
|
category,
|
||||||
position: [number, number];
|
mode,
|
||||||
zoom: number;
|
revisionId,
|
||||||
}
|
onBuildingAction,
|
||||||
|
selectedBuildingId,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
|
||||||
/**
|
const [theme, setTheme] = useState<MapTheme>('light');
|
||||||
* Map area
|
const [position, setPosition] = useState(initialMapViewport.position);
|
||||||
*/
|
const [zoom, setZoom] = useState(initialMapViewport.zoom);
|
||||||
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
theme: 'light',
|
|
||||||
...initialMapViewport
|
|
||||||
};
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
this.handleLocate = this.handleLocate.bind(this);
|
|
||||||
this.themeSwitch = this.themeSwitch.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLocate(lat: number, lng: number, zoom: number){
|
const [mapColourScale, setMapColourScale] = useState<BuildingMapTileset>();
|
||||||
this.setState({
|
|
||||||
position: [lat, lng],
|
|
||||||
zoom: zoom
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(e) {
|
const handleLocate = useCallback(
|
||||||
const { lat, lng } = e.latlng;
|
(lat: number, lng: number, zoom: number) => {
|
||||||
apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
|
setPosition([lat, lng]);
|
||||||
.then(data => {
|
setZoom(zoom);
|
||||||
const building = data?.[0];
|
},
|
||||||
this.props.onBuildingAction(building);
|
[]
|
||||||
}).catch(err => console.error(err));
|
);
|
||||||
}
|
|
||||||
|
|
||||||
themeSwitch(e) {
|
const handleClick = useCallback(
|
||||||
e.preventDefault();
|
async (e) => {
|
||||||
const newTheme = (this.state.theme === 'light')? 'night' : 'light';
|
const {lat, lng} = e.latlng;
|
||||||
this.setState({theme: newTheme});
|
const data = await apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`);
|
||||||
}
|
const building = data?.[0];
|
||||||
|
onBuildingAction(building);
|
||||||
|
},
|
||||||
|
[onBuildingAction],
|
||||||
|
)
|
||||||
|
|
||||||
render() {
|
const themeSwitch = useCallback(
|
||||||
const categoryMapDefinition = categoryMapsConfig[this.props.category];
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const newTheme = (theme === 'light')? 'night' : 'light';
|
||||||
|
setTheme(newTheme);
|
||||||
|
},
|
||||||
|
[theme],
|
||||||
|
)
|
||||||
|
|
||||||
const tileset = categoryMapDefinition.mapStyle;
|
const categoryMapDefinitions = useMemo(() => categoryMapsConfig[category], [category]);
|
||||||
|
|
||||||
const hasSelection = this.props.selectedBuildingId != undefined;
|
useEffect(() => {
|
||||||
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
if(!categoryMapDefinitions.some(def => def.mapStyle === mapColourScale)) {
|
||||||
|
setMapColourScale(categoryMapDefinitions[0].mapStyle);
|
||||||
|
}
|
||||||
|
}, [categoryMapDefinitions, mapColourScale]);
|
||||||
|
|
||||||
return (
|
const hasSelection = selectedBuildingId != undefined;
|
||||||
<div className="map-container">
|
const isEdit = ['edit', 'multi-edit'].includes(mode);
|
||||||
<MapContainer
|
|
||||||
center={initialMapViewport.position}
|
return (
|
||||||
zoom={initialMapViewport.zoom}
|
<div className="map-container">
|
||||||
minZoom={9}
|
<MapContainer
|
||||||
maxZoom={18}
|
center={initialMapViewport.position}
|
||||||
doubleClickZoom={false}
|
zoom={initialMapViewport.zoom}
|
||||||
zoomControl={false}
|
minZoom={9}
|
||||||
attributionControl={false}
|
maxZoom={18}
|
||||||
|
doubleClickZoom={false}
|
||||||
|
zoomControl={false}
|
||||||
|
attributionControl={false}
|
||||||
|
>
|
||||||
|
<ClickHandler onClick={handleClick} />
|
||||||
|
<MapBackgroundColor theme={theme} />
|
||||||
|
<MapViewport position={position} zoom={zoom} />
|
||||||
|
|
||||||
|
<Pane
|
||||||
|
key={theme}
|
||||||
|
name={'cc-base-pane'}
|
||||||
|
style={{zIndex: 50}}
|
||||||
>
|
>
|
||||||
<ClickHandler onClick={this.handleClick} />
|
<CityBaseMapLayer theme={theme} />
|
||||||
<MapBackgroundColor theme={this.state.theme} />
|
<BuildingBaseLayer theme={theme} />
|
||||||
<MapViewport position={this.state.position} zoom={this.state.zoom} />
|
</Pane>
|
||||||
|
|
||||||
<Pane
|
{
|
||||||
key={this.state.theme}
|
mapColourScale &&
|
||||||
name={'cc-base-pane'}
|
<BuildingDataLayer
|
||||||
style={{zIndex: 50}}
|
tileset={mapColourScale}
|
||||||
>
|
revisionId={revisionId}
|
||||||
<CityBaseMapLayer theme={this.state.theme} />
|
/>
|
||||||
<BuildingBaseLayer theme={this.state.theme} />
|
}
|
||||||
</Pane>
|
|
||||||
|
|
||||||
|
<Pane
|
||||||
|
name='cc-overlay-pane'
|
||||||
|
style={{zIndex: 300}}
|
||||||
|
>
|
||||||
|
<CityBoundaryLayer />
|
||||||
|
<BuildingNumbersLayer revisionId={revisionId} />
|
||||||
{
|
{
|
||||||
tileset &&
|
selectedBuildingId &&
|
||||||
<BuildingDataLayer
|
<BuildingHighlightLayer
|
||||||
tileset={tileset}
|
selectedBuildingId={selectedBuildingId}
|
||||||
revisionId={this.props.revisionId}
|
baseTileset={mapColourScale}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
</Pane>
|
||||||
|
|
||||||
<Pane
|
<ZoomControl position="topright" />
|
||||||
name='cc-overlay-pane'
|
<AttributionControl prefix=""/>
|
||||||
style={{zIndex: 300}}
|
</MapContainer>
|
||||||
>
|
{
|
||||||
<CityBoundaryLayer />
|
mode !== 'basic' &&
|
||||||
<BuildingNumbersLayer revisionId={this.props.revisionId} />
|
<>
|
||||||
{
|
{
|
||||||
this.props.selectedBuildingId &&
|
!hasSelection &&
|
||||||
<BuildingHighlightLayer
|
<div className="map-notice">
|
||||||
selectedBuildingId={this.props.selectedBuildingId}
|
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||||
baseTileset={tileset}
|
</div>
|
||||||
/>
|
}
|
||||||
}
|
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={setMapColourScale}/>
|
||||||
</Pane>
|
{/* <ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={theme} /> */}
|
||||||
|
<SearchBox onLocate={handleLocate} />
|
||||||
<ZoomControl position="topright" />
|
</>
|
||||||
<AttributionControl prefix=""/>
|
}
|
||||||
</MapContainer>
|
</div>
|
||||||
{
|
);
|
||||||
this.props.mode !== 'basic' &&
|
|
||||||
<Fragment>
|
|
||||||
{
|
|
||||||
!hasSelection &&
|
|
||||||
<div className="map-notice">
|
|
||||||
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Legend legendConfig={categoryMapDefinition?.legend} />
|
|
||||||
{/* <ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} /> */}
|
|
||||||
<SearchBox onLocate={this.handleLocate} />
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClickHandler({ onClick }: {onClick: (e) => void}) {
|
function ClickHandler({ onClick }: {onClick: (e) => void}) {
|
||||||
@ -180,5 +182,3 @@ function MapViewport({
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColouringMap;
|
|
||||||
|
@ -84,6 +84,27 @@ const LAYER_QUERIES = {
|
|||||||
buildings
|
buildings
|
||||||
WHERE
|
WHERE
|
||||||
likes_total > 0`,
|
likes_total > 0`,
|
||||||
|
community_local_significance_total: `
|
||||||
|
SELECT
|
||||||
|
geometry_id,
|
||||||
|
community_local_significance_total
|
||||||
|
FROM
|
||||||
|
buildings
|
||||||
|
WHERE
|
||||||
|
community_local_significance_total > 0
|
||||||
|
`,
|
||||||
|
community_in_public_ownership: `
|
||||||
|
SELECT
|
||||||
|
geometry_id,
|
||||||
|
CASE
|
||||||
|
WHEN community_public_ownership = 'Not in public/community ownership' THEN false
|
||||||
|
ELSE true
|
||||||
|
END AS in_public_ownership
|
||||||
|
FROM
|
||||||
|
buildings
|
||||||
|
WHERE
|
||||||
|
community_public_ownership IS NOT NULL
|
||||||
|
`,
|
||||||
planning_combined: `
|
planning_combined: `
|
||||||
SELECT
|
SELECT
|
||||||
geometry_id,
|
geometry_id,
|
||||||
|
Loading…
Reference in New Issue
Block a user