Fix TileLayer url bug, reorganise map layers
This commit is contained in:
parent
f714a331ae
commit
0848ba5246
@ -1,4 +1,5 @@
|
|||||||
import { Category } from './categories-config';
|
import { Category } from './categories-config';
|
||||||
|
import { BuildingMapTileset } from './tileserver-config';
|
||||||
|
|
||||||
export type LegendElement = {
|
export type LegendElement = {
|
||||||
color: string;
|
color: string;
|
||||||
@ -16,7 +17,7 @@ export interface LegendConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryMapDefinition {
|
export interface CategoryMapDefinition {
|
||||||
mapStyle: string;
|
mapStyle: BuildingMapTileset;
|
||||||
legend: LegendConfig;
|
legend: LegendConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
app/src/frontend/config/map-config.ts
Normal file
7
app/src/frontend/config/map-config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const defaultMapPosition = {
|
||||||
|
lat: 51.5245255,
|
||||||
|
lng: -0.1338422,
|
||||||
|
zoom: 16
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MapTheme = 'light' | 'night';
|
19
app/src/frontend/config/tileserver-config.ts
Normal file
19
app/src/frontend/config/tileserver-config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* This file defines all the valid tileset names that can be obtained from the tilserver.
|
||||||
|
* Adjust the values here if modifying the list of styles in the tileserver.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type BuildingMapTileset = 'date_year' |
|
||||||
|
'size_height' |
|
||||||
|
'construction_core_material' |
|
||||||
|
'location' |
|
||||||
|
'likes' |
|
||||||
|
'planning_combined' |
|
||||||
|
'sust_dec' |
|
||||||
|
'building_attachment_form' |
|
||||||
|
'landuse' |
|
||||||
|
'dynamics_demolished_count';
|
||||||
|
|
||||||
|
export type SpecialMapTileset = 'base_light' | 'base_night' | 'highlight' | 'number_labels';
|
||||||
|
|
||||||
|
export type MapTileset = BuildingMapTileset | SpecialMapTileset;
|
19
app/src/frontend/map/layers/building-base-layer.tsx
Normal file
19
app/src/frontend/map/layers/building-base-layer.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { MapTheme } from '../../config/map-config';
|
||||||
|
import { MapTileset } from '../../config/tileserver-config';
|
||||||
|
|
||||||
|
import {getTileLayerUrl } from './get-tile-layer-url';
|
||||||
|
|
||||||
|
export function BuildingBaseLayer({ 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={14}
|
||||||
|
maxZoom={19}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
16
app/src/frontend/map/layers/building-data-layer.tsx
Normal file
16
app/src/frontend/map/layers/building-data-layer.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { BuildingMapTileset } from '../../config/tileserver-config';
|
||||||
|
|
||||||
|
import {getTileLayerUrl } from './get-tile-layer-url';
|
||||||
|
|
||||||
|
export function BuildingDataLayer({tileset, revisionId} : { tileset: BuildingMapTileset, revisionId: string }) {
|
||||||
|
return <TileLayer
|
||||||
|
key={`${tileset}-${revisionId}`} /* needed because TileLayer url is not mutable in react-leaflet v3 */
|
||||||
|
url={getTileLayerUrl(tileset, {rev: revisionId})}
|
||||||
|
minZoom={9}
|
||||||
|
maxZoom={19}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
16
app/src/frontend/map/layers/building-highlight-layer.tsx
Normal file
16
app/src/frontend/map/layers/building-highlight-layer.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { BuildingMapTileset } from '../../config/tileserver-config';
|
||||||
|
|
||||||
|
import { getTileLayerUrl } from './get-tile-layer-url';
|
||||||
|
|
||||||
|
export function BuildingHighlightLayer({selectedBuildingId, baseTileset}: {selectedBuildingId: number, baseTileset: BuildingMapTileset}) {
|
||||||
|
return <TileLayer
|
||||||
|
key={`${selectedBuildingId}-${baseTileset}`} /* needed because TileLayer url is not mutable in react-leaflet v3 */
|
||||||
|
url={getTileLayerUrl('highlight', {highlight: `${selectedBuildingId}`, base: baseTileset})}
|
||||||
|
minZoom={13}
|
||||||
|
maxZoom={19}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
14
app/src/frontend/map/layers/building-numbers-layer.tsx
Normal file
14
app/src/frontend/map/layers/building-numbers-layer.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import {getTileLayerUrl } from './get-tile-layer-url';
|
||||||
|
|
||||||
|
export function BuildingNumbersLayer({revisionId}: {revisionId: string}) {
|
||||||
|
return <TileLayer
|
||||||
|
key={`numbers-${revisionId}`} /* needed because TileLayer url is not mutable in react-leaflet v3 */
|
||||||
|
url={getTileLayerUrl('number_labels', {rev: revisionId})}
|
||||||
|
minZoom={17}
|
||||||
|
maxZoom={19}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
33
app/src/frontend/map/layers/city-base-map-layer.tsx
Normal file
33
app/src/frontend/map/layers/city-base-map-layer.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TileLayer } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { MapTheme } from '../../config/map-config';
|
||||||
|
|
||||||
|
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base raster layer for the map.
|
||||||
|
* @param theme map theme
|
||||||
|
*/
|
||||||
|
export function CityBaseMapLayer({theme}: {theme: MapTheme}) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordnance Survey maps - UK / London specific
|
||||||
|
* (replace with appropriate base map for other cities/countries)
|
||||||
|
*/
|
||||||
|
const key = OS_API_KEY;
|
||||||
|
const tilematrixSet = 'EPSG:3857';
|
||||||
|
const layer = theme === 'light' ? 'Light 3857' : 'Night 3857';
|
||||||
|
const baseUrl = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`;
|
||||||
|
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines. <a href=/ordnance-survey-licence.html>OS licence</a>';
|
||||||
|
|
||||||
|
return <TileLayer
|
||||||
|
key={theme} /* needed because TileLayer.key is not mutabe in react-leaflet v3 */
|
||||||
|
url={baseUrl}
|
||||||
|
attribution={attribution}
|
||||||
|
maxNativeZoom={18}
|
||||||
|
maxZoom={19}
|
||||||
|
detectRetina={true}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
17
app/src/frontend/map/layers/city-boundary-layer.tsx
Normal file
17
app/src/frontend/map/layers/city-boundary-layer.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { GeoJsonObject } from 'geojson';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
|
||||||
|
import { apiGet } from '../../apiHelpers';
|
||||||
|
|
||||||
|
export function CityBoundaryLayer() {
|
||||||
|
const [boundaryGeojson, setBoundaryGeojson] = useState<GeoJsonObject>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/geometries/boundary-detailed.geojson')
|
||||||
|
.then(data => setBoundaryGeojson(data as GeoJsonObject));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return boundaryGeojson &&
|
||||||
|
<GeoJSON data={boundaryGeojson} style={{color: '#bbb', fill: false}}/>;
|
||||||
|
}
|
14
app/src/frontend/map/layers/get-tile-layer-url.ts
Normal file
14
app/src/frontend/map/layers/get-tile-layer-url.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MapTileset } from '../../config/tileserver-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a CL tileserver URL for a tileset and a set of parameters
|
||||||
|
* @param tileset the name of the tileset
|
||||||
|
* @param parameters (optional) dictionary of parameter values
|
||||||
|
* @returns A string with the formatted URL
|
||||||
|
*/
|
||||||
|
export function getTileLayerUrl<T extends MapTileset = MapTileset>(tileset: T, parameters?: Record<string, string>) {
|
||||||
|
let paramString = parameters && new URLSearchParams(parameters).toString();
|
||||||
|
paramString = paramString == undefined ? '' : `?${paramString}`;
|
||||||
|
|
||||||
|
return `/tiles/${tileset}/{z}/{x}/{y}{r}.png${paramString}`;
|
||||||
|
}
|
@ -1,21 +1,26 @@
|
|||||||
import { GeoJsonObject } from 'geojson';
|
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { AttributionControl, GeoJSON, MapContainer, TileLayer, ZoomControl, useMapEvent } from 'react-leaflet';
|
import { AttributionControl, MapContainer, ZoomControl, useMapEvent, Pane } from 'react-leaflet';
|
||||||
|
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import './map.css';
|
import './map.css';
|
||||||
|
|
||||||
import { apiGet } from '../apiHelpers';
|
import { apiGet } from '../apiHelpers';
|
||||||
import { HelpIcon } from '../components/icons';
|
import { HelpIcon } from '../components/icons';
|
||||||
|
import { categoryMapsConfig } from '../config/category-maps-config';
|
||||||
|
import { Category } from '../config/categories-config';
|
||||||
|
import { defaultMapPosition, MapTheme } 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 { BuildingBaseLayer } from './layers/building-base-layer';
|
||||||
|
import { BuildingDataLayer } from './layers/building-data-layer';
|
||||||
|
import { BuildingNumbersLayer } from './layers/building-numbers-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 { categoryMapsConfig } from '../config/category-maps-config';
|
|
||||||
import { Category } from '../config/categories-config';
|
|
||||||
import { Building } from '../models/building';
|
|
||||||
|
|
||||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
|
||||||
|
|
||||||
interface ColouringMapProps {
|
interface ColouringMapProps {
|
||||||
selectedBuildingId: number;
|
selectedBuildingId: number;
|
||||||
@ -26,12 +31,12 @@ interface ColouringMapProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ColouringMapState {
|
interface ColouringMapState {
|
||||||
theme: 'light' | 'night';
|
theme: MapTheme;
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
boundary: GeoJsonObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map area
|
* Map area
|
||||||
*/
|
*/
|
||||||
@ -40,10 +45,7 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
theme: 'night',
|
theme: 'night',
|
||||||
lat: 51.5245255,
|
...defaultMapPosition
|
||||||
lng: -0.1338422,
|
|
||||||
zoom: 16,
|
|
||||||
boundary: undefined,
|
|
||||||
};
|
};
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleLocate = this.handleLocate.bind(this);
|
this.handleLocate = this.handleLocate.bind(this);
|
||||||
@ -73,72 +75,12 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
this.setState({theme: newTheme});
|
this.setState({theme: newTheme});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBoundary() {
|
|
||||||
const data = await apiGet('/geometries/boundary-detailed.geojson') as GeoJsonObject;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
boundary: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.getBoundary();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const categoryMapDefinition = categoryMapsConfig[this.props.category];
|
const categoryMapDefinition = categoryMapsConfig[this.props.category];
|
||||||
|
|
||||||
const position: [number, number] = [this.state.lat, this.state.lng];
|
const position: [number, number] = [this.state.lat, this.state.lng];
|
||||||
|
|
||||||
// baselayer
|
|
||||||
const key = OS_API_KEY;
|
|
||||||
const tilematrixSet = 'EPSG:3857';
|
|
||||||
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
|
|
||||||
const baseUrl = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`;
|
|
||||||
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines. <a href=/ordnance-survey-licence.html>OS licence</a>';
|
|
||||||
const baseLayer = <TileLayer
|
|
||||||
url={baseUrl}
|
|
||||||
attribution={attribution}
|
|
||||||
maxNativeZoom={18}
|
|
||||||
maxZoom={19}
|
|
||||||
detectRetina={true}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}{r}.png`;
|
|
||||||
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} maxZoom={19}/>;
|
|
||||||
|
|
||||||
const boundaryLayer = this.state.boundary &&
|
|
||||||
<GeoJSON data={this.state.boundary} style={{color: '#bbb', fill: false}}/>;
|
|
||||||
|
|
||||||
const tileset = categoryMapDefinition.mapStyle;
|
const tileset = categoryMapDefinition.mapStyle;
|
||||||
const dataLayer = tileset != undefined &&
|
|
||||||
<TileLayer
|
|
||||||
key={tileset}
|
|
||||||
url={`/tiles/${tileset}/{z}/{x}/{y}{r}.png?rev=${this.props.revisionId}`}
|
|
||||||
minZoom={9}
|
|
||||||
maxZoom={19}
|
|
||||||
detectRetina={true}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
// highlight
|
|
||||||
const highlightLayer = this.props.selectedBuildingId != undefined &&
|
|
||||||
<TileLayer
|
|
||||||
key={this.props.selectedBuildingId + tileset}
|
|
||||||
url={`/tiles/highlight/{z}/{x}/{y}{r}.png?highlight=${this.props.selectedBuildingId}&base=${tileset}`}
|
|
||||||
minZoom={13}
|
|
||||||
maxZoom={19}
|
|
||||||
zIndex={100}
|
|
||||||
detectRetina={true}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
const numbersLayer = <TileLayer
|
|
||||||
key={this.state.theme}
|
|
||||||
url={`/tiles/number_labels/{z}/{x}/{y}{r}.png?rev=${this.props.revisionId}`}
|
|
||||||
zIndex={200}
|
|
||||||
minZoom={17}
|
|
||||||
maxZoom={19}
|
|
||||||
detectRetina={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const hasSelection = this.props.selectedBuildingId != undefined;
|
const hasSelection = this.props.selectedBuildingId != undefined;
|
||||||
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
||||||
@ -155,12 +97,39 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
>
|
>
|
||||||
<ClickHandler onClick={this.handleClick} />
|
<ClickHandler onClick={this.handleClick} />
|
||||||
{ baseLayer }
|
|
||||||
{ buildingBaseLayer }
|
<Pane
|
||||||
{ boundaryLayer }
|
key={this.state.theme}
|
||||||
{ dataLayer }
|
name={'cc-base-pane'}
|
||||||
{ highlightLayer }
|
style={{zIndex: 50}}
|
||||||
{ numbersLayer }
|
>
|
||||||
|
<CityBaseMapLayer theme={this.state.theme} />
|
||||||
|
<BuildingBaseLayer theme={this.state.theme} />
|
||||||
|
</Pane>
|
||||||
|
|
||||||
|
{
|
||||||
|
tileset &&
|
||||||
|
<BuildingDataLayer
|
||||||
|
tileset={tileset}
|
||||||
|
revisionId={this.props.revisionId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Pane
|
||||||
|
name='cc-overlay-pane'
|
||||||
|
style={{zIndex: 300}}
|
||||||
|
>
|
||||||
|
<CityBoundaryLayer />
|
||||||
|
<BuildingNumbersLayer revisionId={this.props.revisionId} />
|
||||||
|
{
|
||||||
|
this.props.selectedBuildingId &&
|
||||||
|
<BuildingHighlightLayer
|
||||||
|
selectedBuildingId={this.props.selectedBuildingId}
|
||||||
|
baseTileset={tileset}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Pane>
|
||||||
|
|
||||||
<ZoomControl position="topright" />
|
<ZoomControl position="topright" />
|
||||||
<AttributionControl prefix=""/>
|
<AttributionControl prefix=""/>
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
Loading…
Reference in New Issue
Block a user