colouring-montreal/app/src/frontend/map/map.tsx

210 lines
7.1 KiB
TypeScript
Raw Normal View History

import { GeoJsonObject } from 'geojson';
2019-11-07 02:39:26 -05:00
import React, { Component, Fragment } from 'react';
import { AttributionControl, GeoJSON, Map, TileLayer, ZoomControl } from 'react-leaflet-universal';
2019-02-11 04:04:19 -05:00
import 'leaflet/dist/leaflet.css';
2019-11-13 14:20:47 -05:00
import './map.css';
2020-01-02 05:59:13 -05:00
import { apiGet } from '../apiHelpers';
import { HelpIcon } from '../components/icons';
2019-11-07 02:39:26 -05:00
import { Building } from '../models/building';
2019-02-11 04:04:19 -05:00
import Legend from './legend';
import SearchBox from './search-box';
import ThemeSwitcher from './theme-switcher';
2019-11-07 02:39:26 -05:00
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
2019-09-08 20:09:05 -04:00
interface ColouringMapProps {
building?: Building;
2019-09-08 20:09:05 -04:00
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
category: string;
revision_id: number;
selectBuilding: (building: Building) => void;
colourBuilding: (building: Building) => void;
2019-09-08 20:09:05 -04:00
}
2019-09-04 10:05:41 -04:00
interface ColouringMapState {
theme: 'light' | 'night';
lat: number;
lng: number;
zoom: number;
2019-10-28 12:48:59 -04:00
boundary: GeoJsonObject;
2019-09-04 10:05:41 -04:00
}
/**
* Map area
*/
2019-10-17 12:07:34 -04:00
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
2018-09-10 07:40:25 -04:00
constructor(props) {
super(props);
this.state = {
2018-09-27 16:37:32 -04:00
theme: 'night',
2018-09-10 07:40:25 -04:00
lat: 51.5245255,
lng: -0.1338422,
2019-10-28 12:48:59 -04:00
zoom: 16,
boundary: undefined,
2018-09-10 07:40:25 -04:00
};
this.handleClick = this.handleClick.bind(this);
2019-02-11 04:04:19 -05:00
this.handleLocate = this.handleLocate.bind(this);
2018-09-13 11:03:49 -04:00
this.themeSwitch = this.themeSwitch.bind(this);
2018-09-10 07:40:25 -04:00
}
2019-02-11 04:04:19 -05:00
handleLocate(lat, lng, zoom){
this.setState({
lat: lat,
lng: lng,
zoom: zoom
2019-09-08 20:09:05 -04:00
});
2019-05-09 04:16:36 -04:00
}
handleClick(e) {
2019-09-08 20:09:05 -04:00
const mode = this.props.mode;
2020-01-02 05:59:13 -05:00
const { lat, lng } = e.latlng;
apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
.then(data => {
2018-09-30 16:54:47 -04:00
if (data && data.length){
const building = data[0];
2019-09-08 20:09:05 -04:00
if (mode === 'multi-edit') {
2019-05-09 04:16:36 -04:00
// colour building directly
this.props.colourBuilding(building);
} else if (this.props.building == undefined || building.building_id !== this.props.building.building_id){
2019-05-09 04:16:36 -04:00
this.props.selectBuilding(building);
} else {
this.props.selectBuilding(undefined);
2019-05-09 04:16:36 -04:00
}
2018-09-10 07:40:25 -04:00
} else {
if (mode !== 'multi-edit') {
// deselect but keep/return to expected colour theme
// except if in multi-edit (never select building, only colour on click)
this.props.selectBuilding(undefined);
}
2018-09-10 07:40:25 -04:00
}
2020-01-02 05:59:13 -05:00
}).catch(
2018-09-10 18:34:56 -04:00
(err) => console.error(err)
2019-11-07 03:13:30 -05:00
);
}
2018-09-13 11:03:49 -04:00
themeSwitch(e) {
e.preventDefault();
const newTheme = (this.state.theme === 'light')? 'night' : 'light';
this.setState({theme: newTheme});
}
2019-10-28 12:48:59 -04:00
async getBoundary() {
2020-01-02 05:59:13 -05:00
const data = await apiGet('/geometries/boundary-detailed.geojson') as GeoJsonObject;
2019-10-28 12:48:59 -04:00
this.setState({
boundary: data
});
}
componentDidMount() {
this.getBoundary();
}
render() {
2019-10-28 12:48:59 -04:00
const position: [number, number] = [this.state.lat, this.state.lng];
2018-09-13 18:55:53 -04:00
// baselayer
2019-09-08 20:09:05 -04:00
const key = OS_API_KEY;
const tilematrixSet = 'EPSG:3857';
2018-09-13 11:03:49 -04:00
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
2019-09-08 20:09:05 -04:00
const baseUrl = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`;
2019-10-03 09:55:54 -04:00
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
2019-09-08 20:09:05 -04:00
url={baseUrl}
attribution={attribution}
maxNativeZoom={18}
maxZoom={19}
2019-09-08 20:09:05 -04:00
/>;
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}{r}.png`;
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} maxZoom={19}/>;
2018-09-10 17:14:09 -04:00
2019-10-28 12:48:59 -04:00
const boundaryStyleFn = () => ({color: '#bbb', fill: false});
const boundaryLayer = this.state.boundary &&
2019-10-28 12:48:59 -04:00
<GeoJSON data={this.state.boundary} style={boundaryStyleFn}/>;
2018-09-13 18:55:53 -04:00
// colour-data tiles
2019-09-08 20:09:05 -04:00
const cat = this.props.category;
2019-05-27 13:26:29 -04:00
const tilesetByCat = {
age: 'date_year',
2020-01-16 09:52:10 -05:00
size: 'size_height',
construction: 'construction_core_material',
location: 'location',
2020-04-09 10:23:50 -04:00
community: 'likes',
planning: 'planning_combined',
sustainability: 'sust_dec',
type: 'building_attachment_form',
2019-12-02 10:10:45 -05:00
use: 'landuse'
2019-09-08 20:09:05 -04:00
};
2019-05-27 13:26:29 -04:00
const tileset = tilesetByCat[cat];
// pick revision id to bust browser cache
2019-05-09 04:16:36 -04:00
const rev = this.props.revision_id;
2019-09-08 20:09:05 -04:00
const dataLayer = tileset != undefined ?
2018-12-05 15:39:16 -05:00
<TileLayer
2019-05-27 13:26:29 -04:00
key={tileset}
url={`/tiles/${tileset}/{z}/{x}/{y}{r}.png?rev=${rev}`}
2019-09-08 20:09:05 -04:00
minZoom={9}
maxZoom={19}
2019-09-08 20:09:05 -04:00
/>
: null;
2018-09-10 18:34:56 -04:00
2018-09-13 18:55:53 -04:00
// highlight
2019-09-08 20:09:05 -04:00
const highlightLayer = this.props.building != undefined ?
2018-12-05 15:39:16 -05:00
<TileLayer
key={this.props.building.building_id}
url={`/tiles/highlight/{z}/{x}/{y}{r}.png?highlight=${this.props.building.geometry_id}&base=${tileset}`}
minZoom={13}
maxZoom={19}
zIndex={100}
2019-09-08 20:09:05 -04:00
/>
: null;
2018-09-10 17:14:09 -04:00
2019-09-08 20:09:05 -04:00
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
return (
2019-09-04 12:18:45 -04:00
<div className="map-container">
2018-09-13 15:35:27 -04:00
<Map
center={position}
zoom={this.state.zoom}
2019-02-24 10:29:39 -05:00
minZoom={9}
maxZoom={19}
2018-09-13 15:35:27 -04:00
doubleClickZoom={false}
zoomControl={false}
attributionControl={false}
onClick={this.handleClick}
detectRetina={true}
2019-05-27 11:39:16 -04:00
>
2019-09-08 20:09:05 -04:00
{ baseLayer }
{ buildingBaseLayer }
2019-10-28 12:48:59 -04:00
{ boundaryLayer }
2018-09-13 18:55:53 -04:00
{ dataLayer }
2018-09-13 15:35:27 -04:00
{ highlightLayer }
<ZoomControl position="topright" />
<AttributionControl prefix=""/>
2018-09-13 15:35:27 -04:00
</Map>
2019-01-19 14:06:26 -05:00
{
2019-09-08 20:09:05 -04:00
this.props.mode !== 'basic'? (
2019-05-27 11:39:16 -04:00
<Fragment>
2019-09-08 20:09:05 -04:00
{
this.props.building == undefined ?
<div className="map-notice">
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
</div>
: null
}
2019-05-27 11:39:16 -04:00
<Legend slug={cat} />
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
2019-09-08 19:44:26 -04:00
<SearchBox onLocate={this.handleLocate} />
2019-05-27 11:39:16 -04:00
</Fragment>
2019-02-11 04:04:19 -05:00
) : null
}
2019-09-04 12:18:45 -04:00
</div>
);
}
2019-05-27 11:20:00 -04:00
}
export default ColouringMap;