2019-10-29 13:34:27 -04:00
|
|
|
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
|
|
|
|
2019-11-13 14:23:22 -05:00
|
|
|
import 'leaflet/dist/leaflet.css';
|
2019-11-13 14:20:47 -05:00
|
|
|
import './map.css';
|
|
|
|
|
2019-08-14 04:21:42 -04:00
|
|
|
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
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
|
|
|
|
2019-09-08 20:09:05 -04:00
|
|
|
interface ColouringMapProps {
|
2019-11-05 15:13:10 -05:00
|
|
|
building?: Building;
|
2019-09-08 20:09:05 -04:00
|
|
|
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
|
|
|
category: string;
|
|
|
|
revision_id: number;
|
2019-11-05 15:13:10 -05:00
|
|
|
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
|
|
|
}
|
2018-09-09 17:22:44 -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;
|
|
|
|
const lat = e.latlng.lat;
|
|
|
|
const lng = e.latlng.lng;
|
2018-09-10 07:40:25 -04:00
|
|
|
fetch(
|
2019-08-14 09:05:49 -04:00
|
|
|
'/api/buildings/locate?lat='+lat+'&lng='+lng
|
2018-09-10 07:40:25 -04:00
|
|
|
).then(
|
|
|
|
(res) => res.json()
|
|
|
|
).then(function(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);
|
2019-10-02 11:47:45 -04:00
|
|
|
} 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);
|
2019-10-02 11:47:45 -04:00
|
|
|
} else {
|
|
|
|
this.props.selectBuilding(undefined);
|
2019-05-09 04:16:36 -04:00
|
|
|
}
|
2018-09-10 07:40:25 -04:00
|
|
|
} else {
|
2019-10-02 17:17:37 -04:00
|
|
|
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
|
|
|
}
|
2018-09-10 18:34:56 -04:00
|
|
|
}.bind(this)).catch(
|
|
|
|
(err) => console.error(err)
|
2019-11-07 03:13:30 -05:00
|
|
|
);
|
2018-09-09 17:22:44 -04: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() {
|
|
|
|
const res = await fetch('/geometries/boundary-detailed.geojson');
|
|
|
|
const data = await res.json() as GeoJsonObject;
|
|
|
|
this.setState({
|
|
|
|
boundary: data
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.getBoundary();
|
|
|
|
}
|
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
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>';
|
2019-10-02 17:17:37 -04:00
|
|
|
const baseLayer = <TileLayer
|
2019-09-08 20:09:05 -04:00
|
|
|
url={baseUrl}
|
|
|
|
attribution={attribution}
|
2019-10-08 12:40:30 -04:00
|
|
|
maxNativeZoom={18}
|
|
|
|
maxZoom={19}
|
2019-09-08 20:09:05 -04:00
|
|
|
/>;
|
|
|
|
|
2019-09-17 13:21:51 -04:00
|
|
|
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}{r}.png`;
|
2019-10-08 12:40:30 -04:00
|
|
|
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 &&
|
|
|
|
<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 = {
|
2018-10-05 17:20:02 -04:00
|
|
|
age: 'date_year',
|
2018-10-25 07:39:41 -04:00
|
|
|
size: 'size_storeys',
|
|
|
|
location: 'location',
|
2018-10-25 08:48:48 -04:00
|
|
|
like: 'likes',
|
2019-01-19 11:56:03 -05:00
|
|
|
planning: 'conservation_area',
|
2019-10-02 09:34:44 -04:00
|
|
|
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];
|
2018-10-25 17:23:54 -04:00
|
|
|
// 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}
|
2019-09-17 13:21:51 -04:00
|
|
|
url={`/tiles/${tileset}/{z}/{x}/{y}{r}.png?rev=${rev}`}
|
2019-09-08 20:09:05 -04:00
|
|
|
minZoom={9}
|
2019-10-08 12:40:30 -04:00
|
|
|
maxZoom={19}
|
2019-09-08 20:09:05 -04:00
|
|
|
/>
|
2018-10-05 17:20:02 -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}
|
2019-09-17 13:21:51 -04:00
|
|
|
url={`/tiles/highlight/{z}/{x}/{y}{r}.png?highlight=${this.props.building.geometry_id}&base=${tileset}`}
|
2019-10-08 12:40:30 -04:00
|
|
|
minZoom={13}
|
|
|
|
maxZoom={19}
|
2019-09-09 13:14:51 -04:00
|
|
|
zIndex={100}
|
2019-09-08 20:09:05 -04:00
|
|
|
/>
|
2018-10-05 17:20:02 -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);
|
2018-10-25 05:16:18 -04:00
|
|
|
|
2018-09-09 17:22:44 -04:00
|
|
|
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}
|
2019-10-08 12:40:30 -04:00
|
|
|
maxZoom={19}
|
2018-09-13 15:35:27 -04:00
|
|
|
doubleClickZoom={false}
|
|
|
|
zoomControl={false}
|
|
|
|
attributionControl={false}
|
|
|
|
onClick={this.handleClick}
|
2019-09-17 13:21:51 -04:00
|
|
|
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" />
|
2019-09-09 07:35:03 -04:00
|
|
|
<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>
|
2018-09-09 17:22:44 -04:00
|
|
|
);
|
|
|
|
}
|
2019-05-27 11:20:00 -04:00
|
|
|
}
|
2018-09-09 17:22:44 -04:00
|
|
|
|
|
|
|
export default ColouringMap;
|