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

255 lines
9.1 KiB
TypeScript
Raw Normal View History

import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { AttributionControl, MapContainer, ZoomControl, useMapEvent, Pane, useMap } from 'react-leaflet';
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';
import { categoryMapsConfig } from '../config/category-maps-config';
import { Category } from '../config/categories-config';
import { initialMapViewport, mapBackgroundColor, MapTheme, BoroughEnablementState, ParcelEnablementState, FloodEnablementState, ConservationAreasEnablementState, HistoricDataEnablementState } from '../config/map-config';
import { Building } from '../models/building';
2019-11-07 02:39:26 -05:00
import { CityBaseMapLayer } from './layers/city-base-map-layer';
import { CityBoundaryLayer } from './layers/city-boundary-layer';
import { BoroughBoundaryLayer } from './layers/borough-boundary-layer';
2022-10-12 07:13:59 -04:00
import { ParcelBoundaryLayer } from './layers/parcel-boundary-layer';
import { HistoricDataLayer } from './layers/historic-data-layer';
2022-11-08 08:40:37 -05:00
import { FloodBoundaryLayer } from './layers/flood-boundary-layer';
2022-11-08 15:47:01 -05:00
import { ConservationAreaBoundaryLayer } from './layers/conservation-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 SearchBox from './search-box';
import ThemeSwitcher from './theme-switcher';
import BoroughSwitcher from './borough-switcher';
2022-10-12 07:13:59 -04:00
import ParcelSwitcher from './parcel-switcher';
2022-11-08 08:40:37 -05:00
import FloodSwitcher from './flood-switcher';
2022-11-08 15:47:01 -05:00
import ConservationAreaSwitcher from './conservation-switcher';
import HistoricDataSwitcher from './historic-data-switcher';
import { BuildingMapTileset } from '../config/tileserver-config';
2019-09-08 20:09:05 -04:00
interface ColouringMapProps {
selectedBuildingId: number;
2019-09-08 20:09:05 -04:00
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
category: Category;
revisionId: string;
onBuildingAction: (building: Building) => void;
2019-09-08 20:09:05 -04:00
}
export const ColouringMap : FC<ColouringMapProps> = ({
category,
mode,
revisionId,
onBuildingAction,
selectedBuildingId,
children
}) => {
2021-12-09 11:39:00 -05:00
const [theme, setTheme] = useState<MapTheme>('night');
const [borough, setBorough] = useState<BoroughEnablementState>('disabled');
2022-10-12 07:13:59 -04:00
const [parcel, setParcel] = useState<ParcelEnablementState>('disabled');
2022-11-08 08:40:37 -05:00
const [flood, setFlood] = useState<FloodEnablementState>('disabled');
2022-11-08 15:47:01 -05:00
const [conservation, setConservation] = useState<ConservationAreasEnablementState>('disabled');
const [historicData, setHistoricData] = useState<HistoricDataEnablementState>('disabled');
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) => {
setPosition([lat, lng]);
setZoom(zoom);
},
[]
);
const handleClick = useCallback(
async (e) => {
const {lat, lng} = e.latlng;
const data = await apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`);
const building = data?.[0];
onBuildingAction(building);
},
[onBuildingAction],
)
const themeSwitch = useCallback(
(e) => {
e.preventDefault();
const newTheme = (theme === 'light')? 'night' : 'light';
setTheme(newTheme);
},
[theme],
)
const boroughSwitch = useCallback(
(e) => {
e.preventDefault();
const newBorough = (borough === 'enabled')? 'disabled' : 'enabled';
setBorough(newBorough);
},
[borough],
)
2022-10-12 07:13:59 -04:00
const parcelSwitch = useCallback(
(e) => {
e.preventDefault();
const newParcel = (parcel === 'enabled')? 'disabled' : 'enabled';
setParcel(newParcel);
},
[parcel],
)
2022-11-08 08:40:37 -05:00
const floodSwitch = useCallback(
(e) => {
e.preventDefault();
const newFlood = (flood === 'enabled')? 'disabled' : 'enabled';
setFlood(newFlood);
},
[flood],
)
2022-11-08 15:47:01 -05:00
const conservationSwitch = useCallback(
(e) => {
e.preventDefault();
const newConservation = (conservation === 'enabled')? 'disabled' : 'enabled';
setConservation(newConservation);
},
[conservation],
)
const historicDataSwitch = useCallback(
(e) => {
e.preventDefault();
const newHistoric = (historicData === 'enabled')? 'disabled' : 'enabled';
setHistoricData(newHistoric);
},
[historicData],
)
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
center={initialMapViewport.position}
zoom={initialMapViewport.zoom}
minZoom={9}
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}}
2019-05-27 11:39:16 -04:00
>
<CityBaseMapLayer theme={theme} />
<BuildingBaseLayer theme={theme} />
</Pane>
{
mapColourScale &&
<BuildingDataLayer
tileset={mapColourScale}
revisionId={revisionId}
/>
}
<Pane
name='cc-overlay-pane'
style={{zIndex: 300}}
>
<CityBoundaryLayer />
<HistoricDataLayer enablement={historicData}/>
<BoroughBoundaryLayer enablement={borough}/>
2022-10-12 07:13:59 -04:00
<ParcelBoundaryLayer enablement={parcel}/>
2022-11-08 08:40:37 -05:00
<FloodBoundaryLayer enablement={flood}/>
2022-11-08 15:47:01 -05:00
<ConservationAreaBoundaryLayer enablement={conservation}/>
<BuildingNumbersLayer revisionId={revisionId} />
{
selectedBuildingId &&
<BuildingHighlightLayer
selectedBuildingId={selectedBuildingId}
baseTileset={mapColourScale}
2021-12-03 14:32:54 -05:00
/>
}
</Pane>
<ZoomControl position="topright" />
<AttributionControl prefix=""/>
</MapContainer>
{
mode !== 'basic' &&
<>
{
!hasSelection &&
<div className="map-notice">
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
</div>
}
<Legend mapColourScaleDefinitions={categoryMapDefinitions} mapColourScale={mapColourScale} onMapColourScale={setMapColourScale}/>
2021-12-03 14:37:03 -05:00
<ThemeSwitcher onSubmit={themeSwitch} currentTheme={theme} />
<BoroughSwitcher onSubmit={boroughSwitch} currentDisplay={borough} />
2022-10-12 07:13:59 -04:00
<ParcelSwitcher onSubmit={parcelSwitch} currentDisplay={parcel} />
2022-11-08 08:40:37 -05:00
<FloodSwitcher onSubmit={floodSwitch} currentDisplay={flood} />
2022-11-08 15:47:01 -05:00
<ConservationAreaSwitcher onSubmit={conservationSwitch} currentDisplay={conservation} />
<HistoricDataSwitcher onSubmit={historicDataSwitch} currentDisplay={historicData} />
<SearchBox onLocate={handleLocate} />
</>
}
</div>
);
2019-05-27 11:20:00 -04:00
}
2021-12-03 14:32:54 -05:00
function ClickHandler({ onClick }: {onClick: (e) => void}) {
2021-04-26 14:19:06 -04:00
useMapEvent('click', (e) => onClick(e));
2021-12-03 14:32:54 -05:00
2021-04-26 14:19:06 -04:00
return null;
}
2021-12-03 14:32:54 -05:00
function MapBackgroundColor({ theme}: {theme: MapTheme}) {
const map = useMap();
useEffect(() => {
map.getContainer().style.backgroundColor = mapBackgroundColor[theme];
});
return null;
}
function MapViewport({
position,
zoom
}: {
position: [number, number];
zoom: number;
}) {
const map = useMap();
useEffect(() => {
map.setView(position, zoom)
}, [position, zoom]);
return null;
}