From ad6b2195ebb5309a4ae814d0a1d05720a9a6b22c Mon Sep 17 00:00:00 2001 From: Robert Johnstone Date: Mon, 23 Dec 2024 14:51:45 -0400 Subject: [PATCH] Decompose map startup, move map into context, and refactor draw tools --- .../LegacyMap/Controls/PrimaryLayerSelect.tsx | 2 +- app/src/UI/LegacyMap/InvasivesMap.tsx | 9 + app/src/UI/LegacyMap/Map.tsx | 514 ++++++------------ .../helpers/components/Coordinates.tsx | 65 +++ .../helpers/components/DrawControls.tsx | 214 ++++++++ .../helpers/components/MapContext.tsx | 6 + .../helpers/components/PositionMarkers.tsx | 145 +++++ .../helpers/components/ReactiveLayers.tsx | 117 ++++ app/src/UI/LegacyMap/helpers/draw-tools.ts | 266 --------- .../{ => functional}/client-boundaries.ts | 2 +- .../helpers/{ => functional}/constants.ts | 0 .../{ => functional}/current-record.ts | 2 +- .../functional/custom-drawing-modes.ts | 64 +++ .../{ => functional}/layer-definitions.ts | 4 +- .../helpers/{ => functional}/map-init.ts | 2 +- .../{ => functional}/position-tracking.ts | 4 +- .../{ => functional}/recordset-layers.ts | 6 +- .../{ => functional}/server-boundaries.ts | 2 +- .../{ => functional}/utility-functions.ts | 0 .../helpers/{ => functional}/whats-here.ts | 2 +- .../helpers/{ => functional}/wms-layers.ts | 2 +- app/src/state/reducers/tile_cache.ts | 2 +- app/src/state/sagas/map/layer-eligibility.ts | 2 +- 23 files changed, 807 insertions(+), 625 deletions(-) create mode 100644 app/src/UI/LegacyMap/InvasivesMap.tsx create mode 100644 app/src/UI/LegacyMap/helpers/components/Coordinates.tsx create mode 100644 app/src/UI/LegacyMap/helpers/components/DrawControls.tsx create mode 100644 app/src/UI/LegacyMap/helpers/components/MapContext.tsx create mode 100644 app/src/UI/LegacyMap/helpers/components/PositionMarkers.tsx create mode 100644 app/src/UI/LegacyMap/helpers/components/ReactiveLayers.tsx delete mode 100644 app/src/UI/LegacyMap/helpers/draw-tools.ts rename app/src/UI/LegacyMap/helpers/{ => functional}/client-boundaries.ts (94%) rename app/src/UI/LegacyMap/helpers/{ => functional}/constants.ts (100%) rename app/src/UI/LegacyMap/helpers/{ => functional}/current-record.ts (97%) create mode 100644 app/src/UI/LegacyMap/helpers/functional/custom-drawing-modes.ts rename app/src/UI/LegacyMap/helpers/{ => functional}/layer-definitions.ts (99%) rename app/src/UI/LegacyMap/helpers/{ => functional}/map-init.ts (98%) rename app/src/UI/LegacyMap/helpers/{ => functional}/position-tracking.ts (89%) rename app/src/UI/LegacyMap/helpers/{ => functional}/recordset-layers.ts (98%) rename app/src/UI/LegacyMap/helpers/{ => functional}/server-boundaries.ts (94%) rename app/src/UI/LegacyMap/helpers/{ => functional}/utility-functions.ts (100%) rename app/src/UI/LegacyMap/helpers/{ => functional}/whats-here.ts (93%) rename app/src/UI/LegacyMap/helpers/{ => functional}/wms-layers.ts (97%) diff --git a/app/src/UI/LegacyMap/Controls/PrimaryLayerSelect.tsx b/app/src/UI/LegacyMap/Controls/PrimaryLayerSelect.tsx index 4d3803cbd..a298864bb 100644 --- a/app/src/UI/LegacyMap/Controls/PrimaryLayerSelect.tsx +++ b/app/src/UI/LegacyMap/Controls/PrimaryLayerSelect.tsx @@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux'; import { IconButton, Tooltip } from '@mui/material'; import { useSelector } from 'utils/use_selector'; import 'UI/Global.css'; -import { MAP_DEFINITIONS } from 'UI/LegacyMap/helpers/layer-definitions'; +import { MAP_DEFINITIONS } from 'UI/LegacyMap/helpers/functional/layer-definitions'; import { DeviceUnknown, Hd, Landscape, Map, SaveAlt, Sd, SignalCellularNodata } from '@mui/icons-material'; import MapActions from 'state/actions/map'; diff --git a/app/src/UI/LegacyMap/InvasivesMap.tsx b/app/src/UI/LegacyMap/InvasivesMap.tsx new file mode 100644 index 000000000..39fb3f10c --- /dev/null +++ b/app/src/UI/LegacyMap/InvasivesMap.tsx @@ -0,0 +1,9 @@ +import maplibregl, { MapOptions } from 'maplibre-gl'; + +class InvasivesMap extends maplibregl.Map { + constructor(options: MapOptions) { + super(options); + } +} + +export { InvasivesMap }; diff --git a/app/src/UI/LegacyMap/Map.tsx b/app/src/UI/LegacyMap/Map.tsx index 4046940d3..47b0d76b6 100644 --- a/app/src/UI/LegacyMap/Map.tsx +++ b/app/src/UI/LegacyMap/Map.tsx @@ -1,50 +1,43 @@ -import circle from '@turf/circle'; -import maplibregl, { LngLatLike, Map as MapLibre } from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import React, { useContext, useEffect, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; import './map.css'; -import centroid from '@turf/centroid'; - -// Draw tools: -import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; -import { useHistory } from 'react-router-dom'; import { useSelector } from 'utils/use_selector'; import { getCurrentJWT } from 'state/sagas/auth/auth'; import { - allBaseMapLayerIdsNotInDefinition, - allOverlayLayerIdsNotInDefinitions, - allSourceIDsRequiredForDefinition, LAYER_Z_BACKGROUND, + LAYER_Z_FOREGROUND, LAYER_Z_MID, - layersForDefinition, MAP_DEFINITIONS -} from 'UI/LegacyMap/helpers/layer-definitions'; +} from 'UI/LegacyMap/helpers/functional/layer-definitions'; import { Context } from 'utils/tile-cache/context'; -import { mapInit } from 'UI/LegacyMap/helpers/map-init'; import { rebuildLayersOnTableHashUpdate, refreshColoursOnColourUpdate, refreshVisibilityOnToggleUpdate, removeDeletedRecordSetLayersOnRecordSetDelete, removeLayersOnNetworkConnectivityChange -} from 'UI/LegacyMap/helpers/recordset-layers'; -import { addWMSLayersIfNotExist, refreshWMSOnToggle } from 'UI/LegacyMap/helpers/wms-layers'; +} from 'UI/LegacyMap/helpers/functional/recordset-layers'; +import { addWMSLayersIfNotExist, refreshWMSOnToggle } from 'UI/LegacyMap/helpers/functional/wms-layers'; import { addServerBoundariesIfNotExists, refreshServerBoundariesOnToggle -} from 'UI/LegacyMap/helpers/server-boundaries'; +} from 'UI/LegacyMap/helpers/functional/server-boundaries'; import { addClientBoundariesIfNotExists, refreshClientBoundariesOnToggle -} from 'UI/LegacyMap/helpers/client-boundaries'; -import { handlePositionTracking } from 'UI/LegacyMap/helpers/position-tracking'; -import { refreshDrawControls } from 'UI/LegacyMap/helpers/draw-tools'; -import { refreshCurrentRecMakers, refreshHighlightedRecord } from 'UI/LegacyMap/helpers/current-record'; -import { toggleLayerOnBool } from 'UI/LegacyMap/helpers/utility-functions'; -import { refreshWhatsHereFeature } from 'UI/LegacyMap/helpers/whats-here'; +} from 'UI/LegacyMap/helpers/functional/client-boundaries'; import { DEFAULT_LOCAL_LAYERS } from 'state/reducers/map'; +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import { InvasivesMap } from 'UI/LegacyMap/InvasivesMap'; +import { PositionMarkers } from 'UI/LegacyMap/helpers/components/PositionMarkers'; +import maplibregl, { LngLatBoundsLike } from 'maplibre-gl'; +import { MOBILE } from 'state/build-time-config'; +import { PMTiles, Protocol } from 'pmtiles'; +import { TileCacheService } from 'utils/tile-cache'; +import { Coordinates } from 'UI/LegacyMap/helpers/components/Coordinates'; +import { ReactiveLayers } from 'UI/LegacyMap/helpers/components/ReactiveLayers'; +import { DrawControls } from 'UI/LegacyMap/helpers/components/DrawControls'; /* @@ -53,20 +46,15 @@ import { DEFAULT_LOCAL_LAYERS } from 'state/reducers/map'; */ export const Map = ({ children }) => { - const { API_BASE } = useSelector((state) => state.Configuration.current); + const { API_BASE, PUBLIC_MAP_URL } = useSelector((state) => state.Configuration.current); const tileCache = useContext(Context); - const [draw, setDraw] = useState(null); const [mapReady, setMapReady] = useState(false); const mapContainer: React.MutableRefObject = useRef(null); - const map: React.MutableRefObject = useRef(null); + // const map: React.MutableRefObject = useRef(null); const MapMode = useSelector((state) => state.Map.MapMode); - const dispatch = useDispatch(); - const uHistory = useHistory(); - // Avoid remounting map to avoid unnecesssary tile fetches or bad umounts: - const authInitiated = useSelector((state) => state.Auth.initialized); const { authenticated, loggedInOrWorkingOffline } = useSelector((state) => state.Auth); const connectedToNetwork = useSelector((state) => state.Network.connected); @@ -85,66 +73,143 @@ export const Map = ({ children }) => { const map_center = useSelector((state) => state.Map.map_center); const map_zoom = useSelector((state) => state.Map.map_zoom); - // User tracking coords jump and markers/indicators - const userCoords = useSelector((state) => state.Map.userCoords); - const accuracyToggle = useSelector((state) => state.Map.accuracyToggle); - const positionTracking = useSelector((state) => state.Map.positionTracking); - const panned = useSelector((state) => state.Map.panned); - const positionMarker = new maplibregl.Marker({ element: positionMarkerEl }); - const accuracyCircle = useSelector((state) => { - if (state.Map.userCoords?.long) { - return circle([state.Map?.userCoords?.long, state.Map?.userCoords?.lat], state.Map?.userCoords?.accuracy, { - steps: 64, - units: 'meters' - }); - } - return null; - }); - - // Draw tools - determine who needs edit and where the geos get dispatched, what tools to display etc - const whatsHereFeature = useSelector((state) => state.Map.whatsHere?.feature); - const whatsHereToggle = useSelector((state) => state.Map.whatsHere?.toggle); - const whatsHereMarker = new maplibregl.Marker({ element: whatsHereMarkerEl }); + const baseMapLayer = useSelector((state) => state.Map.baseMapLayer); - const tileCacheMode = useSelector((state) => state.Map.tileCacheMode); + const [map, setMap] = useState(); - const appModeUrl = useSelector((state) => state.AppMode.url); - // also used with current marker below: - const activityGeo = useSelector((state) => state.ActivityPage.activity?.geometry); - const drawingCustomLayer = useSelector((state) => state.Map.drawingCustomLayer); + useEffect(() => { + if (!mapContainer.current) { + console.error('Mapinit invoked with invalid reference'); + throw new Error('Mapinit invoked with invalid reference'); + } - //Current rec markers: - const currentActivityShortID = useSelector((state) => state.ActivityPage.activity?.short_id); - const currentIAPPID = useSelector((state) => state.IAPPSitePage.site?.site_id); - const currentIAPPGeo = useSelector((state) => state.IAPPSitePage.site?.geom); - const activityMarker = new maplibregl.Marker({ element: activityMarkerEl }); - const IAPPMarker = new maplibregl.Marker({ element: IAPPMarkerEl }); + const pmtilesProtocol = new Protocol(); + maplibregl.addProtocol('pmtiles', (request) => { + return new Promise((resolve, reject) => { + const callback = (err, data) => { + if (err) { + reject(err); + } else { + resolve({ data }); + } + }; + pmtilesProtocol.tile(request, callback); + }); + }); - //Highlighted Record from main records page: - const userRecordOnHoverRecordRow = useSelector((state) => state.Map.userRecordOnHoverRecordRow); - const userRecordOnHoverRecordType = useSelector((state) => state.Map.userRecordOnHoverRecordType); - const quickPanToRecord = useSelector((state) => state.Map.quickPanToRecord); + const PMTILES_URL = PUBLIC_MAP_URL || `https://nrs.objectstore.gov.bc.ca/uphjps/invasives-local.pmtiles`; + const p = new PMTiles(PMTILES_URL); - const baseMapLayer = useSelector((state) => state.Map.baseMapLayer); - const enabledOverlayLayers = useSelector((state) => state.Map.enabledOverlayLayers); + // this is so we share one instance across the JS code and the map renderer + pmtilesProtocol.add(p); + // pmtilesProtocol.add(new PMTiles(new Fetc())); - const offlineDefinitions = useSelector((state) => state.TileCache?.mapSpecifications); + if (MOBILE) { + if (!tileCache) { + throw new Error('tile cache unexpectedly not available'); + } + maplibregl.addProtocol('baked', async (request) => { + try { + const [repository, z, x, y] = request.url.replace('baked://', '').split('/'); + + return await tileCache.getTile(repository, Number(z), Number(x), Number(y)); + } catch (e) { + // this is a blank 256x256 image + return TileCacheService.generateFallbackTile(); + } + }); + } - const PUBLIC_MAP_URL = useSelector((state) => state.Configuration.current.PUBLIC_MAP_URL); + /* map can have platform-specific options */ + const platformOptions = (() => { + if (MOBILE) { + return { + maxBounds: [-141.7761, 46.41459, -114.049, 60.00678] as LngLatBoundsLike + }; + } + return {}; + })(); + + setMap( + new InvasivesMap({ + ...platformOptions, + container: mapContainer.current, + maxZoom: 24, + zoom: 3, + minZoom: 0, + transformRequest: (url) => { + if (url.includes(API_BASE)) { + return { + url, + headers: { + Authorization: () => { + if (authHeaderRef.current === undefined) { + console.error('requested access before header received'); + return ''; + } + return authHeaderRef.current; + } + } + }; + } + return { + url + }; + }, + center: [map_center[1], map_center[0]], + style: { + ...(MOBILE && { sprite: '/assets/basemaps/sprite/sprite' }), + glyphs: MOBILE + ? '/assets/basemaps/fonts/{fontstack}/{range}.pbf' + : 'https://fonts.openmaptiles.org/{fontstack}/{range}.pbf', + version: 8, + sources: { + ...MAP_DEFINITIONS.reduce((result, item) => { + result[item.name] = item.source; + return result; + }, {}) + }, + layers: [ + { + id: LAYER_Z_BACKGROUND, + type: 'background', + layout: { + visibility: 'none' + } + }, + { + id: LAYER_Z_MID, + type: 'background', + layout: { + visibility: 'none' + } + }, + { + id: LAYER_Z_FOREGROUND, + type: 'background', + layout: { + visibility: 'none' + } + } + ] + } + }) + ); + }, []); useEffect(() => { - if (!map.current || mapReady) return; + if (!map || mapReady) return; - map.current.once('idle', function () { - if (map.current !== null) { - map.current.resize(); + map.once('idle', function () { + if (map !== null) { + map.resize(); } }); - if (map.current.isStyleLoaded()) { + if (map.isStyleLoaded()) { setMapReady(true); } - }, [map?.current?.isStyleLoaded()]); + }, [map?.isStyleLoaded()]); const [currentAuthHeader, setCurrentAuthHeader] = useState(''); const authHeaderRef = useRef(); @@ -172,291 +237,68 @@ export const Map = ({ children }) => { }; }, [authenticated]); - // Map Init - useEffect(() => { - if (map.current || !authInitiated || !map_center) return; - - mapInit({ - map: map, - mapContainer: mapContainer, - api_base: API_BASE, - map_center: map_center, - PUBLIC_MAP_URL: PUBLIC_MAP_URL, - dispatch: dispatch, - tileCache: tileCache, - getAuthHeaderCallback: () => { - if (authHeaderRef.current === undefined) { - console.error('requested access before header received'); - return ''; - } - return authHeaderRef.current; - } - }); - }, [authInitiated, map_center]); - useEffect(() => { if (!mapReady) return; - if (!map.current) return; - removeLayersOnNetworkConnectivityChange(map.current); + if (!map) return; + removeLayersOnNetworkConnectivityChange(map); }, [connectedToNetwork]); // RecordSet Layers: useEffect(() => { if (!mapReady) return; - if (!map.current) return; - rebuildLayersOnTableHashUpdate(storeLayers, map.current, MapMode, API_BASE, connectedToNetwork); - refreshColoursOnColourUpdate(storeLayers, map.current); - refreshVisibilityOnToggleUpdate(storeLayers, map.current); - removeDeletedRecordSetLayersOnRecordSetDelete(storeLayers, map.current); - }, [storeLayers, map.current, mapReady, connectedToNetwork, loggedInOrWorkingOffline]); + if (!map) return; + rebuildLayersOnTableHashUpdate(storeLayers, map, MapMode, API_BASE, connectedToNetwork); + refreshColoursOnColourUpdate(storeLayers, map); + refreshVisibilityOnToggleUpdate(storeLayers, map); + removeDeletedRecordSetLayersOnRecordSetDelete(storeLayers, map); + }, [storeLayers, map, mapReady, connectedToNetwork, loggedInOrWorkingOffline]); // Layer picker: useEffect(() => { if (!mapReady) return; - if (!map.current) return; + if (!map) return; const layers = connectedToNetwork ? simplePickerLayers2 : DEFAULT_LOCAL_LAYERS; - addWMSLayersIfNotExist(layers, map.current); - refreshWMSOnToggle(layers, map.current); - }, [simplePickerLayers2, map.current, mapReady, baseMapLayer, connectedToNetwork]); + addWMSLayersIfNotExist(layers, map); + refreshWMSOnToggle(layers, map); + }, [simplePickerLayers2, map, mapReady, baseMapLayer, connectedToNetwork]); useEffect(() => { if (!mapReady) return; if (authenticated) { - addServerBoundariesIfNotExists(serverBoundaries, map.current); - refreshServerBoundariesOnToggle(serverBoundaries, map.current); + addServerBoundariesIfNotExists(serverBoundaries, map); + refreshServerBoundariesOnToggle(serverBoundaries, map); } - }, [serverBoundaries, authenticated, map.current, mapReady]); + }, [serverBoundaries, authenticated, map, mapReady]); useEffect(() => { if (!mapReady) return; - addClientBoundariesIfNotExists(clientBoundaries, map.current); - refreshClientBoundariesOnToggle(clientBoundaries, map.current); - }, [clientBoundaries, map.current, mapReady]); + addClientBoundariesIfNotExists(clientBoundaries, map); + refreshClientBoundariesOnToggle(clientBoundaries, map); + }, [clientBoundaries, map, mapReady]); // Jump Nav useEffect(() => { if (!mapReady) return; - if (!map.current) return; + if (!map) return; try { if (map_center && map_zoom) { - map.current.jumpTo({ center: map_center, zoom: map_zoom }); + map.jumpTo({ center: map_center, zoom: map_zoom }); } } catch (e) { console.error(e); } }, [map_center, map_zoom]); - // User position tracking and marker - useEffect(() => { - if (!mapReady) return; - handlePositionTracking( - map.current, - positionMarker, - userCoords, - accuracyCircle, - accuracyToggle, - positionTracking, - panned - ); - }, [userCoords, positionTracking, accuracyToggle, mapReady, panned]); - - // set base map layer - useEffect(() => { - if (!mapReady) return; - - if (!map.current) { - return; - } - - if (!baseMapLayer) { - return; - } - - const deactivateBaseLayers = allBaseMapLayerIdsNotInDefinition( - [...MAP_DEFINITIONS, ...(offlineDefinitions || [])], - baseMapLayer - ); - - const deactivateOverlayLayers = allOverlayLayerIdsNotInDefinitions( - [...MAP_DEFINITIONS, ...(offlineDefinitions || [])], - enabledOverlayLayers - ); - - const staticSources = MAP_DEFINITIONS.map((m) => { - return { - id: m.name, - source: m.source - }; - }); - - /* cached layers */ - const cachedSources = (offlineDefinitions || []).map((m) => { - return { - id: m.name, - source: m.source - }; - }); - - const allSources = [...staticSources, ...cachedSources]; - - const sourcesRequired = allSources.filter((s) => { - for (const layerToCheck of [baseMapLayer, ...enabledOverlayLayers]) { - if ( - allSourceIDsRequiredForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], layerToCheck).includes( - s.id - ) - ) { - return true; - } - } - return false; - }); - - const sourcesNotRequired = allSources.filter((s) => !sourcesRequired.some((r) => r.id == s.id)); - - // first remove the unneeded layers - for (const layerId of [...deactivateBaseLayers, ...deactivateOverlayLayers]) { - if (map.current.getLayer(layerId)) { - map.current.removeLayer(layerId); - } - } - - // now we can delete associated sources we no longer reference - for (const source of sourcesNotRequired) { - if (map.current.getSource(source.id)) { - map.current.removeSource(source.id); - } - } - - // ...add the required sources in - for (const source of sourcesRequired) { - if (!map.current.getSource(source.id)) { - map.current.addSource(source.id, source.source); - } - } - - // add the base map layers (which depend on the sources) - for (const layerSpec of layersForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], baseMapLayer)) { - if (!map.current.getLayer(layerSpec.id)) { - map.current.addLayer(layerSpec, LAYER_Z_BACKGROUND); - } - } - - // finally add the overlay layers (which can also depend on the sources) - for (const overlayLayer of enabledOverlayLayers) { - for (const layerSpec of layersForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], overlayLayer)) { - if (!map.current.getLayer(layerSpec.id)) { - map.current.addLayer(layerSpec, LAYER_Z_MID); - } - } - } - }, [baseMapLayer, enabledOverlayLayers, map.current, mapReady]); - - // Handle draw mode changes, controls, and action dispatching: - useEffect(() => { - if (!mapReady) return; - if (!map.current) return; - if (!appModeUrl) return; - - refreshDrawControls({ - map: map.current, - draw, - drawSetter: setDraw, - dispatch, - uHistory, - whatsHereToggle, - tileCacheMode, - appModeUrl, - activityGeo, - drawingCustomLayer - }); - }, [whatsHereToggle, tileCacheMode, appModeUrl, dispatch, map.current, activityGeo, drawingCustomLayer, mapReady]); - - //Current Activity & IAPP Markers - useEffect(() => { - if (!mapReady) return; - refreshCurrentRecMakers(map.current, { - activityGeo, - currentActivityShortID, - currentIAPPID, - currentIAPPGeo, - userRecordOnHoverRecordRow, - activityMarker, - IAPPMarker, - whatsHereMarker, - whatsHereFeature - }); - }, [currentActivityShortID, currentIAPPID, map.current, mapReady, userRecordOnHoverRecordRow]); - - //Highlighted Record - useEffect(() => { - if (!mapReady) return; - if (!map.current) return; - - refreshHighlightedRecord(map.current, { userRecordOnHoverRecordRow, userRecordOnHoverRecordType }); - - if (quickPanToRecord) { - if (userRecordOnHoverRecordRow && userRecordOnHoverRecordType === 'IAPP') { - if (userRecordOnHoverRecordRow.geometry) { - const c = centroid(userRecordOnHoverRecordRow.geometry).geometry.coordinates as LngLatLike; - if (c) { - map.current.jumpTo({ center: c, zoom: 15 }); - } - } - } - if (userRecordOnHoverRecordRow && userRecordOnHoverRecordType === 'Activity') { - if (userRecordOnHoverRecordRow.geometry?.[0]) { - const c = centroid(userRecordOnHoverRecordRow.geometry?.[0]).geometry.coordinates as LngLatLike; - if (c) { - map.current.jumpTo({ - center: c, - zoom: 15 - }); - } - } - } - } - - // Jump Nav - }, [userRecordOnHoverRecordRow, map.current, map?.current?.isStyleLoaded()]); - const [mapLoaded, setMapLoaded] = useState(false); useEffect(() => { setInterval(() => { - if (map.current) { - setMapLoaded(map.current.areTilesLoaded()); + if (map) { + setMapLoaded(map.areTilesLoaded()); } }, 1000); - }, [map.current]); - - // toggle public map pmtile layer - useEffect(() => { - if (!mapReady) return; - if (loggedInOrWorkingOffline) { - toggleLayerOnBool(map.current, 'invasivesbc-pmtile-vector', false); - toggleLayerOnBool(map.current, 'iapp-pmtile-vector', false); - toggleLayerOnBool(map.current, 'invasivesbc-pmtile-vector-label', false); - toggleLayerOnBool(map.current, 'iapp-pmtile-vector-label', false); - } - }, [loggedInOrWorkingOffline, map.current, mapReady]); - - useEffect(() => { - refreshWhatsHereFeature(map.current, { whatsHereFeature }); - }, [whatsHereFeature, appModeUrl, map.current, mapReady]); - - useEffect(() => { - try { - if (!mapReady) return; - if (!userCoords?.heading) return; - if (positionMarker?.getRotation() === userCoords?.heading) return; - positionMarker?.setRotationAlignment('map'); - positionMarker?.setRotation(userCoords?.heading); - } catch (e) { - console.error(e); - } - }, [userCoords?.heading, mapReady]); + }, [map]); return (
@@ -465,30 +307,16 @@ export const Map = ({ children }) => {
Loading tiles...
+ + + + + + + + {children}
); }; - -const positionMarkerEl = document.createElement('div'); -positionMarkerEl.className = 'userTrackingMarker'; -positionMarkerEl.innerHTML = ``; - -const activityMarkerEl = document.createElement('div'); -activityMarkerEl.className = 'activityMarkerEl'; -activityMarkerEl.style.backgroundImage = 'url(/assets/icon/clip.png)'; -activityMarkerEl.style.width = `32px`; -activityMarkerEl.style.height = `32px`; - -const IAPPMarkerEl = document.createElement('div'); -IAPPMarkerEl.className = 'IAPPMarkerEl'; -IAPPMarkerEl.style.backgroundImage = 'url(/assets/iapp_logo.gif)'; -IAPPMarkerEl.style.width = `32px`; -IAPPMarkerEl.style.height = `32px`; - -const whatsHereMarkerEl = document.createElement('div'); -whatsHereMarkerEl.className = 'whatsHereMarkerEl'; -whatsHereMarkerEl.style.backgroundImage = 'url(/assets/icon/pin.svg)'; -whatsHereMarkerEl.style.width = `32px`; -whatsHereMarkerEl.style.height = `32px`; diff --git a/app/src/UI/LegacyMap/helpers/components/Coordinates.tsx b/app/src/UI/LegacyMap/helpers/components/Coordinates.tsx new file mode 100644 index 000000000..2f86c1b1d --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/Coordinates.tsx @@ -0,0 +1,65 @@ +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import { useContext, useEffect, useRef } from 'react'; +import proj4 from 'proj4'; + +const Coordinates = () => { + const map = useContext(MapContext); + const coordinatesContainer = useRef(); + + useEffect(() => { + if (!map) { + return; + } + + coordinatesContainer.current = document.createElement('div'); + coordinatesContainer.current.style.position = 'absolute'; + coordinatesContainer.current.style.top = '10px'; + coordinatesContainer.current.style.left = '90px'; + coordinatesContainer.current.style.background = 'rgba(255, 255, 255, 0.8)'; + coordinatesContainer.current.style.padding = '5px'; + coordinatesContainer.current.style.borderRadius = '5px'; + coordinatesContainer.current.style.zIndex = '99'; + + const container = map.getContainer(); + container.appendChild(coordinatesContainer.current); + + container.addEventListener('mousemove', (e: MouseEvent) => { + const { clientX, clientY } = e; + updateCoordinatesContainer(clientX, clientY); + }); + container.addEventListener('touchstart', (e: TouchEvent) => { + const { clientX, clientY } = e.targetTouches[0]; + updateCoordinatesContainer(clientX, clientY); + }); + }, [map]); + + const updateCoordinatesContainer = (x: number, y: number) => { + if (!coordinatesContainer.current) return; + + const proj4_setdef = (utmZone: number): string => { + const zdef = `+proj=utm +zone=${utmZone} +datum=WGS84 +units=m +no_defs`; + return zdef; + }; + if (!map || !x || !y) { + return; + } + + const { lng, lat } = map.unproject([x, y]); + const utmZone = Math.floor((lng + 180) / 6) + 1; + proj4.defs([ + ['EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs'], + ['EPSG:AUTO', proj4_setdef(utmZone)] + ]); + + const utm: [number, number] = proj4('EPSG:4326', 'EPSG:AUTO', [lng, lat]); + + coordinatesContainer.current.innerHTML = ` +
${lat.toFixed(6)}, ${lng.toFixed(6)}
+
Zone ${utmZone}, E: ${utm[0].toFixed(0)}, N: ${utm[1].toFixed(0)}
+ `; + }; + + return null; +}; + +export { Coordinates }; diff --git a/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx b/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx new file mode 100644 index 000000000..e7ffa5146 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx @@ -0,0 +1,214 @@ +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw'; +import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; +import { useDispatch, useSelector } from 'utils/use_selector'; +import { MAP_ON_SHAPE_CREATE, MAP_ON_SHAPE_UPDATE } from 'state/actions'; +import TileCache from 'state/actions/cache/TileCache'; +import WhatsHere from 'state/actions/whatsHere/WhatsHere'; +import { useHistory } from 'react-router-dom'; +import { DoNothing, LotsOfPointsMode, WhatsHereBoxMode } from 'UI/LegacyMap/helpers/functional/custom-drawing-modes'; +import maplibregl, { IControl } from 'maplibre-gl'; +import { createRoot } from 'react-dom/client'; + +import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; + +// @ts-expect-error mapboxdraw compatibility with maplibre-gl issue +MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'; +// @ts-expect-error mapboxdraw compatibility with maplibre-gl issue +MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'; +// @ts-expect-error mapboxdraw compatibility with maplibre-gl issue +MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'; + +enum TargetMode { + DISABLED = 'DISABLED', + GENERIC = 'GENERIC', + WHATS_HERE = 'WHATS_HERE', + CUSTOM_LAYER = 'CUSTOM_LAYER', + ACTIVITY = 'ACTIVITY', + TILE_CACHE = 'TILE_CACHE' +} + +const DrawControls = () => { + const map = useContext(MapContext); + + const whatsHereToggle = useSelector((state) => state.Map.whatsHere.toggle); + const tileCacheMode = useSelector((state) => state.Map.tileCacheMode); + const activityGeo = useSelector((state) => state.ActivityPage.activity?.geometry); + const drawingCustomLayer = useSelector((state) => state.Map.drawingCustomLayer); + const appModeURL = useSelector((state) => state.AppMode.url); + + const dispatch = useDispatch(); + const drawInstance = useRef(); + + const uHistory = useHistory(); + + const [mode, setMode] = useState(TargetMode.DISABLED); + + const drawCreate = useCallback((event) => { + if (!drawInstance.current) return; + + //enforce one at a time everywhere + const feature = event.features[0]; + try { + drawInstance.current.deleteAll(); + drawInstance.current.add(feature); + } catch (e) { + console.error(e); + } + + switch (mode) { + case TargetMode.WHATS_HERE: { + dispatch(WhatsHere.map_feature({ type: 'Feature', geometry: feature.geometry })); + uHistory.push('/WhatsHere'); + break; + } + case TargetMode.ACTIVITY: { + break; + } + + case TargetMode.TILE_CACHE: { + dispatch(TileCache.setTileCacheShape({ geometry: feature.geometry })); + break; + } + case TargetMode.DISABLED: + default: { + dispatch({ type: MAP_ON_SHAPE_CREATE, payload: feature }); + break; + } + } + }, []); + + // setup mode based on what's going on in the redux store / current url + useEffect(() => { + console.log('recomputing mode'); + + const genericDrawTarget = true; + + if (whatsHereToggle) { + setMode(TargetMode.WHATS_HERE); + return; + } else if (tileCacheMode) { + setMode(TargetMode.TILE_CACHE); + return; + } else if (drawingCustomLayer) { + setMode(TargetMode.CUSTOM_LAYER); + return; + } else if (appModeURL?.includes('Activity')) { + setMode(TargetMode.ACTIVITY); + return; + } else if (genericDrawTarget) { + setMode(TargetMode.GENERIC); + return; + } + setMode(TargetMode.DISABLED); + }, [whatsHereToggle, tileCacheMode, drawingCustomLayer, appModeURL]); + + const drawShapeUpdate = useCallback((event) => { + if (!drawInstance.current) return; + + const editedGeo = drawInstance.current.getAll().features[0]; + if (editedGeo?.id !== event?.features?.[0]?.id) { + dispatch({ type: MAP_ON_SHAPE_UPDATE, payload: editedGeo }); + } + }, []); + + useEffect(() => { + if (!map) { + return; + } + + console.log(`setting up for ${mode}`); + + if (mode == TargetMode.DISABLED) { + return; + } + + const modes = (() => { + if (tileCacheMode) { + return { + ...MapboxDraw.modes + }; + } else { + return Object.assign( + { + draw_rectangle: DrawRectangle, + do_nothing: DoNothing, + lots_of_points: LotsOfPointsMode, + whats_here_box_mode: WhatsHereBoxMode + }, + MapboxDraw.modes + ); + } + })(); + + drawInstance.current = new MapboxDraw({ + displayControlsDefault: true, + controls: { + combine_features: false, + uncombine_features: false + }, + defaultMode: mode == TargetMode.WHATS_HERE ? 'whats_here_box_mode' : 'simple_select', + modes: modes as { [modeKey: string]: DrawCustomMode } + }); + + const drawModeDisplay = new DrawModeDisplay(mode); + + map.on('draw.create', drawCreate); + map.on('draw.selectionchange', drawShapeUpdate); + + map.addControl(drawInstance.current as unknown as IControl, 'top-left'); + map.addControl(drawModeDisplay, 'top-left'); + + // cleanup + return () => { + if (!map) { + return; + } + map.off('draw.create', drawCreate); + map.off('draw.selectionChange', drawShapeUpdate); + if (drawInstance.current) { + (map as unknown as mapboxgl.Map).removeControl(drawInstance.current); + drawInstance.current = undefined; + } + + map.removeControl(drawModeDisplay); + + console.debug('mapboxdraw listener cleanup complete'); + }; + }, [map, mode]); + + return null; +}; + +class DrawModeDisplay implements IControl { + _text: string; + _map: maplibregl.Map | undefined; + _container: HTMLDivElement | undefined; + + constructor(mode: TargetMode) { + this._text = mode; + } + + onAdd(map: maplibregl.Map): HTMLElement { + this._map = map; + const control = document.createElement('div'); + control.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + + const root = createRoot(control); + + root.render(<>Current drawing mode: {this._text}); + + this._container = control; + + return this._container; + } + + onRemove(_map: maplibregl.Map) { + if (this._container?.parentNode) { + this._container.parentNode.removeChild(this._container); + } + } +} + +export { DrawControls }; diff --git a/app/src/UI/LegacyMap/helpers/components/MapContext.tsx b/app/src/UI/LegacyMap/helpers/components/MapContext.tsx new file mode 100644 index 000000000..b4da92182 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/MapContext.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { InvasivesMap } from 'UI/LegacyMap/InvasivesMap'; + +const MapContext = React.createContext(undefined); + +export { MapContext }; diff --git a/app/src/UI/LegacyMap/helpers/components/PositionMarkers.tsx b/app/src/UI/LegacyMap/helpers/components/PositionMarkers.tsx new file mode 100644 index 000000000..fa198600c --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/PositionMarkers.tsx @@ -0,0 +1,145 @@ +import { useContext, useEffect } from 'react'; +import { refreshWhatsHereFeature } from 'UI/LegacyMap/helpers/functional/whats-here'; +import { refreshCurrentRecMakers, refreshHighlightedRecord } from 'UI/LegacyMap/helpers/functional/current-record'; +import centroid from '@turf/centroid'; +import maplibregl, { LngLatLike } from 'maplibre-gl'; +import { useSelector } from 'utils/use_selector'; +import circle from '@turf/circle'; +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import { handlePositionTracking } from 'UI/LegacyMap/helpers/functional/position-tracking'; + +const PositionMarkers = ({ mapReady }) => { + const map = useContext(MapContext); + + // User tracking coords jump and markers/indicators + const userCoords = useSelector((state) => state.Map.userCoords); + const accuracyToggle = useSelector((state) => state.Map.accuracyToggle); + const positionTracking = useSelector((state) => state.Map.positionTracking); + const panned = useSelector((state) => state.Map.panned); + const positionMarker = new maplibregl.Marker({ element: positionMarkerEl }); + const accuracyCircle = useSelector((state) => { + if (state.Map.userCoords?.long) { + return circle([state.Map?.userCoords?.long, state.Map?.userCoords?.lat], state.Map?.userCoords?.accuracy, { + steps: 64, + units: 'meters' + }); + } + return null; + }); + + // Draw tools - determine who needs edit and where the geos get dispatched, what tools to display etc + const whatsHereFeature = useSelector((state) => state.Map.whatsHere?.feature); + const whatsHereMarker = new maplibregl.Marker({ element: whatsHereMarkerEl }); + + const appModeUrl = useSelector((state) => state.AppMode.url); + // also used with current marker below: + const activityGeo = useSelector((state) => state.ActivityPage.activity?.geometry); + + //Current rec markers: + const currentActivityShortID = useSelector((state) => state.ActivityPage.activity?.short_id); + const currentIAPPID = useSelector((state) => state.IAPPSitePage.site?.site_id); + const currentIAPPGeo = useSelector((state) => state.IAPPSitePage.site?.geom); + const activityMarker = new maplibregl.Marker({ element: activityMarkerEl }); + const IAPPMarker = new maplibregl.Marker({ element: IAPPMarkerEl }); + + //Highlighted Record from main records page: + const userRecordOnHoverRecordRow = useSelector((state) => state.Map.userRecordOnHoverRecordRow); + const userRecordOnHoverRecordType = useSelector((state) => state.Map.userRecordOnHoverRecordType); + const quickPanToRecord = useSelector((state) => state.Map.quickPanToRecord); + + //Current Activity & IAPP Markers + useEffect(() => { + if (!mapReady) return; + refreshCurrentRecMakers(map, { + activityGeo, + currentActivityShortID, + currentIAPPID, + currentIAPPGeo, + userRecordOnHoverRecordRow, + activityMarker, + IAPPMarker, + whatsHereMarker, + whatsHereFeature + }); + }, [currentActivityShortID, currentIAPPID, map, mapReady, userRecordOnHoverRecordRow]); + + //Highlighted Record + useEffect(() => { + if (!mapReady) return; + if (!map) return; + + refreshHighlightedRecord(map, { userRecordOnHoverRecordRow, userRecordOnHoverRecordType }); + + if (quickPanToRecord) { + if (userRecordOnHoverRecordRow && userRecordOnHoverRecordType === 'IAPP') { + if (userRecordOnHoverRecordRow.geometry) { + const c = centroid(userRecordOnHoverRecordRow.geometry).geometry.coordinates as LngLatLike; + if (c) { + map.jumpTo({ center: c, zoom: 15 }); + } + } + } + if (userRecordOnHoverRecordRow && userRecordOnHoverRecordType === 'Activity') { + if (userRecordOnHoverRecordRow.geometry?.[0]) { + const c = centroid(userRecordOnHoverRecordRow.geometry?.[0]).geometry.coordinates as LngLatLike; + if (c) { + map.jumpTo({ + center: c, + zoom: 15 + }); + } + } + } + } + + // Jump Nav + }, [userRecordOnHoverRecordRow, map, map?.isStyleLoaded()]); + + useEffect(() => { + refreshWhatsHereFeature(map, { whatsHereFeature }); + }, [whatsHereFeature, appModeUrl, map, mapReady]); + + useEffect(() => { + try { + if (!mapReady) return; + if (!userCoords?.heading) return; + if (positionMarker?.getRotation() === userCoords?.heading) return; + positionMarker?.setRotationAlignment('map'); + positionMarker?.setRotation(userCoords?.heading); + } catch (e) { + console.error(e); + } + }, [userCoords?.heading, mapReady]); + + // User position tracking and marker + useEffect(() => { + if (!mapReady) return; + handlePositionTracking(map, positionMarker, userCoords, accuracyCircle, accuracyToggle, positionTracking, panned); + }, [userCoords, positionTracking, accuracyToggle, mapReady, panned]); + + return null; +}; + +const positionMarkerEl = document.createElement('div'); +positionMarkerEl.className = 'userTrackingMarker'; +positionMarkerEl.innerHTML = ``; + +const activityMarkerEl = document.createElement('div'); +activityMarkerEl.className = 'activityMarkerEl'; +activityMarkerEl.style.backgroundImage = 'url(/assets/icon/clip.png)'; +activityMarkerEl.style.width = `32px`; +activityMarkerEl.style.height = `32px`; + +const IAPPMarkerEl = document.createElement('div'); +IAPPMarkerEl.className = 'IAPPMarkerEl'; +IAPPMarkerEl.style.backgroundImage = 'url(/assets/iapp_logo.gif)'; +IAPPMarkerEl.style.width = `32px`; +IAPPMarkerEl.style.height = `32px`; + +const whatsHereMarkerEl = document.createElement('div'); +whatsHereMarkerEl.className = 'whatsHereMarkerEl'; +whatsHereMarkerEl.style.backgroundImage = 'url(/assets/icon/pin.svg)'; +whatsHereMarkerEl.style.width = `32px`; +whatsHereMarkerEl.style.height = `32px`; + +export { PositionMarkers }; diff --git a/app/src/UI/LegacyMap/helpers/components/ReactiveLayers.tsx b/app/src/UI/LegacyMap/helpers/components/ReactiveLayers.tsx new file mode 100644 index 000000000..1412b1b76 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/ReactiveLayers.tsx @@ -0,0 +1,117 @@ +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import { useContext, useEffect } from 'react'; +import { + allBaseMapLayerIdsNotInDefinition, + allOverlayLayerIdsNotInDefinitions, + allSourceIDsRequiredForDefinition, + LAYER_Z_BACKGROUND, + LAYER_Z_MID, + layersForDefinition, + MAP_DEFINITIONS +} from 'UI/LegacyMap/helpers/functional/layer-definitions'; +import { useSelector } from 'utils/use_selector'; + +const ReactiveLayers = ({ mapReady }) => { + const map = useContext(MapContext); + + const baseMapLayer = useSelector((state) => state.Map.baseMapLayer); + const enabledOverlayLayers = useSelector((state) => state.Map.enabledOverlayLayers); + + const offlineDefinitions = useSelector((state) => state.TileCache?.mapSpecifications); + + // set base map layer + useEffect(() => { + if (!mapReady) return; + + if (!map) { + return; + } + + if (!baseMapLayer) { + return; + } + + const deactivateBaseLayers = allBaseMapLayerIdsNotInDefinition( + [...MAP_DEFINITIONS, ...(offlineDefinitions || [])], + baseMapLayer + ); + + const deactivateOverlayLayers = allOverlayLayerIdsNotInDefinitions( + [...MAP_DEFINITIONS, ...(offlineDefinitions || [])], + enabledOverlayLayers + ); + + const staticSources = MAP_DEFINITIONS.map((m) => { + return { + id: m.name, + source: m.source + }; + }); + + /* cached layers */ + const cachedSources = (offlineDefinitions || []).map((m) => { + return { + id: m.name, + source: m.source + }; + }); + + const allSources = [...staticSources, ...cachedSources]; + + const sourcesRequired = allSources.filter((s) => { + for (const layerToCheck of [baseMapLayer, ...enabledOverlayLayers]) { + if ( + allSourceIDsRequiredForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], layerToCheck).includes( + s.id + ) + ) { + return true; + } + } + return false; + }); + + const sourcesNotRequired = allSources.filter((s) => !sourcesRequired.some((r) => r.id == s.id)); + + // first remove the unneeded layers + for (const layerId of [...deactivateBaseLayers, ...deactivateOverlayLayers]) { + if (map.getLayer(layerId)) { + map.removeLayer(layerId); + } + } + + // now we can delete associated sources we no longer reference + for (const source of sourcesNotRequired) { + if (map.getSource(source.id)) { + map.removeSource(source.id); + } + } + + // ...add the required sources in + for (const source of sourcesRequired) { + if (!map.getSource(source.id)) { + map.addSource(source.id, source.source); + } + } + + // add the base map layers (which depend on the sources) + for (const layerSpec of layersForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], baseMapLayer)) { + if (!map.getLayer(layerSpec.id)) { + map.addLayer(layerSpec, LAYER_Z_BACKGROUND); + } + } + + // finally add the overlay layers (which can also depend on the sources) + for (const overlayLayer of enabledOverlayLayers) { + for (const layerSpec of layersForDefinition([...MAP_DEFINITIONS, ...(offlineDefinitions || [])], overlayLayer)) { + if (!map.getLayer(layerSpec.id)) { + map.addLayer(layerSpec, LAYER_Z_MID); + } + } + } + }, [baseMapLayer, enabledOverlayLayers, map, mapReady]); + + return null; +}; + +export { ReactiveLayers }; diff --git a/app/src/UI/LegacyMap/helpers/draw-tools.ts b/app/src/UI/LegacyMap/helpers/draw-tools.ts deleted file mode 100644 index fc9be9c61..000000000 --- a/app/src/UI/LegacyMap/helpers/draw-tools.ts +++ /dev/null @@ -1,266 +0,0 @@ -import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw'; -import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; - -import maplibregl from 'maplibre-gl'; -import WhatsHere from 'state/actions/whatsHere/WhatsHere'; -import TileCache from 'state/actions/cache/TileCache'; -import { MAP_ON_SHAPE_CREATE, MAP_ON_SHAPE_UPDATE } from 'state/actions'; -import { AppDispatch } from 'utils/use_selector'; -import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; - -// @ts-ignore -MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl'; -// @ts-ignore -MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-'; -// @ts-ignore -MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group'; - -interface RefreshDrawControlsOptions { - map: maplibregl.Map; - draw; - drawSetter; - dispatch: AppDispatch; - uHistory; - whatsHereToggle: boolean; - tileCacheMode: boolean; - appModeUrl: string; - activityGeo; - drawingCustomLayer: boolean; -} - -export const refreshDrawControls = (options: RefreshDrawControlsOptions) => { - const { - map, - draw, - drawSetter, - dispatch, - uHistory, - whatsHereToggle, - tileCacheMode, - appModeUrl, - activityGeo, - drawingCustomLayer - } = options; - /* - We fully tear down map box draw and re-add depending on app state / route, to have conditionally rendered controls: - Because mapbox draw doesn't clean up its old sources properly we need to do it manually - */ - try { - if (map.hasControl(draw)) { - map.removeControl(draw); - drawSetter(null); - } - } catch (e) { - console.error(e); - } - - if (!map.hasControl(draw)) { - const noMapVisible = /Report|Batch|Landing|WhatsHere/.test(appModeUrl); - const userInActivity = /Activity/.test(appModeUrl); - let hideControls = (noMapVisible || !userInActivity) && !drawingCustomLayer; - if (tileCacheMode) { - hideControls = false; - } - - initDrawModes({ - map, - drawSetter, - dispatch, - uHistory, - hideControls, - activityGeo: userInActivity ? activityGeo : null, - whatsHereToggle, - tileCacheMode, - - draw - }); - } -}; - -const customDrawListenerCreate = (drawInstance, dispatch, uHistory, whats_here_toggle, tileCacheMode) => (e) => { - //enforce one at a time everywhere - const feature = e.features[0]; - try { - if (drawInstance) { - drawInstance.deleteAll(); - drawInstance.add(feature); - } - } catch (e) { - console.error(e); - } - - // For what's here - if (whats_here_toggle) { - dispatch(WhatsHere.map_feature({ type: 'Feature', geometry: feature.geometry })); - uHistory.push('/WhatsHere'); - } else if (tileCacheMode) { - dispatch(TileCache.setTileCacheShape({ geometry: feature.geometry })); - } else { - dispatch({ type: MAP_ON_SHAPE_CREATE, payload: feature }); - } -}; - -const customDrawListenerSelectionChange = (drawInstance: MapboxDraw, dispatch) => (e) => { - const editedGeo = drawInstance.getAll().features[0]; - if (editedGeo?.id !== e?.features?.[0]?.id) { - dispatch({ type: MAP_ON_SHAPE_UPDATE, payload: editedGeo }); - } -}; - -const attachedListeners: WeakRef[] = []; - -interface InitDrawModesOptions { - map: maplibregl.Map; - drawSetter; - dispatch: AppDispatch; - uHistory; - hideControls: boolean; - activityGeo; - whatsHereToggle: boolean; - tileCacheMode: boolean; - draw; -} - -export const initDrawModes = (options: InitDrawModesOptions) => { - const { map, drawSetter, dispatch, uHistory, hideControls, activityGeo, whatsHereToggle, tileCacheMode, draw } = - options; - - ['draw.selectionchange', 'draw.create', 'draw.update'].map((eName) => { - map._listeners[eName]?.map((l) => { - let indexToSplice = -1; - let refFound = false; - - for (let i = 0; i < attachedListeners.length; i++) { - if (attachedListeners[i].deref() === l) { - refFound = true; - indexToSplice = i; - } - } - - if (refFound) { - // remove from ref list - attachedListeners.splice(indexToSplice, 1); - map.off(eName, l); - } - }); - }); - - const DoNothing: any = {}; - DoNothing.onSetup = function (opts) { - // if(map.draw && activityGeo) - if (activityGeo) { - this.addFeature(this.newFeature(activityGeo[0])); - } - - const state: any = {}; - state.count = opts.count || 0; - return state; - }; - DoNothing.onClick = function (state, e) { - this.changeMode('draw_polygon'); - }; - - DoNothing.toDisplayFeatures = function (state, geojson, display) { - geojson.properties.active = MapboxDraw.constants.activeStates.ACTIVE; - display(geojson); - }; - - DoNothing.on; - - const WhatsHereBoxMode: any = { ...DrawRectangle }; - - //Example from docs - keeping as template: - const LotsOfPointsMode: any = {}; - - // When the mode starts this function will be called. - // The `opts` argument comes from `draw.changeMode('lotsofpoints', {count:7})`. - // The value returned should be an object and will be passed to all other lifecycle functions - LotsOfPointsMode.onSetup = function (opts) { - const state: any = {}; - state.count = opts.count || 0; - return state; - }; - - // Whenever a user clicks on the map, Draw will call `onClick` - LotsOfPointsMode.onClick = function (state, e) { - // `this.newFeature` takes geojson and makes a DrawFeature - const point = this.newFeature({ - type: 'Feature', - properties: { - count: state.count - }, - geometry: { - type: 'Point', - coordinates: [e.lngLat.lng, e.lngLat.lat] - } - }); - this.addFeature(point); // puts the point on the map - }; - - // Whenever a user clicks on a key while focused on the map, it will be sent here - LotsOfPointsMode.onKeyUp = function (state, e) { - if (e.keyCode === 27) return this.changeMode('simple_select'); - }; - - // This is the only required function for a mode. - // It decides which features currently in Draw's data store will be rendered on the map. - // All features passed to `display` will be rendered, so you can pass multiple display features per internal feature. - // See `styling-draw` in `API.md` for advice on making display features - LotsOfPointsMode.toDisplayFeatures = function (state, geojson, display) { - display(geojson); - }; - - const mode = (() => { - if (whatsHereToggle) { - return 'whats_here_box_mode'; - } - return 'simple_select'; - })(); - - const modes = (() => { - if (tileCacheMode) { - return { - ...MapboxDraw.modes - }; - } else { - return Object.assign( - { - draw_rectangle: DrawRectangle, - do_nothing: DoNothing, - lots_of_points: LotsOfPointsMode, - whats_here_box_mode: WhatsHereBoxMode - }, - MapboxDraw.modes - ); - } - })(); - - // Add the new draw mode to the MapboxDraw object - const localDraw = new MapboxDraw({ - displayControlsDefault: !hideControls, - controls: { - combine_features: false, - uncombine_features: false - }, - defaultMode: mode, - // Adds the LotsOfPointsMode to the built-in set of modes - modes: modes as { [modeKey: string]: DrawCustomMode } - }); - - const drawCreateListener = customDrawListenerCreate(localDraw, dispatch, uHistory, whatsHereToggle, tileCacheMode); - const drawSelectionchangeListener = customDrawListenerSelectionChange(localDraw, dispatch); - - map.on('draw.create', drawCreateListener); - map.on('draw.selectionchange', drawSelectionchangeListener); - - attachedListeners.push(new WeakRef(drawSelectionchangeListener)); - attachedListeners.push(new WeakRef(drawCreateListener)); - - if (!map.hasControl(draw)) { - map.addControl(localDraw, 'top-left'); - } - if (activityGeo) { - localDraw.add({ type: 'FeatureCollection', features: activityGeo }); - } - drawSetter(localDraw); -}; diff --git a/app/src/UI/LegacyMap/helpers/client-boundaries.ts b/app/src/UI/LegacyMap/helpers/functional/client-boundaries.ts similarity index 94% rename from app/src/UI/LegacyMap/helpers/client-boundaries.ts rename to app/src/UI/LegacyMap/helpers/functional/client-boundaries.ts index 82535b7db..daeaeac29 100644 --- a/app/src/UI/LegacyMap/helpers/client-boundaries.ts +++ b/app/src/UI/LegacyMap/helpers/functional/client-boundaries.ts @@ -1,4 +1,4 @@ -import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/layer-definitions'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; export const addClientBoundariesIfNotExists = (clientBoundaries, map) => { if (map && clientBoundaries?.length > 0) { diff --git a/app/src/UI/LegacyMap/helpers/constants.ts b/app/src/UI/LegacyMap/helpers/functional/constants.ts similarity index 100% rename from app/src/UI/LegacyMap/helpers/constants.ts rename to app/src/UI/LegacyMap/helpers/functional/constants.ts diff --git a/app/src/UI/LegacyMap/helpers/current-record.ts b/app/src/UI/LegacyMap/helpers/functional/current-record.ts similarity index 97% rename from app/src/UI/LegacyMap/helpers/current-record.ts rename to app/src/UI/LegacyMap/helpers/functional/current-record.ts index a5605b5e7..136233f40 100644 --- a/app/src/UI/LegacyMap/helpers/current-record.ts +++ b/app/src/UI/LegacyMap/helpers/functional/current-record.ts @@ -1,5 +1,5 @@ import centroid from '@turf/centroid'; -import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/layer-definitions'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; export const refreshCurrentRecMakers = (map, options: any) => { if (options.IAPPMarker && options.currentIAPPGeo?.geometry && options.currentIAPPID) { diff --git a/app/src/UI/LegacyMap/helpers/functional/custom-drawing-modes.ts b/app/src/UI/LegacyMap/helpers/functional/custom-drawing-modes.ts new file mode 100644 index 000000000..1fc003ac3 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/functional/custom-drawing-modes.ts @@ -0,0 +1,64 @@ +import MapboxDraw, { DrawCustomMode } from '@mapbox/mapbox-gl-draw'; +import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; + +const DoNothing: Partial = {}; +DoNothing.onSetup = function (opts) { + const state = { + count: opts.count ?? 0 + }; + + return state; +}; +DoNothing.onClick = function () { + this.changeMode('draw_polygon'); +}; + +DoNothing.toDisplayFeatures = function (state, geojson, display) { + if (Object.hasOwn(geojson, 'properties')) { + geojson.properties.active = MapboxDraw.constants.activeStates.ACTIVE; + } + + display(geojson); +}; + +const WhatsHereBoxMode: Partial = { ...DrawRectangle }; + +//Example from docs - keeping as template: +const LotsOfPointsMode: Partial = {}; + +// When the mode starts this function will be called. +// The `opts` argument comes from `draw.changeMode('lotsofpoints', {count:7})`. +// The value returned should be an object and will be passed to all other lifecycle functions +LotsOfPointsMode.onSetup = function (opts) { + const state = { + count: opts.count ?? 0 + }; + return state; +}; + +// Whenever a user clicks on the map, Draw will call `onClick` +LotsOfPointsMode.onClick = function (state, e) { + // `this.newFeature` takes geojson and makes a DrawFeature + const point = this.newFeature({ + type: 'Feature', + properties: { + count: state.count + }, + geometry: { + type: 'Point', + coordinates: [e.lngLat.lng, e.lngLat.lat] + } + }); + this.addFeature(point); // puts the point on the map +}; + +// Whenever a user clicks on a key while focused on the map, it will be sent here +LotsOfPointsMode.onKeyUp = function (state, e) { + if (e.keyCode === 27) return this.changeMode('simple_select'); +}; + +LotsOfPointsMode.toDisplayFeatures = function (state, geojson, display) { + display(geojson); +}; + +export { LotsOfPointsMode, DoNothing, WhatsHereBoxMode }; diff --git a/app/src/UI/LegacyMap/helpers/layer-definitions.ts b/app/src/UI/LegacyMap/helpers/functional/layer-definitions.ts similarity index 99% rename from app/src/UI/LegacyMap/helpers/layer-definitions.ts rename to app/src/UI/LegacyMap/helpers/functional/layer-definitions.ts index c75c28dc0..392ff2bd1 100644 --- a/app/src/UI/LegacyMap/helpers/layer-definitions.ts +++ b/app/src/UI/LegacyMap/helpers/functional/layer-definitions.ts @@ -1,5 +1,5 @@ -import VECTOR_MAP_FONT_FACE from 'constants/vectorMapFontFace'; import { LayerSpecification, SourceSpecification } from 'maplibre-gl'; +import VECTOR_MAP_FONT_FACE from 'constants/vectorMapFontFace'; import { MOBILE } from 'state/build-time-config'; // these layers are used as placeholders so the others can be placed relative to them @@ -235,7 +235,7 @@ const MAP_DEFINITIONS: MapSourceAndLayerDefinition[] = [ mode: MapSourceAndLayerDefinitionMode.OVERLAY, - predicates: new MapDefinitionEligibilityPredicatesBuilder().requiresAnonymous(false).build(), + predicates: new MapDefinitionEligibilityPredicatesBuilder().requiresAnonymous(true).build(), source: { type: 'vector', url: 'pmtiles://https://nrs.objectstore.gov.bc.ca/rzivsz/invasives-prod.pmtiles' diff --git a/app/src/UI/LegacyMap/helpers/map-init.ts b/app/src/UI/LegacyMap/helpers/functional/map-init.ts similarity index 98% rename from app/src/UI/LegacyMap/helpers/map-init.ts rename to app/src/UI/LegacyMap/helpers/functional/map-init.ts index 7a9041e21..2bb1a9503 100644 --- a/app/src/UI/LegacyMap/helpers/map-init.ts +++ b/app/src/UI/LegacyMap/helpers/functional/map-init.ts @@ -10,7 +10,7 @@ import { LAYER_Z_FOREGROUND, LAYER_Z_MID, MAP_DEFINITIONS -} from 'UI/LegacyMap/helpers/layer-definitions'; +} from 'UI/LegacyMap/helpers/functional/layer-definitions'; interface MapInitOptions { map: React.MutableRefObject; diff --git a/app/src/UI/LegacyMap/helpers/position-tracking.ts b/app/src/UI/LegacyMap/helpers/functional/position-tracking.ts similarity index 89% rename from app/src/UI/LegacyMap/helpers/position-tracking.ts rename to app/src/UI/LegacyMap/helpers/functional/position-tracking.ts index 42f329223..ef91fdf31 100644 --- a/app/src/UI/LegacyMap/helpers/position-tracking.ts +++ b/app/src/UI/LegacyMap/helpers/functional/position-tracking.ts @@ -1,5 +1,5 @@ -import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/layer-definitions'; -import { toggleLayerOnBool } from 'UI/LegacyMap/helpers/utility-functions'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; +import { toggleLayerOnBool } from 'UI/LegacyMap/helpers/functional/utility-functions'; export const handlePositionTracking = ( map, diff --git a/app/src/UI/LegacyMap/helpers/recordset-layers.ts b/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts similarity index 98% rename from app/src/UI/LegacyMap/helpers/recordset-layers.ts rename to app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts index 210489aae..310c65989 100644 --- a/app/src/UI/LegacyMap/helpers/recordset-layers.ts +++ b/app/src/UI/LegacyMap/helpers/functional/recordset-layers.ts @@ -6,9 +6,9 @@ import maplibregl, { SourceSpecification, SymbolLayerSpecification } from 'maplibre-gl'; -import { LAYER_Z_BACKGROUND, LAYER_Z_FOREGROUND, LAYER_Z_MID } from 'UI/LegacyMap/helpers/layer-definitions'; -import { FALLBACK_COLOR } from 'UI/LegacyMap/helpers/constants'; -import { safelySetPaintProperty } from 'UI/LegacyMap/helpers/utility-functions'; +import { LAYER_Z_BACKGROUND, LAYER_Z_FOREGROUND, LAYER_Z_MID } from 'UI/LegacyMap/helpers/functional/layer-definitions'; +import { FALLBACK_COLOR } from 'UI/LegacyMap/helpers/functional/constants'; +import { safelySetPaintProperty } from 'UI/LegacyMap/helpers/functional/utility-functions'; import { MOBILE } from 'state/build-time-config'; import { RecordSetType } from 'interfaces/UserRecordSet'; import VECTOR_MAP_FONT_FACE from 'constants/vectorMapFontFace'; diff --git a/app/src/UI/LegacyMap/helpers/server-boundaries.ts b/app/src/UI/LegacyMap/helpers/functional/server-boundaries.ts similarity index 94% rename from app/src/UI/LegacyMap/helpers/server-boundaries.ts rename to app/src/UI/LegacyMap/helpers/functional/server-boundaries.ts index cce3f4fec..875f373cb 100644 --- a/app/src/UI/LegacyMap/helpers/server-boundaries.ts +++ b/app/src/UI/LegacyMap/helpers/functional/server-boundaries.ts @@ -1,4 +1,4 @@ -import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/layer-definitions'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; export const addServerBoundariesIfNotExists = (serverBoundaries, map) => { if (map && serverBoundaries?.length > 0) { diff --git a/app/src/UI/LegacyMap/helpers/utility-functions.ts b/app/src/UI/LegacyMap/helpers/functional/utility-functions.ts similarity index 100% rename from app/src/UI/LegacyMap/helpers/utility-functions.ts rename to app/src/UI/LegacyMap/helpers/functional/utility-functions.ts diff --git a/app/src/UI/LegacyMap/helpers/whats-here.ts b/app/src/UI/LegacyMap/helpers/functional/whats-here.ts similarity index 93% rename from app/src/UI/LegacyMap/helpers/whats-here.ts rename to app/src/UI/LegacyMap/helpers/functional/whats-here.ts index 2892f79c8..a61fa2906 100644 --- a/app/src/UI/LegacyMap/helpers/whats-here.ts +++ b/app/src/UI/LegacyMap/helpers/functional/whats-here.ts @@ -1,4 +1,4 @@ -import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/layer-definitions'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; export const refreshWhatsHereFeature = (map, options: any) => { const layerID = 'WhatsHereFeatureLayer'; diff --git a/app/src/UI/LegacyMap/helpers/wms-layers.ts b/app/src/UI/LegacyMap/helpers/functional/wms-layers.ts similarity index 97% rename from app/src/UI/LegacyMap/helpers/wms-layers.ts rename to app/src/UI/LegacyMap/helpers/functional/wms-layers.ts index 03c9032d4..c7ddfcecd 100644 --- a/app/src/UI/LegacyMap/helpers/wms-layers.ts +++ b/app/src/UI/LegacyMap/helpers/functional/wms-layers.ts @@ -1,4 +1,4 @@ -import { LAYER_Z_FOREGROUND, LAYER_Z_MID } from 'UI/LegacyMap/helpers/layer-definitions'; +import { LAYER_Z_FOREGROUND, LAYER_Z_MID } from 'UI/LegacyMap/helpers/functional/layer-definitions'; export const addWMSLayersIfNotExist = (simplePickerLayers2: any, map) => { simplePickerLayers2.map((layer) => { diff --git a/app/src/state/reducers/tile_cache.ts b/app/src/state/reducers/tile_cache.ts index 3557d329a..62b5a38ac 100644 --- a/app/src/state/reducers/tile_cache.ts +++ b/app/src/state/reducers/tile_cache.ts @@ -7,7 +7,7 @@ import { MapDefinitionEligibilityPredicatesBuilder, MapSourceAndLayerDefinition, MapSourceAndLayerDefinitionMode -} from 'UI/LegacyMap/helpers/layer-definitions'; +} from 'UI/LegacyMap/helpers/functional/layer-definitions'; interface TileCacheState { mapSpecifications: MapSourceAndLayerDefinition[]; diff --git a/app/src/state/sagas/map/layer-eligibility.ts b/app/src/state/sagas/map/layer-eligibility.ts index 731e50ed3..e29b6a7ab 100644 --- a/app/src/state/sagas/map/layer-eligibility.ts +++ b/app/src/state/sagas/map/layer-eligibility.ts @@ -3,7 +3,7 @@ import { MAP_DEFINITIONS, MapSourceAndLayerDefinition, MapSourceAndLayerDefinitionMode -} from 'UI/LegacyMap/helpers/layer-definitions'; +} from 'UI/LegacyMap/helpers/functional/layer-definitions'; import { RootState } from 'state/reducers/rootReducer'; import { MOBILE } from 'state/build-time-config'; import MapActions from 'state/actions/map';