diff --git a/app/package-lock.json b/app/package-lock.json index a5f1697b1..6f966871e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -19,7 +19,7 @@ "@capacitor/geolocation": "^6.0.1", "@capacitor/ios": "^6.1.2", "@ionic/pwa-elements": "^3.3.0", - "@mapbox/mapbox-gl-draw": "^1.4.3", + "@mapbox/mapbox-gl-draw": "^1.5.0", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@mui/x-data-grid": "^6.19.11", @@ -43,7 +43,6 @@ "@turf/inside": "^5.0.0", "@turf/line-to-polygon": "^6.5.0", "@turf/turf": "^6.5.0", - "@types/mapbox__mapbox-gl-draw": "^1.4.7", "@types/proj4": "^2.5.5", "@xmldom/xmldom": "^0.8.10", "async": "^3.2.6", @@ -101,6 +100,7 @@ "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.13", "@types/lodash.debounce": "^4.0.9", + "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/node": "^20.11.16", "@types/react": "^18", "@types/react-dom": "^18", @@ -4099,6 +4099,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==", + "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/@mapbox/point-geometry": { @@ -7939,6 +7940,7 @@ "version": "1.4.8", "resolved": "https://registry.npmjs.org/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.4.8.tgz", "integrity": "sha512-700zPikQXfFMB2vtkJdXSROiqS5F19guf6QdYqvlgCdaMxGmdlLITRq6/zFpzfVQDrgpWex5M8vLtbwjZfup8g==", + "dev": true, "license": "MIT", "dependencies": { "@types/geojson": "*", @@ -9982,6 +9984,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", + "devOptional": true, "license": "ISC" }, "node_modules/check-error": { @@ -10960,6 +10963,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "devOptional": true, "license": "MIT" }, "node_modules/cssesc": { @@ -13827,6 +13831,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "devOptional": true, "license": "ISC" }, "node_modules/handlebars": { @@ -17603,6 +17608,7 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.8.0.tgz", "integrity": "sha512-7iQ6wxAf8UedbNYTzNsyr2J25ozIBA4vmKY0xUDXQlHEokulzPENwjjmLxHQGRylDpOmR0c8kPEbtHCaQE2eMw==", + "devOptional": true, "license": "SEE LICENSE IN LICENSE.txt", "workspaces": [ "src/style-spec", @@ -17649,18 +17655,21 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "devOptional": true, "license": "ISC" }, "node_modules/mapbox-gl/node_modules/earcut": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", + "devOptional": true, "license": "ISC" }, "node_modules/mapbox-gl/node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "devOptional": true, "license": "ISC" }, "node_modules/maplibre-gl": { @@ -21808,6 +21817,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz", "integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=4.0.0" diff --git a/app/package.json b/app/package.json index 0bb205fba..e22213a24 100644 --- a/app/package.json +++ b/app/package.json @@ -23,7 +23,7 @@ "@capacitor/geolocation": "^6.0.1", "@capacitor/ios": "^6.1.2", "@ionic/pwa-elements": "^3.3.0", - "@mapbox/mapbox-gl-draw": "^1.4.3", + "@mapbox/mapbox-gl-draw": "^1.5.0", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@mui/x-data-grid": "^6.19.11", @@ -47,7 +47,6 @@ "@turf/inside": "^5.0.0", "@turf/line-to-polygon": "^6.5.0", "@turf/turf": "^6.5.0", - "@types/mapbox__mapbox-gl-draw": "^1.4.7", "@types/proj4": "^2.5.5", "@xmldom/xmldom": "^0.8.10", "async": "^3.2.6", @@ -105,6 +104,7 @@ "@types/geojson": "^7946.0.14", "@types/jest": "^29.5.13", "@types/lodash.debounce": "^4.0.9", + "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/node": "^20.11.16", "@types/react": "^18", "@types/react-dom": "^18", 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/LayerPicker/LayerPicker.tsx b/app/src/UI/LegacyMap/LayerPicker/LayerPicker.tsx index 03a5faaea..5f6439cd4 100644 --- a/app/src/UI/LegacyMap/LayerPicker/LayerPicker.tsx +++ b/app/src/UI/LegacyMap/LayerPicker/LayerPicker.tsx @@ -2,7 +2,6 @@ import { MOBILE } from 'state/build-time-config'; import LayersIcon from '@mui/icons-material/Layers'; import CloseIcon from '@mui/icons-material/Close'; import { IconButton, Switch } from '@mui/material'; -import './LayerPicker.css'; import { useState } from 'react'; import LpModules from 'constants/LpModules'; import LayerPickerPathOption from './LayerPickerPathRow'; @@ -14,6 +13,8 @@ import Accordion from 'UI/Accordion/Accordion'; import { useDispatch, useSelector } from 'utils/use_selector'; import UserSettings from 'state/actions/userSettings/UserSettings'; +import './LayerPicker.css'; + export const LayerPicker = () => { const closeLayerPicker = () => { setShowLayerPicker(false); diff --git a/app/src/UI/LegacyMap/Map.tsx b/app/src/UI/LegacyMap/Map.tsx index d15b52e0a..f8bfaa784 100644 --- a/app/src/UI/LegacyMap/Map.tsx +++ b/app/src/UI/LegacyMap/Map.tsx @@ -1,49 +1,48 @@ -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, removeLayersOnNetworkConnectivityChange -} from 'UI/LegacyMap/helpers/recordset-layers'; -import { addWMSLayersIfNotExist, refreshWMSOnToggle, hideWMSIfUnauthorized } from 'UI/LegacyMap/helpers/wms-layers'; +} from 'UI/LegacyMap/helpers/functional/recordset-layers'; +import { + addWMSLayersIfNotExist, + hideWMSIfUnauthorized, + 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 { CurrentActivityLayer } from 'UI/LegacyMap/helpers/components/CurrentActivityLayer'; +import { DrawControls } from 'UI/LegacyMap/helpers/components/DrawControls'; +import { toggleLayerOnBool } from 'UI/LegacyMap/helpers/functional/utility-functions'; /* @@ -52,20 +51,16 @@ 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, rolesInitialized } = useSelector((state) => state.Auth); const connectedToNetwork = useSelector((state) => state.Network.connected); @@ -84,66 +79,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(); @@ -171,297 +243,84 @@ 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); - }, [storeLayers, map.current, mapReady, connectedToNetwork, loggedInOrWorkingOffline]); + if (!map) return; + rebuildLayersOnTableHashUpdate(storeLayers, map, MapMode, API_BASE, connectedToNetwork); + refreshColoursOnColourUpdate(storeLayers, map); + refreshVisibilityOnToggleUpdate(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; if (!authenticated || !rolesInitialized) { - hideWMSIfUnauthorized(layers, map.current); + hideWMSIfUnauthorized(layers, map); return; } - addWMSLayersIfNotExist(layers, map.current, API_BASE); - refreshWMSOnToggle(layers, map.current); - }, [simplePickerLayers2, map.current, mapReady, baseMapLayer, connectedToNetwork, authenticated, rolesInitialized]); + addWMSLayersIfNotExist(layers, map, API_BASE); + refreshWMSOnToggle(layers, map); + }, [simplePickerLayers2, map, mapReady, baseMapLayer, connectedToNetwork, authenticated, rolesInitialized]); 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]); + }, [map]); // toggle public map pmtile layer useEffect(() => { if (!mapReady) return; - if (!map.current) return; + if (!map) 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); + toggleLayerOnBool(map, 'invasivesbc-pmtile-vector', false); + toggleLayerOnBool(map, 'iapp-pmtile-vector', false); + toggleLayerOnBool(map, 'invasivesbc-pmtile-vector-label', false); + toggleLayerOnBool(map, 'iapp-pmtile-vector-label', false); } - }, [userCoords?.heading, mapReady]); + }, [loggedInOrWorkingOffline, map, mapReady]); return (
@@ -470,30 +329,17 @@ 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/CurrentActivityLayer.tsx b/app/src/UI/LegacyMap/helpers/components/CurrentActivityLayer.tsx new file mode 100644 index 000000000..a3497e0e1 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/CurrentActivityLayer.tsx @@ -0,0 +1,99 @@ +import { useContext, useEffect, useState } from 'react'; +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import { LAYER_Z_FOREGROUND } from 'UI/LegacyMap/helpers/functional/layer-definitions'; +import { useSelector } from 'utils/use_selector'; + +const CurrentActivityLayer = ({ mapReady }) => { + const map = useContext(MapContext); + const [geo, setGeo] = useState(null); + + const activityGeometryArray = useSelector((state) => state.ActivityPage.activity?.geometry); + const { url } = useSelector((state) => state.AppMode); + + // react to changes in the geometry or current page and set our rendered geo appropriately + // render if a) we're on the Activity page and b) There is a geo object in the Activity + useEffect(() => { + if (activityGeometryArray && activityGeometryArray[0] && url?.includes('Activity')) { + setGeo(activityGeometryArray[0]); + } else { + setGeo(null); + } + }, [activityGeometryArray, url]); + + useEffect(() => { + if (!map) return; + if (!mapReady) return; + + // add the layer if needed + + const LAYER_ID = 'current-activity-'; + + const SHAPE_LAYER = `${LAYER_ID}-shape`; + const OUTLINE_LAYER = `${LAYER_ID}-outline`; + const ZOOM_CIRCLE_LAYER = `${LAYER_ID}-zoomoutcircle`; + + if (geo) { + map + .addSource(LAYER_ID, { + type: 'geojson', + data: geo + }) + .addLayer( + { + id: SHAPE_LAYER, + source: LAYER_ID, + type: 'fill', + paint: { + 'fill-color': 'white', + 'fill-outline-color': 'black', + 'fill-opacity': 0.7 + }, + minzoom: 0, + maxzoom: 24 + }, + LAYER_Z_FOREGROUND + ) + .addLayer( + { + id: OUTLINE_LAYER, + source: LAYER_ID, + type: 'line', + paint: { + 'line-color': 'black', + 'line-opacity': 1, + 'line-width': 3 + }, + minzoom: 0, + maxzoom: 24 + }, + LAYER_Z_FOREGROUND + ) + .addLayer( + { + id: ZOOM_CIRCLE_LAYER, + source: LAYER_ID, + type: 'circle', + paint: { + 'circle-color': 'white', + 'circle-radius': 3 + }, + minzoom: 0, + maxzoom: 24 + }, + LAYER_Z_FOREGROUND + ); + + return () => { + // cleanup effect -- remove created entries in reverse + map.removeLayer(ZOOM_CIRCLE_LAYER); + map.removeLayer(OUTLINE_LAYER); + map.removeLayer(SHAPE_LAYER); + map.removeSource(LAYER_ID); + }; + } + }, [geo]); + + return null; +}; + +export { CurrentActivityLayer }; 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..87e57a2b7 --- /dev/null +++ b/app/src/UI/LegacyMap/helpers/components/DrawControls.tsx @@ -0,0 +1,270 @@ +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { MapContext } from 'UI/LegacyMap/helpers/components/MapContext'; +import MapboxDraw 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, Root } 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 drawingCustomLayer = useSelector((state) => state.Map.drawingCustomLayer); + const appModeURL = useSelector((state) => state.AppMode.url); + + const dispatch = useDispatch(); + const drawInstance = useRef(); + const drawModeDisplay = useRef(); + + const uHistory = useHistory(); + + const [mode, setMode] = useState(TargetMode.DISABLED); + + // keep a ref to mode so we don't need to keep re-binding the callback for maplibre. keep it in sync with a hook. + const modeRef = useRef(TargetMode.DISABLED); + + useEffect(() => { + modeRef.current = mode; + }, [mode]); + + const drawCreate = useCallback((event) => { + if (!drawInstance.current) return; + + const currentMode = modeRef.current; + + //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 (currentMode) { + case TargetMode.WHATS_HERE: { + dispatch(WhatsHere.map_feature({ type: 'Feature', geometry: feature.geometry })); + uHistory.push('/WhatsHere'); + break; + } + case TargetMode.ACTIVITY: { + dispatch({ type: MAP_ON_SHAPE_CREATE, payload: feature }); + break; + } + case TargetMode.TILE_CACHE: { + dispatch(TileCache.setTileCacheShape({ geometry: feature.geometry })); + break; + } + case TargetMode.DISABLED: { + drawInstance.current.deleteAll(); + break; + } + 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(() => { + 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); + } else { + setMode(TargetMode.DISABLED); + } + }, [whatsHereToggle, tileCacheMode, drawingCustomLayer, appModeURL]); + + const drawShapeUpdate = useCallback((event) => { + if (!drawInstance.current) return; + + if (!['direct_select', 'simple_select'].includes(drawInstance.current.getMode())) { + // we're not done drawing until we revert to one of these modes + 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 (drawModeDisplay.current) { + drawModeDisplay.current.setMode(mode); + } + + if (drawInstance.current) { + // we changed modes, so reset everything + drawInstance.current.deleteAll(); + + switch (mode) { + case TargetMode.WHATS_HERE: + drawInstance.current.changeMode('whats_here_box_mode'); + break; + case TargetMode.DISABLED: + drawInstance.current.changeMode('do_nothing'); + break; + case TargetMode.ACTIVITY: + drawInstance.current.changeMode('do_nothing'); + break; + default: + break; + } + + //drawInstance.current.changeMode('whats_here_box_mode'); + } + }, [mode]); + + useEffect(() => { + if (!map) { + return; + } + + drawInstance.current = new MapboxDraw({ + displayControlsDefault: true, + controls: { + combine_features: false, + uncombine_features: false + }, + modes: { + ...MapboxDraw.modes, + draw_rectangle: DrawRectangle, + do_nothing: DoNothing, + lots_of_points: LotsOfPointsMode, + whats_here_box_mode: WhatsHereBoxMode + }, + styles: [ + { + id: 'gl-draw-line', + type: 'line', + // filter: ['all', ['==', '$type', 'LineString']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': '#D20C0C', + 'line-dasharray': [0.2, 2], + 'line-width': 3 + } + } + ] + }); + + drawModeDisplay.current = 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.current, '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; + } + + if (drawModeDisplay.current) { + map.removeControl(drawModeDisplay.current); + drawModeDisplay.current = undefined; + } + }; + }, [map]); + + return null; +}; + +class DrawModeDisplay implements IControl { + _text: string; + _map: maplibregl.Map | undefined; + _container: HTMLDivElement | undefined; + + _root: Root | undefined = undefined; + + constructor(mode: TargetMode) { + this._text = mode; + } + + setMode(mode: TargetMode) { + this._text = mode; + this._rerender(); + } + + _rerender() { + if (this._root) { + this._root.render(<>Current drawing mode: {this._text}); + } + } + + onAdd(map: maplibregl.Map): HTMLElement { + this._map = map; + const control = document.createElement('div'); + control.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + + this._root = createRoot(control); + + this._rerender(); + + this._container = control; + + return this._container; + } + + onRemove(_map: maplibregl.Map) { + if (this._root) { + this._root.unmount(); + this._root = undefined; + } + if (this._container?.parentNode) { + this._container.parentNode.removeChild(this._container); + this._container = undefined; + } + } +} + +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 70% rename from app/src/UI/LegacyMap/helpers/current-record.ts rename to app/src/UI/LegacyMap/helpers/functional/current-record.ts index a5605b5e7..163dc8260 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) { @@ -24,40 +24,48 @@ export const refreshCurrentRecMakers = (map, options: any) => { }; export const refreshHighlightedRecord = (map, options: any) => { - const layerID = 'highlightRecordLayer'; - if (map && map.getLayer(layerID + 'shape')) { - map.removeLayer(layerID + 'shape'); + const LAYER_ID = 'highlightRecordLayer'; + + const SHAPE_LAYER = `${LAYER_ID}-shape`; + const OUTLINE_LAYER = `${LAYER_ID}-outline`; + const ZOOM_CIRCLE_LAYER = `${LAYER_ID}-zoomoutcircle`; + + if (!map) { + return; + } + + if (map.getLayer(SHAPE_LAYER)) { + map.removeLayer(SHAPE_LAYER); } - if (map && map.getLayer(layerID + 'outline')) { - map.removeLayer(layerID + 'outline'); + if (map.getLayer(OUTLINE_LAYER)) { + map.removeLayer(OUTLINE_LAYER); } - if (map && map.getLayer(layerID + 'zoomoutcircle')) { - map.removeLayer(layerID + 'zoomoutcircle'); + if (map.getLayer(ZOOM_CIRCLE_LAYER)) { + map.removeLayer(ZOOM_CIRCLE_LAYER); } - if (map && map.getLayer(layerID)) { - map.removeLayer(layerID); + if (map.getLayer(LAYER_ID)) { + map.removeLayer(LAYER_ID); } - if (map && map.getSource(layerID)) { - map.removeSource(layerID); + if (map.getSource(LAYER_ID)) { + map.removeSource(LAYER_ID); } if ( - map && options.userRecordOnHoverRecordType === 'Activity' && options.userRecordOnHoverRecordRow && options.userRecordOnHoverRecordRow?.geometry?.[0] ) { map - .addSource(layerID, { + .addSource(LAYER_ID, { type: 'geojson', data: options.userRecordOnHoverRecordRow.geometry[0] }) .addLayer( { - id: layerID + 'shape', - source: layerID, + id: SHAPE_LAYER, + source: LAYER_ID, type: 'fill', paint: { 'fill-color': 'white', @@ -71,8 +79,8 @@ export const refreshHighlightedRecord = (map, options: any) => { ) .addLayer( { - id: layerID + 'outline', - source: layerID, + id: OUTLINE_LAYER, + source: LAYER_ID, type: 'line', paint: { 'line-color': 'black', @@ -86,8 +94,8 @@ export const refreshHighlightedRecord = (map, options: any) => { ) .addLayer( { - id: layerID + 'zoomoutcircle', - source: layerID, + id: ZOOM_CIRCLE_LAYER, + source: LAYER_ID, type: 'circle', paint: { 'circle-color': 'white', @@ -100,16 +108,16 @@ export const refreshHighlightedRecord = (map, options: any) => { ); } - if (map && options.userRecordOnHoverRecordType === 'IAPP' && options.userRecordOnHoverRecordRow) { + if (options.userRecordOnHoverRecordType === 'IAPP' && options.userRecordOnHoverRecordRow) { map - .addSource(layerID, { + .addSource(LAYER_ID, { type: 'geojson', data: options.userRecordOnHoverRecordRow.geometry }) .addLayer( { - id: layerID, - source: layerID, + id: LAYER_ID, + source: LAYER_ID, type: 'circle', paint: { 'circle-color': 'yellow', 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 8338036f7..a24f3b7ef 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, UserRecordCacheStatus } 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 98% rename from app/src/UI/LegacyMap/helpers/wms-layers.ts rename to app/src/UI/LegacyMap/helpers/functional/wms-layers.ts index ef55b229e..0bfa0c4eb 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'; import { MOBILE } from 'state/build-time-config'; export const addWMSLayersIfNotExist = (simplePickerLayers2: any, map, API_BASE) => { 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.ts b/app/src/state/sagas/map.ts index 4761c2dda..f08280b77 100644 --- a/app/src/state/sagas/map.ts +++ b/app/src/state/sagas/map.ts @@ -1,6 +1,7 @@ -import { bboxPolygon, Feature, buffer } from '@turf/turf'; +import { bboxPolygon, buffer, Feature } from '@turf/turf'; import booleanIntersects from '@turf/boolean-intersects'; import { all, call, debounce, fork, put, select, take, takeEvery, takeLatest } from 'redux-saga/effects'; +import { PayloadAction } from '@reduxjs/toolkit'; import { getSearchCriteriaFromFilters } from '../../utils/miscYankedFromComponents'; import { ACTIVITIES_GEOJSON_GET_ONLINE, @@ -75,7 +76,6 @@ import { RootState } from 'state/reducers/rootReducer'; import TileCache from 'state/actions/cache/TileCache'; import { LAYER_ELIGIBILITY_UPDATE } from 'state/sagas/map/layer-eligibility'; import { RECORD_COLOURS } from 'constants/colors'; -import { PayloadAction } from '@reduxjs/toolkit'; import { IRemoveFilter, IUpdateFilter } from 'state/actions/userSettings/RecordSet'; import { selectNetworkConnected, selectNetworkState } from 'state/reducers/network'; import UserRecord from 'interfaces/UserRecord'; @@ -644,6 +644,7 @@ function* handle_MAP_INIT_FOR_RECORDSETS() { type: string; payload: any; } + const userSettingsState = yield select(selectUserSettings); const recordSets = Object.keys(userSettingsState.recordSets); const mapMode = yield select((state) => state.Map.MapMode); 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';