From 79ec7bb63ef2b022627408aab53dee37ed5b09d1 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 3 Nov 2023 16:03:32 -0400 Subject: [PATCH] refactor(store): Refactor components to use new store, only miss biggest one Closes #1461 --- .../core/components/crosshair/crosshair.tsx | 108 +++++------------- .../core/components/export/export-modal.tsx | 8 +- .../components/footer-tabs/footer-tabs.tsx | 12 +- .../core/components/geolocator/geo-list.tsx | 20 +--- .../components/geolocator/geolocator-style.ts | 15 +++ .../core/components/geolocator/geolocator.tsx | 1 + .../src/core/components/map/map.tsx | 27 +---- .../components/nav-bar/buttons/fullscreen.tsx | 4 +- .../core/components/nav-bar/buttons/home.tsx | 34 +----- .../components/nav-bar/buttons/location.tsx | 30 ++--- .../components/nav-bar/buttons/zoom-in.tsx | 24 +--- .../components/nav-bar/buttons/zoom-out.tsx | 24 +--- .../src/core/containers/focus-trap.tsx | 10 +- .../src/core/containers/shell.tsx | 7 +- .../app-state.ts | 16 ++- .../map-state.ts | 73 +++++++++++- .../geoview-core/src/core/utils/constant.ts | 2 + .../geo/layer/other/cluster-placeholder.ts | 20 ++++ .../src/geo/utils/custom-attribution.ts | 2 +- 19 files changed, 192 insertions(+), 245 deletions(-) create mode 100644 packages/geoview-core/src/geo/layer/other/cluster-placeholder.ts diff --git a/packages/geoview-core/src/core/components/crosshair/crosshair.tsx b/packages/geoview-core/src/core/components/crosshair/crosshair.tsx index 041f913f0dc..cdf9087ba04 100644 --- a/packages/geoview-core/src/core/components/crosshair/crosshair.tsx +++ b/packages/geoview-core/src/core/components/crosshair/crosshair.tsx @@ -1,129 +1,77 @@ -import { useEffect, useRef, useContext } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; -import { toLonLat } from 'ol/proj'; -import { KeyboardPan } from 'ol/interaction'; - -import { getGeoViewStore } from '@/core/stores/stores-managers'; - -import { MapContext } from '@/core/app-start'; - import { Box, Fade, Typography } from '@/ui'; -import { TypeMapMouseInfo } from '@/api/events/payloads'; - import { getSxClasses } from './crosshair-style'; import { CrosshairIcon } from './crosshair-icon'; import { useAppCrosshairsActive } from '@/core/stores/store-interface-and-intial-values/app-state'; -import { useMapCenterCoordinates, useMapElement, useMapProjection } from '@/core/stores/store-interface-and-intial-values/map-state'; +import { useMapElement, useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state'; /** * Create a Crosshair when map is focus with the keyboard so user can click on the map * @returns {JSX.Element} the crosshair component */ export function Crosshair(): JSX.Element { - const mapConfig = useContext(MapContext); - const { mapId } = mapConfig; - const { t } = useTranslation(); const theme = useTheme(); const sxClasses = getSxClasses(theme); // get store values - // tracks if the last action was done through a keyboard (map navigation) or mouse (mouse movement) - const store = getGeoViewStore(mapId); const isCrosshairsActive = useAppCrosshairsActive(); - const projection = useMapProjection(); - const mapCoord = useMapCenterCoordinates(); const mapElement = useMapElement(); - - // use reference as the mapElement from the store is undefined - // TODO: Find what is going on with mapElement for focus-trap and crosshair and crosshair + map coord for this component - // ? maybe because simulate click is in an event listener, it is best to use useRef - const isCrosshairsActiveRef = useRef(isCrosshairsActive); - isCrosshairsActiveRef.current = isCrosshairsActive; - const mapCoordRef = useRef(mapCoord); - mapCoordRef.current = mapCoord; + const { setClickCoordinates, setMapKeyboardPanInteractions } = useMapStoreActions(); // do not use useState for item used inside function only without rendering... use useRef const panelButtonId = useRef(''); - - let panDelta = 128; + const panDelta = useRef(128); /** * Simulate map mouse click to trigger details panel * @function simulateClick - * @param {KeyboardEvent} evt the keyboard event + * @param {KeyboardEvent} event the keyboard event */ - function simulateClick(evt: KeyboardEvent): void { - if (evt.key === 'Enter') { - if (isCrosshairsActiveRef.current) { - // updater store with the lnglat point - const mapClickCoordinatesFetch: TypeMapMouseInfo = { - projected: [0, 0], - pixel: [0, 0], - lnglat: toLonLat(mapCoordRef.current, `EPSG:${projection}`), - dragging: false, - }; - store.setState({ - mapState: { ...store.getState().mapState, clickCoordinates: mapClickCoordinatesFetch }, - }); - } + const simulateClick = useCallback((event: KeyboardEvent) => { + if (event.key === 'Enter') { + setClickCoordinates(); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); /** * Modify the pixelDelta value for the keyboard pan on Shift arrow up or down * - * @param {KeyboardEvent} evt the keyboard event to trap + * @param {KeyboardEvent} event the keyboard event to trap */ - function managePanDelta(evt: KeyboardEvent): void { - if ((evt.key === 'ArrowDown' && evt.shiftKey) || (evt.key === 'ArrowUp' && evt.shiftKey)) { - panDelta = evt.key === 'ArrowDown' ? (panDelta -= 10) : (panDelta += 10); - panDelta = panDelta < 10 ? 10 : panDelta; // minus panDelta reset the value so we need to trap + const managePanDelta = useCallback((event: KeyboardEvent) => { + if ((event.key === 'ArrowDown' && event.shiftKey) || (event.key === 'ArrowUp' && event.shiftKey)) { + panDelta.current = event.key === 'ArrowDown' ? (panDelta.current -= 10) : (panDelta.current += 10); + panDelta.current = panDelta.current < 10 ? 10 : panDelta.current; // minus panDelta reset the value so we need to trap - // replace the KeyboardPan interraction by a new one - // const mapElement = mapElementRef.current; - mapElement.getInteractions().forEach((interactionItem) => { - if (interactionItem instanceof KeyboardPan) { - mapElement.removeInteraction(interactionItem); - } - }); - mapElement.addInteraction(new KeyboardPan({ pixelDelta: panDelta })); + setMapKeyboardPanInteractions(panDelta.current); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { - const unsubIsCrosshair = getGeoViewStore(mapId).subscribe( - (state) => state.appState.isCrosshairsActive, - (curCrosshair, prevCrosshair) => { - if (curCrosshair !== prevCrosshair) { - const mapHTMLElement = mapElement.getTargetElement(); - - if (curCrosshair) { - panelButtonId.current = 'detailsPanel'; - - mapHTMLElement.addEventListener('keydown', simulateClick); - mapHTMLElement.addEventListener('keydown', managePanDelta); - } else { - mapHTMLElement.removeEventListener('keydown', simulateClick); - mapHTMLElement.removeEventListener('keydown', managePanDelta); - } - } - } - ); + const mapHTMLElement = mapElement.getTargetElement(); + if (isCrosshairsActive) { + panelButtonId.current = 'detailsPanel'; + mapHTMLElement.addEventListener('keydown', simulateClick); + mapHTMLElement.addEventListener('keydown', managePanDelta); + } else { + mapHTMLElement.removeEventListener('keydown', simulateClick); + mapHTMLElement.removeEventListener('keydown', managePanDelta); + } return () => { - const mapHTMLElement = mapElement.getTargetElement(); - unsubIsCrosshair(); mapHTMLElement.removeEventListener('keydown', simulateClick); mapHTMLElement.removeEventListener('keydown', managePanDelta); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [isCrosshairsActive, mapElement, simulateClick, managePanDelta]); return (
void; }; -const sxClasses = { - main: { - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - '& span': { - fontSize: '0.75rem', - ':first-of-type': { - fontWeight: 'bold', - fontSize: '0.875rem', - }, - }, - }, -}; - type tooltipProp = Pick; /** @@ -70,7 +56,7 @@ export default function GeoList({ geoListItems, zoomToLocation }: GeoListProps) zoomToLocation([geoListItem.lng, geoListItem.lat], geoListItem.bbox)}> - + {geoListItem.name} {!!geoListItem.tag && geoListItem.tag.length && !!geoListItem.tag[0] && ( {`, ${geoListItem.tag[0]}`} @@ -82,7 +68,7 @@ export default function GeoList({ geoListItems, zoomToLocation }: GeoListProps) {!!geoListItem.tag && geoListItem.tag.length > 1 && !!geoListItem.tag[1] && ( - + {geoListItem.tag[1]} )} diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts b/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts index 86c8910417a..7339cc25b75 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts +++ b/packages/geoview-core/src/core/components/geolocator/geolocator-style.ts @@ -40,6 +40,21 @@ export const sxClasses = { }, }; +export const sxClassesList = { + main: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + '& span': { + fontSize: '0.75rem', + ':first-of-type': { + fontWeight: 'bold', + fontSize: '0.875rem', + }, + }, + }, +}; + export const StyledInputField = styled(Input)(({ theme }) => ({ color: 'inherit', width: '100%', diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index d8c1f7c185d..85e614e1187 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -38,6 +38,7 @@ export function Geolocator() { const urlRef = useRef(`${serviceUrls!.geolocator}&lang=${i18n.language}`); + // internal state const [data, setData] = useState(); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(false); diff --git a/packages/geoview-core/src/core/components/map/map.tsx b/packages/geoview-core/src/core/components/map/map.tsx index 89852bfe56f..689a7778476 100644 --- a/packages/geoview-core/src/core/components/map/map.tsx +++ b/packages/geoview-core/src/core/components/map/map.tsx @@ -13,9 +13,7 @@ import { Extent } from 'ol/extent'; import { Box, useMediaQuery } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { useStore } from 'zustand'; import { XYZ } from 'ol/source'; -import { getGeoViewStore } from '@/core/stores/stores-managers'; import { NorthArrow, NorthPoleFlag } from '@/core/components/north-arrow/north-arrow'; import { Crosshair } from '@/core/components/crosshair/crosshair'; @@ -33,6 +31,7 @@ import { MapViewer } from '@/geo/map/map-viewer'; import { payloadIsABasemapLayerArray, payloadIsAMapViewProjection, PayloadBaseClass } from '@/api/events/payloads'; import { TypeBasemapProps, TypeMapFeaturesConfig } from '../../types/global-types'; import { sxClasses } from './map-style'; +import { useMapLoaded, useMapNorthArrow, useMapOverviewMap } from '@/core/stores/store-interface-and-intial-values/map-state'; export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { const { map: mapConfig } = mapFeaturesConfig; @@ -47,9 +46,9 @@ export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { const [overviewBaseMap, setOverviewBaseMap] = useState(undefined); // get values from the store - const overviewMap = useStore(getGeoViewStore(mapId), (state) => state.mapState.overviewMap); - const northArrow = useStore(getGeoViewStore(mapId), (state) => state.mapState.northArrow); - const mapLoaded = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapLoaded); + const overviewMap = useMapOverviewMap(); + const northArrow = useMapNorthArrow(); + const mapLoaded = useMapLoaded(); // create a new map viewer instance const viewer: MapViewer = api.maps[mapId]; @@ -59,24 +58,6 @@ export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { // if screen size is medium and up const deviceSizeMedUp = useMediaQuery(defaultTheme.breakpoints.up('md')); - // TODO: do not deal with stuff not related to create the payload in the event, use the event on or store state to listen to change and do what is needed. - // !This was in mapZoomEnd event.... listen to the event in proper place - // Object.keys(layers).forEach((layer) => { - // if (layer.endsWith('-unclustered')) { - // const clusterLayerId = layer.replace('-unclustered', ''); - // const splitZoom = - // (api.maps[mapId].layer.registeredLayers[clusterLayerId].source as TypeVectorSourceInitialConfig)!.cluster!.splitZoom || 7; - // if (prevZoom < splitZoom && currentZoom >= splitZoom) { - // api.maps[mapId].layer.registeredLayers[clusterLayerId]?.olLayer!.setVisible(false); - // api.maps[mapId].layer.registeredLayers[layer]?.olLayer!.setVisible(true); - // } - // if (prevZoom >= splitZoom && currentZoom < splitZoom) { - // api.maps[mapId].layer.registeredLayers[clusterLayerId]?.olLayer!.setVisible(true); - // api.maps[mapId].layer.registeredLayers[layer]?.olLayer!.setVisible(false); - // } - // } - // }); - const initCGPVMap = (cgpvMap: OLMap) => { cgpvMap.set('mapId', mapId); diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/fullscreen.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/fullscreen.tsx index 6f48564f07b..ba03a4c292a 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/fullscreen.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/fullscreen.tsx @@ -3,7 +3,6 @@ import { useContext } from 'react'; import { useTheme } from '@mui/material/styles'; import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; import { IconButton, FullscreenIcon, FullscreenExitIcon } from '@/ui'; import { TypeHTMLElement } from '@/core/types/global-types'; import { getSxClasses } from '../nav-bar-style'; @@ -50,8 +49,7 @@ export default function Fullscreen(): JSX.Element { const element = document.getElementById(`shell-${mapId}`); if (element) { - setFullScreenActive(!isFullScreen); - api.maps[mapId].toggleFullscreen(!isFullScreen, element as TypeHTMLElement); + setFullScreenActive(!isFullScreen, element as TypeHTMLElement); // if state will become fullscreen, add event listerner to trap exit by ESC key // put a timeout for the toggle to fullscreen to happen diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/home.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/home.tsx index 33e44519278..488b79b79f4 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/home.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/home.tsx @@ -1,17 +1,8 @@ -import { useContext } from 'react'; - import { useTheme } from '@mui/material/styles'; -import { fromLonLat } from 'ol/proj'; -import { Extent } from 'ol/extent'; -import { FitOptions } from 'ol/View'; - -import { getGeoViewStore } from '@/core/stores/stores-managers'; - -import { MapContext } from '@/core/app-start'; import { IconButton, HomeIcon } from '@/ui'; -import { api } from '@/app'; import { getSxClasses } from '../nav-bar-style'; +import { useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state'; /** * Create a home button to return the user to the map center @@ -19,31 +10,14 @@ import { getSxClasses } from '../nav-bar-style'; * @returns {JSX.Element} the created home button */ export default function Home(): JSX.Element { - const mapConfig = useContext(MapContext); - const { mapId } = mapConfig; - const theme = useTheme(); const sxClasses = getSxClasses(theme); - /** - * Return user to map initial center - */ - function setHome() { - // get map and set initial bounds to use in zoom home - const store = getGeoViewStore(mapId); - const { center, zoom } = store.getState().mapConfig!.map.viewSettings; - const projectionConfig = api.projection.projections[store.getState().mapState.currentProjection]; - - const projectedCoords = fromLonLat(center, projectionConfig); - const extent: Extent = [...projectedCoords, ...projectedCoords]; - - const options: FitOptions = { padding: [100, 100, 100, 100], maxZoom: zoom, duration: 500 }; - - api.maps[mapId].zoomToExtent(extent, options); - } + // get store actions + const { zoomToInitialExtent } = useMapStoreActions(); return ( - setHome()} sx={sxClasses.navButton}> + zoomToInitialExtent()} sx={sxClasses.navButton}> ); diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/location.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/location.tsx index 37304245001..fc73028bbe0 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/location.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/location.tsx @@ -2,16 +2,11 @@ import { useContext } from 'react'; import { useTheme } from '@mui/material/styles'; -import { fromLonLat } from 'ol/proj'; -import { Extent } from 'ol/extent'; -import { FitOptions } from 'ol/View'; - -// import { getGeoViewStore } from '@/core/stores/stores-managers'; - import { MapContext } from '@/core/app-start'; import { IconButton, EmojiPeopleIcon } from '@/ui'; -import { Coordinate, api } from '@/app'; +import { api } from '@/app'; import { getSxClasses } from '../nav-bar-style'; +import { useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state'; /** * Create a location button to zoom to user location @@ -25,28 +20,19 @@ export default function Location(): JSX.Element { const theme = useTheme(); const sxClasses = getSxClasses(theme); + // get store actions + const { zoomToMyLocation } = useMapStoreActions(); + /** * Zoom to user location */ function zoomToMe() { - let coordinates: Coordinate = []; - function success(pos: GeolocationPosition) { - coordinates = [pos.coords.longitude, pos.coords.latitude]; - - const { currentProjection } = api.maps[mapId]; - const projectionConfig = api.projection.projections[currentProjection]; - - const projectedCoords = fromLonLat(coordinates, projectionConfig); - const extent: Extent = [...projectedCoords, ...projectedCoords]; - - const options: FitOptions = { padding: [100, 100, 100, 100], maxZoom: 13, duration: 500 }; - - api.maps[mapId].zoomToExtent(extent, options); + function success(position: GeolocationPosition) { + zoomToMyLocation(position); } function error(err: GeolocationPositionError) { - // eslint-disable-next-line no-console - console.warn(`ERROR(${err.code}): ${err.message}`); + api.utilities.showWarning(mapId, `ERROR(${err.code}): ${err.message}`, true); } navigator.geolocation.getCurrentPosition(success, error); diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-in.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-in.tsx index 7e7fdd2162c..984221dc75f 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-in.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-in.tsx @@ -1,13 +1,8 @@ -import { useContext } from 'react'; - import { useTheme } from '@mui/material/styles'; -import { getGeoViewStore } from '@/core/stores/stores-managers'; - -import { MapContext } from '@/core/app-start'; import { IconButton, ZoomInIcon } from '@/ui'; import { getSxClasses } from '../nav-bar-style'; -import { OL_ZOOM_DURATION } from '@/core/utils/constant'; +import { useMapStoreActions, useMapZoom } from '@/core/stores/store-interface-and-intial-values/map-state'; /** * Create a zoom in button @@ -15,24 +10,15 @@ import { OL_ZOOM_DURATION } from '@/core/utils/constant'; * @returns {JSX.Element} return the created zoom in button */ export default function ZoomIn(): JSX.Element { - const mapConfig = useContext(MapContext); - const { mapId } = mapConfig; - const theme = useTheme(); const sxClasses = getSxClasses(theme); - /** - * Causes the map to zoom in - */ - function zoomIn() { - const currentZoom = getGeoViewStore(mapId).getState().mapState.zoom; - const { mapElement } = getGeoViewStore(mapId).getState().mapState; - - if (currentZoom) mapElement.getView().animate({ zoom: currentZoom + 0.5, duration: OL_ZOOM_DURATION }); - } + // get store values + const zoom = useMapZoom(); + const { setZoom } = useMapStoreActions(); return ( - zoomIn()} sx={sxClasses.navButton}> + setZoom(zoom + 0.5)} sx={sxClasses.navButton}> ); diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-out.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-out.tsx index d52b53fef16..df4a559505d 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-out.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/zoom-out.tsx @@ -1,13 +1,8 @@ -import { useContext } from 'react'; - import { useTheme } from '@mui/material/styles'; -import { getGeoViewStore } from '@/core/stores/stores-managers'; - import { IconButton, ZoomOutIcon } from '@/ui'; -import { MapContext } from '@/core/app-start'; import { getSxClasses } from '../nav-bar-style'; -import { OL_ZOOM_DURATION } from '@/core/utils/constant'; +import { useMapStoreActions, useMapZoom } from '@/core/stores/store-interface-and-intial-values/map-state'; /** * Create a zoom out button @@ -15,24 +10,15 @@ import { OL_ZOOM_DURATION } from '@/core/utils/constant'; * @returns {JSX.Element} return the new created zoom out button */ export default function ZoomOut(): JSX.Element { - const mapConfig = useContext(MapContext); - const { mapId } = mapConfig; - const theme = useTheme(); const sxClasses = getSxClasses(theme); - /** - * Causes the map to zoom out - */ - function zoomOut() { - const currentZoom = getGeoViewStore(mapId).getState().mapState.zoom; - const { mapElement } = getGeoViewStore(mapId).getState().mapState; - - if (currentZoom) mapElement.getView().animate({ zoom: currentZoom - 0.5, duration: OL_ZOOM_DURATION }); - } + // get store values + const zoom = useMapZoom(); + const { setZoom } = useMapStoreActions(); return ( - zoomOut()} sx={sxClasses.navButton}> + setZoom(zoom - 0.5)} sx={sxClasses.navButton}> ); diff --git a/packages/geoview-core/src/core/containers/focus-trap.tsx b/packages/geoview-core/src/core/containers/focus-trap.tsx index 0e9a7c720e7..e0ee8c2c956 100644 --- a/packages/geoview-core/src/core/containers/focus-trap.tsx +++ b/packages/geoview-core/src/core/containers/focus-trap.tsx @@ -5,14 +5,12 @@ import { useTranslation } from 'react-i18next'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { useStore } from 'zustand'; -import { getGeoViewStore } from '@/core/stores/stores-managers'; - import { Modal, Button } from '@/ui'; import { HtmlToReact } from './html-to-react'; import { getFocusTrapSxClasses } from './containers-style'; import { disableScrolling } from '@/app'; import { useAppStoreActions } from '../stores/store-interface-and-intial-values/app-state'; +import { useMapElement } from '../stores/store-interface-and-intial-values/map-state'; /** * Interface for the focus trap properties @@ -44,12 +42,10 @@ export function FocusTrapDialog(props: FocusTrapProps): JSX.Element { // get store values // tracks if the last action was done through a keyboard (map navigation) or mouse (mouse movement) - const store = getGeoViewStore(mapId); const { setCrosshairActive } = useAppStoreActions(); - const mapElementStore = useStore(store, (state) => state.mapState.mapElement); + const mapElementStore = useMapElement(); - // ? useRef, if not mapElementStore is undefined - may be because this component is created before the mapElement - // TODO: Find what is going on with mapElement for focus-trap and crosshair + // ? useRef, if not mapElementStore is undefined - happen because the value is used inside a event listener const mapElementRef = useRef(mapElementStore); mapElementRef.current = mapElementStore; diff --git a/packages/geoview-core/src/core/containers/shell.tsx b/packages/geoview-core/src/core/containers/shell.tsx index 02c672f1f9a..054714bd5bf 100644 --- a/packages/geoview-core/src/core/containers/shell.tsx +++ b/packages/geoview-core/src/core/containers/shell.tsx @@ -7,9 +7,6 @@ import { useTheme } from '@mui/material/styles'; import FocusTrap from 'focus-trap-react'; -import { useStore } from 'zustand'; -import { getGeoViewStore } from '@/core/stores/stores-managers'; - import { Map } from '@/core/components/map/map'; import { Appbar } from '@/core/components/app-bar/app-bar'; import { Navbar } from '@/core/components/nav-bar/nav-bar'; @@ -32,6 +29,7 @@ import { } from '@/api/events/payloads'; import { MapContext } from '@/core/app-start'; import { getShellSxClasses } from './containers-style'; +import { useMapLoaded } from '../stores/store-interface-and-intial-values/map-state'; /** * Interface for the shell properties @@ -58,12 +56,13 @@ export function Shell(props: ShellProps): JSX.Element { // internal component state // set the active trap value for FocusTrap and pass the callback to the dialog window const [activeTrap, setActivetrap] = useState(false); + // render additional components if added by api const [components, setComponents] = useState>({}); const [update, setUpdate] = useState(0); // get values from the store - const mapLoaded = useStore(getGeoViewStore(mapFeaturesConfig.mapId), (state) => state.mapState.mapLoaded); + const mapLoaded = useMapLoaded(); /** * Set the focus trap diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/app-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/app-state.ts index 0c578bba58a..f549a26eb47 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/app-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/app-state.ts @@ -2,7 +2,8 @@ import { useStore } from 'zustand'; import { useGeoViewStore } from '@/core/stores/stores-managers'; import { TypeSetStore, TypeGetStore } from '@/core/stores/geoview-store'; -import { NotificationDetailsType } from '@/core/types/cgpv-types'; +import { NotificationDetailsType, TypeHTMLElement } from '@/core/types/cgpv-types'; +import { api } from '@/app'; export interface IAppState { isCrosshairsActive: boolean; @@ -11,7 +12,7 @@ export interface IAppState { actions: { setCrosshairActive: (active: boolean) => void; - setFullScreenActive: (active: boolean) => void; + setFullScreenActive: (active: boolean, element?: TypeHTMLElement) => void; addNotification: (notif: NotificationDetailsType) => void; removeNotification: (key: string) => void; }; @@ -24,21 +25,24 @@ export function initializeAppState(set: TypeSetStore, get: TypeGetStore) { notifications: [], actions: { - setCrosshairActive: (isCrosshairsActive: boolean) => { + setCrosshairActive: (active: boolean) => { set({ appState: { ...get().appState, - isCrosshairsActive, + isCrosshairsActive: active, }, }); }, - setFullScreenActive: (isFullscreenActive: boolean) => { + setFullScreenActive: (active: boolean, element?: undefined) => { set({ appState: { ...get().appState, - isFullscreenActive, + isFullscreenActive: active, }, }); + + // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion + if (element !== undefined) api.maps[get().mapId].toggleFullscreen(active, element); }, addNotification: (notif: NotificationDetailsType) => { set({ diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts index 481b73c784d..0b4e4133ce0 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts @@ -3,8 +3,11 @@ import debounce from 'lodash/debounce'; import { Map as OLMap, MapEvent, MapBrowserEvent, View } from 'ol'; import { Coordinate } from 'ol/coordinate'; import { ObjectEvent } from 'ol/Object'; -import { toLonLat } from 'ol/proj'; +import { fromLonLat, toLonLat } from 'ol/proj'; import Overlay from 'ol/Overlay'; +import { KeyboardPan } from 'ol/interaction'; +import { Extent } from 'ol/extent'; +import { FitOptions } from 'ol/View'; import { useStore } from 'zustand'; import { useGeoViewStore } from '@/core/stores/stores-managers'; @@ -13,6 +16,8 @@ import { TypeSetStore, TypeGetStore } from '@/core/stores/geoview-store'; import { TypeValidMapProjectionCodes } from '@/core/types/global-types'; import { TypeMapMouseInfo } from '@/api/events/payloads'; import { TypeInteraction } from '@/geo/map/map-schema-types'; +import { OL_ZOOM_DURATION, OL_ZOOM_PADDING } from '@/core/utils/constant'; +import { api } from '@/app'; interface TypeScaleInfo { lineWidth: string; @@ -35,7 +40,7 @@ export interface IMapState { overviewMapHideZoom: number; rotation: number; scale: TypeScaleInfo; - zoom?: number | undefined; + zoom: number; onMapMoveEnd: (event: MapEvent) => void; onMapPointerMove: (event: MapEvent) => void; @@ -44,11 +49,16 @@ export interface IMapState { onMapZoomEnd: (event: ObjectEvent) => void; actions: { + setClickCoordinates: () => void; setFixNorth: (ifFix: boolean) => void; setMapElement: (mapElem: OLMap) => void; + setMapKeyboardPanInteractions: (panDelta: number) => void; setOverlayNorthMarker: (overlay: Overlay) => void; setOverlayNorthMarkerRef: (htmlRef: HTMLElement) => void; setRotation: (degree: number) => void; + setZoom: (zoom: number) => void; + zoomToInitialExtent: () => void; + zoomToMyLocation: (position: GeolocationPosition) => void; }; } @@ -71,7 +81,7 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore) { pointerPosition: undefined, rotation: 0, scale: { lineWidth: '', labelGraphic: '', labelNumeric: '' }, - zoom: undefined, + zoom: 0, onMapMoveEnd: debounce((event: MapEvent) => { const coords = event.map.getView().getCenter()!; @@ -150,6 +160,14 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore) { }, 100), actions: { + setClickCoordinates: () => { + set({ + mapState: { + ...get().mapState, + clickCoordinates: get().mapState.pointerPosition, // trigger click event from pointer position + }, + }); + }, setFixNorth: (isFix: boolean) => { set({ mapState: { @@ -165,10 +183,22 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore) { mapLoaded: true, mapElement: mapElem, scale: setScale(get().mapId), - zoom: mapElem.getView().getZoom(), + zoom: mapElem.getView().getZoom() as number, }, }); }, + setMapKeyboardPanInteractions: (panDelta: number) => { + const { mapElement } = get().mapState; + + // replace the KeyboardPan interraction by a new one + // const mapElement = mapElementRef.current; + mapElement.getInteractions().forEach((interactionItem) => { + if (interactionItem instanceof KeyboardPan) { + mapElement.removeInteraction(interactionItem); + } + }); + mapElement.addInteraction(new KeyboardPan({ pixelDelta: panDelta })); + }, setOverlayNorthMarker: (overlay: Overlay) => { set({ mapState: { @@ -190,7 +220,37 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore) { }); // set ol map rotation - get().mapState.mapElement.getView().animate({ rotation: 0 }); + get().mapState.mapElement.getView().animate({ rotation: degree }); + }, + setZoom: (zoom: number) => { + set({ + mapState: { + ...get().mapState, + zoom, + }, + }); + + get().mapState.mapElement.getView().animate({ zoom, duration: OL_ZOOM_DURATION }); + }, + zoomToInitialExtent: () => { + const { center, zoom } = get().mapConfig!.map.viewSettings; + const projectedCoords = fromLonLat(center, `EPSG:${get().mapState.currentProjection}`); + const extent: Extent = [...projectedCoords, ...projectedCoords]; + const options: FitOptions = { padding: OL_ZOOM_PADDING, maxZoom: zoom, duration: OL_ZOOM_DURATION }; + + // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion + api.maps[get().mapId].zoomToExtent(extent, options); + }, + zoomToMyLocation: (position: GeolocationPosition) => { + const projectedCoords = fromLonLat( + [position.coords.longitude, position.coords.latitude], + `EPSG:${get().mapState.currentProjection}` + ); + const extent: Extent = [...projectedCoords, ...projectedCoords]; + const options: FitOptions = { padding: OL_ZOOM_PADDING, maxZoom: 13, duration: OL_ZOOM_DURATION }; + + // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion + api.maps[get().mapId].zoomToExtent(extent, options); }, }, }; @@ -206,10 +266,13 @@ export const useMapProjection = () => useStore(useGeoViewStore(), (state) => sta export const useMapElement = () => useStore(useGeoViewStore(), (state) => state.mapState.mapElement); export const useMapFixNorth = () => useStore(useGeoViewStore(), (state) => state.mapState.fixNorth); export const useMapInteraction = () => useStore(useGeoViewStore(), (state) => state.mapState.interaction); +export const useMapLoaded = () => useStore(useGeoViewStore(), (state) => state.mapState.mapLoaded); export const useMapNorthArrow = () => useStore(useGeoViewStore(), (state) => state.mapState.northArrow); export const useMapOverlayNorthMarker = () => useStore(useGeoViewStore(), (state) => state.mapState.overlayNorthMarker); +export const useMapOverviewMap = () => useStore(useGeoViewStore(), (state) => state.mapState.overviewMap); export const useMapPointerPosition = () => useStore(useGeoViewStore(), (state) => state.mapState.pointerPosition); export const useMapRotation = () => useStore(useGeoViewStore(), (state) => state.mapState.rotation); export const useMapScale = () => useStore(useGeoViewStore(), (state) => state.mapState.scale); +export const useMapZoom = () => useStore(useGeoViewStore(), (state) => state.mapState.zoom); export const useMapStoreActions = () => useStore(useGeoViewStore(), (state) => state.mapState.actions); diff --git a/packages/geoview-core/src/core/utils/constant.ts b/packages/geoview-core/src/core/utils/constant.ts index 03ba13003b4..2f38423e81c 100644 --- a/packages/geoview-core/src/core/utils/constant.ts +++ b/packages/geoview-core/src/core/utils/constant.ts @@ -13,3 +13,5 @@ export const OL_ZOOM_DURATION = 500; // The north pole position use for north arrow marker and get north arrow rotation angle // north value (set longitude to be half of Canada extent (142° W, 52° W)) - projection central meridian is -95 export const NORTH_POLE_POSITION: [number, number] = [90, -95]; + +export const OL_ZOOM_PADDING: [number, number, number, number] = [100, 100, 100, 100]; diff --git a/packages/geoview-core/src/geo/layer/other/cluster-placeholder.ts b/packages/geoview-core/src/geo/layer/other/cluster-placeholder.ts new file mode 100644 index 00000000000..ed70e22c40c --- /dev/null +++ b/packages/geoview-core/src/geo/layer/other/cluster-placeholder.ts @@ -0,0 +1,20 @@ +// src/core/components/map/map.tsx +// TODO: do not deal with stuff not related to create the payload in the event, use the event on or store state to listen to change and do what is needed. +// !This was in mapZoomEnd event.... listen to the event in proper place +// Object.keys(layers).forEach((layer) => { +// if (layer.endsWith('-unclustered')) { +// const clusterLayerId = layer.replace('-unclustered', ''); +// const splitZoom = +// (api.maps[mapId].layer.registeredLayers[clusterLayerId].source as TypeVectorSourceInitialConfig)!.cluster!.splitZoom || 7; +// if (prevZoom < splitZoom && currentZoom >= splitZoom) { +// api.maps[mapId].layer.registeredLayers[clusterLayerId]?.olLayer!.setVisible(false); +// api.maps[mapId].layer.registeredLayers[layer]?.olLayer!.setVisible(true); +// } +// if (prevZoom >= splitZoom && currentZoom < splitZoom) { +// api.maps[mapId].layer.registeredLayers[clusterLayerId]?.olLayer!.setVisible(true); +// api.maps[mapId].layer.registeredLayers[layer]?.olLayer!.setVisible(false); +// } +// } +// }); + +export {}; diff --git a/packages/geoview-core/src/geo/utils/custom-attribution.ts b/packages/geoview-core/src/geo/utils/custom-attribution.ts index 4bd5c562646..d45746ccc6f 100644 --- a/packages/geoview-core/src/geo/utils/custom-attribution.ts +++ b/packages/geoview-core/src/geo/utils/custom-attribution.ts @@ -53,4 +53,4 @@ export class CustomAttribution extends OLAttribution { } } } -} \ No newline at end of file +}