From fe129551473fed0822cc15f4fd20b3db2979c122 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Thu, 28 Sep 2023 08:25:07 -0400 Subject: [PATCH] refactor(store): Refactor north arrow to use the store and remove api from footer element Closes #1329 --- .../public/templates/default-config.html | 4 +- .../public/templates/projections.html | 4 +- .../src/api/eventProcessors/index.ts | 11 +- .../api/eventProcessors/map-event-process.ts | 45 ++++- .../notification-event-process.ts | 33 +++ .../src/api/events/constants/notifications.ts | 7 +- .../events/payloads/notification-payload.ts | 2 +- .../src/core/components/app-bar/app-bar.tsx | 2 +- .../app-bar/buttons/notifications.tsx | 189 ------------------ .../components/attribution/attribution.tsx | 10 +- .../components/click-marker/click-marker.tsx | 7 +- .../core/components/crosshair/crosshair.tsx | 50 +++-- .../core/components/data-table/data-panel.tsx | 2 +- .../footer-bar/footer-bar-expand-button.tsx | 15 +- .../footer-bar/footer-bar-fixnorth-switch.tsx | 46 +---- .../footer-bar/footer-bar-rotation-button.tsx | 90 +-------- .../components/footer-bar/footer-bar-style.ts | 63 ++++++ .../core/components/footer-bar/footer-bar.tsx | 57 ++---- .../geoview-core/src/core/components/index.ts | 3 +- .../src/core/components/map/map.tsx | 53 +++-- .../components/north-arrow/north-arrow.tsx | 157 ++++++--------- .../notifications/notifications-api.ts | 85 -------- .../notifications/notifications-style.ts | 19 ++ .../notifications/notifications.tsx | 143 +++++++++++++ .../overview-map/overview-map-toggle.tsx | 1 + .../components/overview-map/overview-map.tsx | 92 ++++----- .../src/core/components/scale/scale.tsx | 21 +- .../src/core/containers/shell.tsx | 18 +- .../src/core/stores/geoview-store.ts | 59 ++++-- .../src/core/stores/stores-managers.ts | 13 ++ packages/geoview-core/src/geo/map/map.ts | 20 +- .../circular-progress-style.ts | 27 +++ .../circular-progress/circular-progress.tsx | 18 +- 33 files changed, 657 insertions(+), 709 deletions(-) create mode 100644 packages/geoview-core/src/api/eventProcessors/notification-event-process.ts delete mode 100644 packages/geoview-core/src/core/components/app-bar/buttons/notifications.tsx create mode 100644 packages/geoview-core/src/core/components/footer-bar/footer-bar-style.ts delete mode 100644 packages/geoview-core/src/core/components/notifications/notifications-api.ts create mode 100644 packages/geoview-core/src/core/components/notifications/notifications-style.ts create mode 100644 packages/geoview-core/src/core/components/notifications/notifications.tsx create mode 100644 packages/geoview-core/src/ui/circular-progress/circular-progress-style.ts diff --git a/packages/geoview-core/public/templates/default-config.html b/packages/geoview-core/public/templates/default-config.html index 9c5f72631de..d590fe601c4 100644 --- a/packages/geoview-core/public/templates/default-config.html +++ b/packages/geoview-core/public/templates/default-config.html @@ -172,7 +172,7 @@

4. Load layers with bad config values

} ] }, - 'components': ['overview-map', 'nav-bar'], + 'components': ['overview-map'], 'corePackages': [], 'theme': 'dark', 'suportedLanguages': ['en'] @@ -268,7 +268,7 @@

7. Load config from function call

] }] }, - 'components': ['app-bar', 'overview-map', 'nav-bar'], + 'components': ['overview-map', 'nav-bar'], 'corePackages': [], 'theme': 'dark', 'suportedLanguages': ['en'] diff --git a/packages/geoview-core/public/templates/projections.html b/packages/geoview-core/public/templates/projections.html index a0ac1726e76..5dc9078be15 100644 --- a/packages/geoview-core/public/templates/projections.html +++ b/packages/geoview-core/public/templates/projections.html @@ -68,7 +68,7 @@

1. Web Mercator Projection

'labeled': true } }, - 'components': ['overview-map'], + 'components': ['overview-map', 'north-arrow'], 'corePackages': [], 'theme': 'dark', 'suportedLanguages': ['en'] @@ -103,7 +103,7 @@

2. LCC Projection

'labeled': true } }, - 'components': ['overview-map'], + 'components': ['overview-map', 'north-arrow'], 'corePackages': [], 'theme': 'dark', 'suportedLanguages': ['en'] diff --git a/packages/geoview-core/src/api/eventProcessors/index.ts b/packages/geoview-core/src/api/eventProcessors/index.ts index 4ff6b15d911..8ebb65352e5 100644 --- a/packages/geoview-core/src/api/eventProcessors/index.ts +++ b/packages/geoview-core/src/api/eventProcessors/index.ts @@ -1,15 +1,20 @@ import { GeoViewStoreType } from '@/core/stores/geoview-store'; -import { AppBarEventProcessor } from './appBar-event-process'; -import { MapEventProcessor } from './map-event-process'; +import { AppBarEventProcessor } from '@/api/eventProcessors/appBar-event-process'; +import { MapEventProcessor } from '@/api/eventProcessors/map-event-process'; +import { NotificationEventProcessor } from '@/api/eventProcessors/notification-event-process'; -const mapEventProcessor = new MapEventProcessor(); const appBarEventProcessor = new AppBarEventProcessor(); +const mapEventProcessor = new MapEventProcessor(); +const notificationEventProcessor = new NotificationEventProcessor(); export function initializeEventProcessors(store: GeoViewStoreType) { mapEventProcessor.onInitialize(store); appBarEventProcessor.onInitialize(store); + notificationEventProcessor.onInitialize(store); } export function destroyEventProcessors() { mapEventProcessor.onDestroy(); + appBarEventProcessor.onDestroy(); + notificationEventProcessor.onDestroy(); } diff --git a/packages/geoview-core/src/api/eventProcessors/map-event-process.ts b/packages/geoview-core/src/api/eventProcessors/map-event-process.ts index 1a91420c43f..630b84d8339 100644 --- a/packages/geoview-core/src/api/eventProcessors/map-event-process.ts +++ b/packages/geoview-core/src/api/eventProcessors/map-event-process.ts @@ -1,7 +1,15 @@ import { GeoViewStoreType } from '@/core/stores/geoview-store'; import { AbstractEventProcessor } from './abstract-event-processor'; import { api } from '@/app'; -import { mapPayload, lngLatPayload, mapMouseEventPayload, numberPayload } from '@/api/events/payloads'; +import { + mapPayload, + lngLatPayload, + mapMouseEventPayload, + numberPayload, + payloadIsAMapViewProjection, + PayloadBaseClass, + mapViewProjectionPayload, +} from '@/api/events/payloads'; import { EVENT_NAMES } from '@/api/events/event-types'; export class MapEventProcessor extends AbstractEventProcessor { @@ -35,6 +43,17 @@ export class MapEventProcessor extends AbstractEventProcessor { } ); + const unsubMapProjection = store.subscribe( + (state) => state.mapState.currentProjection, + (cur, prev) => { + // because emit and on from api events can be trigger in loop, compare also the api value + if (cur !== prev && api.maps[mapId].currentProjection !== cur!) { + api.maps[mapId].currentProjection = cur!; + api.event.emit(mapViewProjectionPayload(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapId, cur!)); + } + } + ); + const unsubMapZoom = store.subscribe( (state) => state.mapState.zoom, (cur, prev) => { @@ -55,7 +74,29 @@ export class MapEventProcessor extends AbstractEventProcessor { } ); + // TODO: add a destroy events on store/map destroy + api.event.on( + EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, + (payload: PayloadBaseClass) => { + // because emit and on from api events can be trigger in loop, compare also the api value + if (payloadIsAMapViewProjection(payload) && api.maps[mapId].currentProjection !== payload.projection!) { + api.maps[mapId].currentProjection = payload.projection!; + store.setState({ + mapState: { ...store.getState().mapState, currentProjection: payload.projection! }, + }); + } + }, + mapId + ); + // add to arr of subscriptions so it can be destroyed later - this.subscriptionArr.push(unsubMapLoaded, unsubMapCenterCoord, unsubMapPointerPosition, unsubMapZoom, unsubMapSingleClick); + this.subscriptionArr.push( + unsubMapLoaded, + unsubMapCenterCoord, + unsubMapPointerPosition, + unsubMapProjection, + unsubMapZoom, + unsubMapSingleClick + ); } } diff --git a/packages/geoview-core/src/api/eventProcessors/notification-event-process.ts b/packages/geoview-core/src/api/eventProcessors/notification-event-process.ts new file mode 100644 index 00000000000..6de1dcf4e4b --- /dev/null +++ b/packages/geoview-core/src/api/eventProcessors/notification-event-process.ts @@ -0,0 +1,33 @@ +import { GeoViewStoreType } from '@/core/stores/geoview-store'; +import { AbstractEventProcessor } from './abstract-event-processor'; +import { PayloadBaseClass, payloadIsANotification } from '@/api/events/payloads'; +import { EVENT_NAMES } from '@/api/events/event-types'; +import { api, generateId } from '@/app'; + +export class NotificationEventProcessor extends AbstractEventProcessor { + onInitialize(store: GeoViewStoreType) { + const { mapId } = store.getState(); + + // TODO: add a destroy events on store/map destroy + // when the add notification is triggered, add it to the strore notifications array + api.event.on( + EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, + (payload: PayloadBaseClass) => { + if (payloadIsANotification(payload)) { + store.setState((state) => ({ + notificationState: { + notifications: [ + ...state.notificationState.notifications, + { key: generateId(), notificationType: payload.notificationType, message: payload.message }, + ], + }, + })); + } + }, + mapId + ); + + // add to arr of subscriptions so it can be destroyed later + this.subscriptionArr.push(); + } +} diff --git a/packages/geoview-core/src/api/events/constants/notifications.ts b/packages/geoview-core/src/api/events/constants/notifications.ts index a66a95e85c4..854e27a165a 100644 --- a/packages/geoview-core/src/api/events/constants/notifications.ts +++ b/packages/geoview-core/src/api/events/constants/notifications.ts @@ -6,7 +6,7 @@ import { EventStringId } from '../event-types'; */ /** Valid keys for the NOTIFICATIONS */ -export type NotificationsEventKey = 'NOTIFICATION_ADD' | 'NOTIFICATION_REMOVE'; +export type NotificationsEventKey = 'NOTIFICATION_ADD'; /** Record that associates NOTIFICATIONS event keys to their event string id */ export const NOTIFICATIONS: Record = { @@ -14,9 +14,4 @@ export const NOTIFICATIONS: Record = { * Event triggered when a new notification has been added */ NOTIFICATION_ADD: 'notification/add', - - /** - * Event triggered when a notification has been removed - */ - NOTIFICATION_REMOVE: 'notification/remove', }; diff --git a/packages/geoview-core/src/api/events/payloads/notification-payload.ts b/packages/geoview-core/src/api/events/payloads/notification-payload.ts index 26737f07852..1af8fb69454 100644 --- a/packages/geoview-core/src/api/events/payloads/notification-payload.ts +++ b/packages/geoview-core/src/api/events/payloads/notification-payload.ts @@ -3,7 +3,7 @@ import { PayloadBaseClass } from './payload-base-class'; import { EventStringId, EVENT_NAMES } from '../event-types'; /** Valid events that can create NotificationPayload */ -const validEvents: EventStringId[] = [EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_REMOVE]; +const validEvents: EventStringId[] = [EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD]; /** * type guard function that redefines a PayloadBaseClass as a NotificationPayload diff --git a/packages/geoview-core/src/core/components/app-bar/app-bar.tsx b/packages/geoview-core/src/core/components/app-bar/app-bar.tsx index 95f2c040630..270df5bf1cb 100644 --- a/packages/geoview-core/src/core/components/app-bar/app-bar.tsx +++ b/packages/geoview-core/src/core/components/app-bar/app-bar.tsx @@ -14,7 +14,7 @@ import { TypeButtonPanel } from '@/ui/panel/panel-types'; import Export from './buttons/export'; import Geolocator from './buttons/geolocator'; -import Notifications from './buttons/notifications'; +import Notifications from '@/core/components/notifications/notifications'; import Version from './buttons/version'; import ExportModal from '../export/export-modal'; diff --git a/packages/geoview-core/src/core/components/app-bar/buttons/notifications.tsx b/packages/geoview-core/src/core/components/app-bar/buttons/notifications.tsx deleted file mode 100644 index 93174d089e5..00000000000 --- a/packages/geoview-core/src/core/components/app-bar/buttons/notifications.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { useCallback, useContext, useEffect, useState, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Typography } from '@mui/material'; -import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; - -import { EVENT_NAMES } from '@/api/events/event-types'; -import { notificationPayload, NotificationPayload, PayloadBaseClass, payloadIsANotification } from '@/api/events/payloads'; - -import { Box, Popover, InfoIcon, ErrorIcon, WarningIcon, CheckCircleIcon, CloseIcon, IconButton, NotificationsIcon, Badge } from '@/ui'; -import { NotificationDetailsType } from '@/core/components/notifications/notifications-api'; - -/** - * Notification PNG Button component - * - * @returns {JSX.Element} the notification button - */ -export default function Notifications(): JSX.Element { - const [anchorEl, setAnchorEl] = useState(null); - const [notificationsList, setNotificationsList] = useState([]); - const notificationsListRef = useRef([]); - const [notificationsCount, setNotificationsCount] = useState(0); - const notificationsCountRef = useRef(0); - - const { t } = useTranslation(); - - const mapConfig = useContext(MapContext); - - const { mapId } = mapConfig; - - const handleOpenPopover = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClosePopover = () => { - setAnchorEl(null); - }; - - /** - * Add a notification - */ - const addNotification = useCallback((payload: NotificationPayload) => { - // push the notification to the end of the list - const toAdd = payload as NotificationDetailsType; - notificationsListRef.current.push(toAdd); - setNotificationsList(notificationsListRef.current); - notificationsCountRef.current += 1; - setNotificationsCount(notificationsCountRef.current); - }, []); - - /** - * Remove a notification - */ - const removeNotification = useCallback((payload: NotificationDetailsType) => { - const state = [...notificationsListRef.current]; - const index = state.findIndex((notif) => notif.message === payload.message && payload.notificationType === notif.notificationType); - if (index > -1) { - state.splice(index, 1); - notificationsListRef.current = state; - setNotificationsList(notificationsListRef.current); - notificationsCountRef.current -= 1; - setNotificationsCount(notificationsCountRef.current); - } - }, []); - - const handleRemoveNotificationClick = (payload: NotificationDetailsType) => { - const state = [...notificationsListRef.current]; - const index = state.findIndex((notif) => notif.message === payload.message && payload.notificationType === notif.notificationType); - if (index > -1) { - api.event.emit(notificationPayload(EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_REMOVE, mapId, payload.notificationType, payload.message)); - } - }; - - const notificationAddListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsANotification(payload)) addNotification(payload); - }; - - const notificationRemoveListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsANotification(payload)) removeNotification(payload); - }; - - /** - * Manage the notifications 'add', 'remove' - */ - useEffect(() => { - // listen to new notification - api.event.on(EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, notificationAddListenerFunction, mapId); - - // listen on notification removal - api.event.on(EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_REMOVE, notificationRemoveListenerFunction, mapId); - - return () => { - api.event.off(EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, mapId, notificationAddListenerFunction); - api.event.off(EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_REMOVE, mapId, notificationRemoveListenerFunction); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mapId]); - - function getNotificationIcon(notification: NotificationDetailsType) { - switch (notification.notificationType) { - case 'success': - return ; - case 'info': - return ; - case 'warning': - return ; - default: - return ; - } - } - - function renderNotification(notification: NotificationDetailsType, index: number) { - return ( - -
{getNotificationIcon(notification)}
- {notification.message} - handleRemoveNotificationClick(notification)}> - - -
- ); - } - - const open = Boolean(anchorEl); - - return ( - <> - - - - - - - - - {t('appbar.notifications')} - -
-
- - {notificationsList.length > 0 ? ( - notificationsList.map((details, index) => renderNotification(details, index)) - ) : ( - {t('appbar.no_notifications_available')} - )} - -
-
- - ); -} diff --git a/packages/geoview-core/src/core/components/attribution/attribution.tsx b/packages/geoview-core/src/core/components/attribution/attribution.tsx index 2eca51239b2..97cc4f84d0d 100644 --- a/packages/geoview-core/src/core/components/attribution/attribution.tsx +++ b/packages/geoview-core/src/core/components/attribution/attribution.tsx @@ -9,7 +9,6 @@ import { useStore } from 'zustand'; import { getGeoViewStore } from '@/core/stores/stores-managers'; import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; import { Tooltip, Box } from '@/ui'; const useStyles = makeStyles((theme) => ({ @@ -150,11 +149,10 @@ export function Attribution(): JSX.Element { const attributionTextRef = useRef>([]); // get the expand or collapse from store + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); const expanded = useStore(getGeoViewStore(mapId), (state) => state.footerBarState.expanded); useEffect(() => { - const { map } = api.maps[mapId]; - const attributionTextElement = document.getElementById(`${mapId}-attribution-text`) as HTMLElement; const attributionControl = new CustomAttribution( @@ -180,12 +178,12 @@ export function Attribution(): JSX.Element { } attributionControl.formatAttribution(); - map.addControl(attributionControl); + mapElement.addControl(attributionControl); return () => { - map.removeControl(attributionControl); + mapElement.removeControl(attributionControl); }; - }, [mapId]); + }, [mapId, mapElement]); return ( diff --git a/packages/geoview-core/src/core/components/click-marker/click-marker.tsx b/packages/geoview-core/src/core/components/click-marker/click-marker.tsx index e05658b735c..9566aa97421 100644 --- a/packages/geoview-core/src/core/components/click-marker/click-marker.tsx +++ b/packages/geoview-core/src/core/components/click-marker/click-marker.tsx @@ -51,7 +51,10 @@ export function ClickMarker(): JSX.Element { element: document.getElementById(clickMarkerId) as HTMLElement, stopEvent: false, }); - api.maps[mapId].map.addOverlay(clickMarkerOverlay); + + // TODO: repair using the store map element + //! came from the map creation on function call + if (api.maps[mapId].map !== undefined) api.maps[mapId].map.addOverlay(clickMarkerOverlay); // create resources to highlight features const animationSource = new VectorSource(); @@ -86,7 +89,7 @@ export function ClickMarker(): JSX.Element { api.maps[mapId].map .getOverlayById(`${mapId}-clickmarker`) .setPosition(fromLonLat(lnglat, `EPSG:${api.maps[mapId].currentProjection}`)); - }, 0); + }, 100); setShowMarker(true); } diff --git a/packages/geoview-core/src/core/components/crosshair/crosshair.tsx b/packages/geoview-core/src/core/components/crosshair/crosshair.tsx index a140fb55419..6bf1133dbab 100644 --- a/packages/geoview-core/src/core/components/crosshair/crosshair.tsx +++ b/packages/geoview-core/src/core/components/crosshair/crosshair.tsx @@ -10,7 +10,7 @@ import { KeyboardPan } from 'ol/interaction'; import { useStore } from 'zustand'; import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; +import { TypeEventHandlerFunction, api } from '@/app'; import { EVENT_NAMES } from '@/api/events/event-types'; import { CrosshairIcon } from './crosshair-icon'; @@ -125,31 +125,39 @@ export function Crosshair(): JSX.Element { useEffect(() => { const { map } = api.maps[mapId]; - const mapContainer = map.getTargetElement(); - - const eventMapInKeyFocusListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsAInKeyfocus(payload)) { - if (interaction !== 'static') { - store.setState({ isCrosshairsActive: true }); - - mapContainer.addEventListener('keydown', simulateClick); - mapContainer.addEventListener('keydown', managePanDelta); - panelButtonId.current = 'detailsPanel'; + // TODO: repair using the store map element + //! came fromt he map creation on function call + let eventMapInKeyFocusListenerFunction: TypeEventHandlerFunction; + if (map !== undefined) { + const mapContainer = map.getTargetElement(); + + eventMapInKeyFocusListenerFunction = (payload: PayloadBaseClass) => { + if (payloadIsAInKeyfocus(payload)) { + if (interaction !== 'static') { + store.setState({ isCrosshairsActive: true }); + + mapContainer.addEventListener('keydown', simulateClick); + mapContainer.addEventListener('keydown', managePanDelta); + panelButtonId.current = 'detailsPanel'; + } } - } - }; + }; - // on map keyboard focus, add crosshair - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_IN_KEYFOCUS, eventMapInKeyFocusListenerFunction, mapId); + // on map keyboard focus, add crosshair + api.event.on(EVENT_NAMES.MAP.EVENT_MAP_IN_KEYFOCUS, eventMapInKeyFocusListenerFunction, mapId); - // when map blur, remove the crosshair and click event - mapContainer.addEventListener('blur', removeCrosshair); + // when map blur, remove the crosshair and click event + mapContainer.addEventListener('blur', removeCrosshair); + } return () => { - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_IN_KEYFOCUS, mapId, eventMapInKeyFocusListenerFunction); - mapContainer.removeEventListener('keydown', simulateClick); - mapContainer.removeEventListener('keydown', managePanDelta); - mapContainer.removeEventListener('keydown', removeCrosshair); + if (map !== undefined) { + api.event.off(EVENT_NAMES.MAP.EVENT_MAP_IN_KEYFOCUS, mapId, eventMapInKeyFocusListenerFunction); + const mapContainer = map.getTargetElement(); + mapContainer.removeEventListener('keydown', simulateClick); + mapContainer.removeEventListener('keydown', managePanDelta); + mapContainer.removeEventListener('keydown', removeCrosshair); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/packages/geoview-core/src/core/components/data-table/data-panel.tsx b/packages/geoview-core/src/core/components/data-table/data-panel.tsx index fb66342e11f..8356e2c3d33 100644 --- a/packages/geoview-core/src/core/components/data-table/data-panel.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-panel.tsx @@ -135,7 +135,7 @@ export function Datapanel({ layerData, mapId, projectionConfig, layerKeys, layer {layerKeys[selectedLayerIndex]} - + {!isLoading && layerKeys.map((layerKey, index) => ( diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar-expand-button.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar-expand-button.tsx index 78021d81f45..740bd87453c 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar-expand-button.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar-expand-button.tsx @@ -5,18 +5,7 @@ import { getGeoViewStore } from '@/core/stores/stores-managers'; import { ExpandMoreIcon, ExpandLessIcon, IconButton, Box } from '@/ui'; import { MapContext } from '@/core/app-start'; - -const sxClasses = { - expandbuttonContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: 'primary.light', - height: '30px', - width: '30px', - marginLeft: '5px', - }, -}; +import { sxClassesExportButton } from './footer-bar-style'; /** * Footerbar Expand Button component @@ -79,7 +68,7 @@ export function FooterbarExpandButton(): JSX.Element { return ( - (expanded ? collapseFooterbar() : expandFooterbar())}> + (expanded ? collapseFooterbar() : expandFooterbar())}> {expanded ? : } diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar-fixnorth-switch.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar-fixnorth-switch.tsx index 3417f2d33f3..4dd01eea606 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar-fixnorth-switch.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar-fixnorth-switch.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; @@ -6,15 +6,9 @@ import { useStore } from 'zustand'; import { getGeoViewStore } from '@/core/stores/stores-managers'; import { Switch } from '@/ui'; - -import { api } from '@/app'; import { MapContext } from '@/core/app-start'; - import { PROJECTION_NAMES } from '@/geo/projection/projection'; -import { EVENT_NAMES } from '@/api/events/event-types'; -import { PayloadBaseClass, booleanPayload, payloadIsAMapViewProjection } from '@/api/events/payloads'; - /** * Footerbar Fix North Switch component * @@ -26,54 +20,34 @@ export function FooterbarFixNorthSwitch(): JSX.Element { const { t } = useTranslation(); - const [mapProjection, setMapProjection] = useState(`EPSG:${api.maps[mapId].currentProjection}`); - const [switchChecked, setSwitchChecked] = useState(false); - const [isNorthEnable] = useState(api.maps[mapId].mapFeaturesConfig.components!.indexOf('north-arrow') > -1); - // get the expand or collapse from store const expanded = useStore(getGeoViewStore(mapId), (state) => state.footerBarState.expanded); + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); + const isNorthEnable = useStore(getGeoViewStore(mapId), (state) => state.mapState.northArrow); + const isFixNorth = useStore(getGeoViewStore(mapId), (state) => state.mapState.fixNorth); + const mapProjection = `EPSG:${useStore(getGeoViewStore(mapId), (state) => state.mapState.currentProjection)}`; /** * Emit an event to specify the map to rotate to keep north straight */ const fixNorth = (event: React.ChangeEvent) => { - setSwitchChecked(!switchChecked); - // this event will be listen by the north-arrow.tsx component - api.event.emit(booleanPayload(EVENT_NAMES.MAP.EVENT_MAP_FIX_NORTH, mapId, event.target.checked)); + getGeoViewStore(mapId).setState({ + mapState: { ...getGeoViewStore(mapId).getState().mapState, fixNorth: event.target.checked }, + }); // if unchecked, reset rotation if (!event.target.checked) { - const { map } = api.maps[mapId]; - map.getView().animate({ + mapElement.getView().animate({ rotation: 0, }); } }; - const eventMapViewProjectionChangeListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsAMapViewProjection(payload)) { - setMapProjection(`EPSG:${payload.projection}`); - - // uncheck the control - if (switchChecked) setSwitchChecked(false); - } - }; - - useEffect(() => { - // listen to geoview-basemap-panel package change projection event - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, eventMapViewProjectionChangeListenerFunction, mapId); - - return () => { - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapId, eventMapViewProjectionChangeListenerFunction); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return (
{expanded && mapProjection === PROJECTION_NAMES.LCC && isNorthEnable ? ( - + ) : null}
); diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar-rotation-button.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar-rotation-button.tsx index f22291b55fb..6455645a2e2 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar-rotation-button.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar-rotation-button.tsx @@ -1,30 +1,13 @@ -import { useContext, useEffect, useRef } from 'react'; - -import { View } from 'ol'; +import { useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { ArrowUpIcon, IconButton } from '@/ui'; +import { useStore } from 'zustand'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; +import { ArrowUpIcon, IconButton } from '@/ui'; import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; - -import { EVENT_NAMES } from '@/api/events/event-types'; -import { PayloadBaseClass, payloadIsAMapViewProjection } from '@/api/events/payloads'; - -const sxClasses = { - rotationButton: { - height: 25, - width: 25, - marginRight: 5, - }, - rotationIcon: { - fontSize: 'fontSize', - width: '1.5em', - height: '1.5em', - color: 'primary.light', - }, -}; +import { sxClassesRotationButton } from './footer-bar-style'; /** * Footerbar Rotation Button component @@ -39,70 +22,19 @@ export function FooterbarRotationButton(): JSX.Element { const iconRef = useRef(null); - /** - * Reset the map rotation - */ - const resetRotation = () => { - const { map } = api.maps[mapId]; - - map.getView().animate({ - rotation: 0, - }); - }; - - /** - * Set the rotation icon on masp view rotation change - */ - const setViewRotationEvent = () => { - const { map } = api.maps[mapId]; - - map.getView().on('change:rotation', (e) => { - const rotation = (e.target as View).getRotation(); - - if (iconRef && iconRef.current) { - const icon = iconRef.current as HTMLElement; - - icon.style.transform = `rotate(${rotation}rad)`; - } - }); - }; - - const eventMapViewProjectionChangeCallbackFunction = (payload: PayloadBaseClass) => { - if (payloadIsAMapViewProjection(payload)) { - // reset icon rotation to 0 because the new view rotation is 0 - // will be set again by proper function if needed (i.e. if fix north switch is checked) - if (iconRef && iconRef.current) { - const icon = iconRef.current as HTMLElement; - icon.style.transform = `rotate(${0}rad)`; - } - - // recreate the event on projection change because there is a new view - setViewRotationEvent(); - } - }; - - useEffect(() => { - // set rotation change event - setViewRotationEvent(); - - // listen to geoview-basemap-panel package change projection event to reset icon rotation - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, eventMapViewProjectionChangeCallbackFunction, mapId); - - return () => { - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapId, eventMapViewProjectionChangeCallbackFunction); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // get the values from store + const mapRotation = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapRotation); + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); return ( resetRotation()} + onClick={() => mapElement.getView().animate({ rotation: 0 })} > - + ); } diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar-style.ts b/packages/geoview-core/src/core/components/footer-bar/footer-bar-style.ts new file mode 100644 index 00000000000..3331f17345a --- /dev/null +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar-style.ts @@ -0,0 +1,63 @@ +// footer-bar.tsx +export const sxClassesFooterBar = { + footerBarContainer: { + zIndex: 50, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + width: 'calc(100% - 64px)', + minHeight: '35px', + maxHeight: '35px', + backdropFilter: 'blur(5px)', + backgroundColor: '#000000cc', + pointerEvents: 'all', + position: 'absolute', + left: '64px', + bottom: 0, + gap: 0.5, + }, + mouseScaleControlsContainer: { + display: 'flex', + flexDirection: 'row', + gap: 20, + '& button': { + cursor: 'pointer', + margin: 'auto', + }, + }, + rotationControlsContainer: { + display: 'flex', + flexDirection: 'column', + marginLeft: 20, + alignItems: 'flex-end', + }, +}; + +// footer-bar-expand-button.tsx +export const sxClassesExportButton = { + expandbuttonContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: 'primary.light', + height: '30px', + width: '30px', + marginLeft: '5px', + }, +}; + +// footer-bar-rotation-button.tsx +export const sxClassesRotationButton = { + rotationButton: { + height: 25, + width: 25, + marginRight: 5, + }, + rotationIcon: { + fontSize: 'fontSize', + width: '1.5em', + height: '1.5em', + color: 'primary.light', + }, +}; diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx index 5fd6b210e90..534ba4e2646 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx @@ -1,7 +1,10 @@ import { MutableRefObject, useContext, useRef } from 'react'; +import { useStore } from 'zustand'; + import { useMediaQuery } from '@mui/material'; import { useTheme } from '@mui/material/styles'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; import { Box } from '@/ui'; import { Attribution } from '../attribution/attribution'; @@ -12,40 +15,7 @@ import { MapContext } from '@/core/app-start'; import { FooterbarExpandButton } from './footer-bar-expand-button'; import { FooterbarRotationButton } from './footer-bar-rotation-button'; import { FooterbarFixNorthSwitch } from './footer-bar-fixnorth-switch'; - -const sxClasses = { - footerBarContainer: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - width: 'calc(100% - 64px)', - minHeight: '35px', - maxHeight: '35px', - backdropFilter: 'blur(5px)', - backgroundColor: '#000000cc', - pointerEvents: 'all', - position: 'absolute', - left: '64px', - bottom: 0, - gap: 0.5, - }, - mouseScaleControlsContainer: { - display: 'flex', - flexDirection: 'row', - gap: 20, - '& button': { - cursor: 'pointer', - margin: 'auto', - }, - }, - rotationControlsContainer: { - display: 'flex', - flexDirection: 'column', - marginLeft: 20, - alignItems: 'flex-end', - }, -}; +import { sxClassesFooterBar } from './footer-bar-style'; /** * Create a footer bar element that contains attribtuion, mouse position and scale @@ -54,28 +24,29 @@ const sxClasses = { */ export function Footerbar(): JSX.Element { const mapConfig = useContext(MapContext); - - const { mapId, interaction } = mapConfig; + const { mapId } = mapConfig; const footerBarRef = useRef(); const defaultTheme = useTheme(); + // get value from the store + // if map is static do not display mouse position or rotation controls + const interaction = useStore(getGeoViewStore(mapId), (state) => state.mapState.interaction); + // if screen size is medium and up const deviceSizeMedUp = useMediaQuery(defaultTheme.breakpoints.up('sm')); - // If map is static do not display mouse position or rotation controls - const mapIsDynamic = interaction === 'dynamic'; return ( - }> + }> {deviceSizeMedUp && } - - {deviceSizeMedUp && mapIsDynamic && } + + {deviceSizeMedUp && interaction === 'dynamic' && } - {mapIsDynamic && ( - + {interaction === 'dynamic' && ( + diff --git a/packages/geoview-core/src/core/components/index.ts b/packages/geoview-core/src/core/components/index.ts index c78490f7adf..dda81736ad2 100644 --- a/packages/geoview-core/src/core/components/index.ts +++ b/packages/geoview-core/src/core/components/index.ts @@ -1,7 +1,6 @@ export * from './app-bar/buttons/export'; export * from './app-bar/buttons/geolocator'; export * from './app-bar/buttons/help'; -export * from './app-bar/buttons/notifications'; export * from './app-bar/buttons/version'; export * from './app-bar/app-bar'; export * from './app-bar/app-bar-buttons'; @@ -49,7 +48,7 @@ export * from './nav-bar/nav-bar-buttons'; export * from './nav-bar/nav-bar'; export * from './north-arrow/north-arrow-icon'; export * from './north-arrow/north-arrow'; -export * from './notifications/notifications-api'; +export * from './notifications/notifications'; export * from './overview-map/overview-map-toggle'; export * from './overview-map/overview-map'; export * from './scale/scale'; diff --git a/packages/geoview-core/src/core/components/map/map.tsx b/packages/geoview-core/src/core/components/map/map.tsx index 63b8563b9ce..4dcc051deda 100644 --- a/packages/geoview-core/src/core/components/map/map.tsx +++ b/packages/geoview-core/src/core/components/map/map.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { useEffect, useState, useRef, MutableRefObject } from 'react'; +import { useEffect, useRef, MutableRefObject } from 'react'; import { fromLonLat, toLonLat } from 'ol/proj'; import OLMap from 'ol/Map'; @@ -8,22 +8,25 @@ import TileLayer from 'ol/layer/Tile'; import { Collection } from 'ol'; import BaseLayer from 'ol/layer/Base'; import Source from 'ol/source/Source'; +import { Extent } from 'ol/extent'; import makeStyles from '@mui/styles/makeStyles'; import { useMediaQuery } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { Extent } from 'ol/extent'; -import { NorthArrow, NorthPoleFlag } from '../north-arrow/north-arrow'; -import { Crosshair } from '../crosshair/crosshair'; -import { Footerbar } from '../footer-bar/footer-bar'; -import { OverviewMap } from '../overview-map/overview-map'; -import { ClickMarker } from '../click-marker/click-marker'; -import { HoverTooltip } from '../hover-tooltip/hover-tooltip'; +import { useStore } from 'zustand'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; + +import { NorthArrow, NorthPoleFlag } from '@/core/components/north-arrow/north-arrow'; +import { Crosshair } from '@/core/components/crosshair/crosshair'; +import { Footerbar } from '@/core/components/footer-bar/footer-bar'; +import { OverviewMap } from '@/core/components/overview-map/overview-map'; +import { ClickMarker } from '@/core/components/click-marker/click-marker'; +import { HoverTooltip } from '@/core/components/hover-tooltip/hover-tooltip'; -import { disableScrolling, generateId } from '../../utils/utilities'; +import { addNotificationWarning, disableScrolling, generateId } from '@/core/utils/utilities'; -import { api, inKeyfocusPayload, notificationPayload } from '@/app'; +import { api, inKeyfocusPayload } from '@/app'; import { EVENT_NAMES } from '@/api/events/event-types'; import { MapViewer } from '@/geo/map/map'; @@ -41,18 +44,20 @@ const useStyles = makeStyles(() => ({ })); export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { - const { map: mapConfig, components } = mapFeaturesConfig; + const { map: mapConfig } = mapFeaturesConfig; // make sure the id is not undefined // eslint-disable-next-line react/destructuring-assignment const mapId = mapFeaturesConfig.mapId ? mapFeaturesConfig.mapId : generateId(''); - const [isLoaded, setIsLoaded] = useState(false); const classes = useStyles(); // get ref to div element const mapElement = useRef(); + // get values from the store + const { mapLoaded, northArrow, overviewMap } = useStore(getGeoViewStore(mapId), (state) => state.mapState); + // create a new map viewer instance const viewer: MapViewer = api.maps[mapId]; @@ -92,9 +97,6 @@ export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { }); viewer.toggleMapInteraction(mapConfig.interaction); - - // emit the map loaded event - setIsLoaded(true); }; const initMap = async () => { @@ -107,15 +109,8 @@ export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { if (mapConfig.viewSettings?.extent) { if (projection.getCode() === 'EPSG:3978') { // eslint-disable-next-line no-console - console.error('Extents not available for LLC projections (EPSG: 3978)'); - api.event.emit( - notificationPayload( - EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, - mapId, - 'warning', - 'Extents not available for LLC projections (EPSG: 3978)' - ) - ); + console.log('Extents not available for LLC projections (EPSG: 3978)'); + addNotificationWarning(mapId, 'Extents not available for LLC projections (EPSG: 3978)'); } else { const mins = fromLonLat([mapConfig.viewSettings.extent[0], mapConfig.viewSettings.extent[1]], projection.getCode()); const maxs = fromLonLat([mapConfig.viewSettings.extent[2], mapConfig.viewSettings.extent[3]], projection.getCode()); @@ -257,16 +252,14 @@ export function Map(mapFeaturesConfig: TypeMapFeaturesConfig): JSX.Element { return ( /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
} className={classes.mapContainer} tabIndex={0}> - {isLoaded && ( + {mapLoaded && ( <> - {components !== undefined && components.indexOf('north-arrow') > -1 && ( - - )} - + {northArrow && } + - {deviceSizeMedUp && components !== undefined && components.indexOf('overview-map') > -1 && } + {deviceSizeMedUp && overviewMap && } {deviceSizeMedUp && } )} diff --git a/packages/geoview-core/src/core/components/north-arrow/north-arrow.tsx b/packages/geoview-core/src/core/components/north-arrow/north-arrow.tsx index 8a59f44a296..1f01f873b5e 100644 --- a/packages/geoview-core/src/core/components/north-arrow/north-arrow.tsx +++ b/packages/geoview-core/src/core/components/north-arrow/north-arrow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState, useContext } from 'react'; +import { useEffect, useRef, useState, useContext } from 'react'; import OLMap from 'ol/Map'; import Overlay from 'ol/Overlay'; @@ -8,16 +8,12 @@ import { toLonLat, fromLonLat } from 'ol/proj'; import { useTheme } from '@mui/material/styles'; import makeStyles from '@mui/styles/makeStyles'; -import debounce from 'lodash/debounce'; +import { useStore } from 'zustand'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; import { PROJECTION_NAMES } from '@/geo/projection/projection'; - -import { NorthArrowIcon, NorthPoleIcon } from './north-arrow-icon'; - import { MapContext } from '@/core/app-start'; -import { api } from '@/app'; -import { EVENT_NAMES } from '@/api/events/event-types'; -import { payloadIsAMapViewProjection, payloadIsABoolean, PayloadBaseClass } from '@/api/events/payloads'; +import { NorthArrowIcon, NorthPoleIcon } from './north-arrow-icon'; const useStyles = makeStyles((theme) => ({ northArrowContainer: { @@ -34,39 +30,38 @@ const useStyles = makeStyles((theme) => ({ // north value (set longitude to be half of Canada extent (142° W, 52° W)) - projection central meridian is -95 const northPolePosition: [number, number] = [90, -95]; -// interface used for NorthArrow props -interface NorthArrowProps { - projection: string; -} - /** * Create a north arrow * * @returns {JSX.Element} the north arrow component */ -export function NorthArrow(props: NorthArrowProps): JSX.Element { - const { projection } = props; +export function NorthArrow(): JSX.Element { + const mapConfig = useContext(MapContext); + const { mapId } = mapConfig; + // access transitions + const defaultTheme = useTheme(); + // TODO: remove make style const classes = useStyles(); + // do not use useState for item used inside function only without rendering... use useRef + const isNorthFixedValue = useRef(false); const northArrowRef = useRef(null); - const [rotationAngle, setRotationAngle] = useState({ angle: 0 }); - const [isNorthVisible, setIsNorthVisible] = useState(false); - const [northOffset, setNorthOffset] = useState(0); - // keep track of rotation angle for fix north let angle = 0; - // do not use useState for item used inside function only without rendering... use useRef - const isNorthFixedValue = useRef(false); - - // access transitions - const defaultTheme = useTheme(); - - const mapConfig = useContext(MapContext); + // internal component state + const [rotationAngle, setRotationAngle] = useState({ angle: 0 }); + const [isNorthVisible, setIsNorthVisible] = useState(false); + const [northOffset, setNorthOffset] = useState(0); - const { mapId } = mapConfig; + // get the values from store + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); + const mapProjection = useRef(''); + const mapProjectionCode = useStore(getGeoViewStore(mapId), (state) => state.mapState.currentProjection); + mapProjection.current = `EPSG:${mapProjectionCode}`; + const fixNorth = useStore(getGeoViewStore(mapId), (state) => state.mapState.fixNorth); /** * Get north arrow bearing. Angle use to rotate north arrow for non Web Mercator projection @@ -82,10 +77,7 @@ export function NorthArrow(props: NorthArrowProps): JSX.Element { // map center (we use botton parallel to introduce less distortion) const extent = map.getView().calculateExtent(); - const center: Coordinate = toLonLat( - [(extent[0] + extent[2]) / 2, extent[1]], - api.projection.projections[api.maps[mapId].currentProjection] - ); + const center: Coordinate = toLonLat([(extent[0] + extent[2]) / 2, extent[1]], mapProjection.current); const pointB = { x: center[0], y: center[1] }; // set info on longitude and latitude @@ -117,7 +109,7 @@ export function NorthArrow(props: NorthArrowProps): JSX.Element { // Check the container value for top middle of the screen // Convert this value to a lat long coordinate const pointXY = [map.getSize()![0] / 2, 1]; - const pt = toLonLat(map.getCoordinateFromPixel(pointXY), api.projection.projections[api.maps[mapId].currentProjection]); + const pt = toLonLat(map.getCoordinateFromPixel(pointXY), mapProjection.current); // If user is pass north, long value will start to be positive (other side of the earth). // This will work only for LCC Canada. @@ -182,7 +174,7 @@ export function NorthArrow(props: NorthArrowProps): JSX.Element { * @param {OLMap} map the map */ function manageArrow(map: OLMap): void { - if (projection === PROJECTION_NAMES.LCC) { + if (mapProjection.current === PROJECTION_NAMES.LCC) { // Because of the projection, corners are wrapped and central value of the polygon may be higher then corners values. // There is no easy way to see if the user sees the north pole just by using bounding box. One of the solution may // be to use a debounce function to call on moveEnd where we @@ -209,14 +201,14 @@ export function NorthArrow(props: NorthArrowProps): JSX.Element { angle = arrowAngle; // set map rotation to keep fix north - api.maps[mapId].map.getView().animate({ + mapElement.getView().animate({ rotation: ((180 - arrowAngle) * (2 * Math.PI)) / 360, }); setRotationAngle({ angle: 0 }); } else { // set arrow rotation - const mapRotation = map.getView().getRotation() * (180 / Math.PI); + const mapRotation = fixNorth ? map.getView().getRotation() * (180 / Math.PI) : 0; setRotationAngle({ angle: 90 - angleDegrees + mapRotation }); } @@ -226,45 +218,38 @@ export function NorthArrow(props: NorthArrowProps): JSX.Element { } } - /** - * Map moveend event callback - */ - // eslint-disable-next-line react-hooks/exhaustive-deps - const onMapMoveEnd = useCallback( - debounce((e) => { - const map = e.map as OLMap; - manageArrow(map); - }, 500), - [] - ); - - const mapFixNorthListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsABoolean(payload)) { - isNorthFixedValue.current = payload.status; - - // if north is fix, trigger the map rotation - if (payload.status) { - manageArrow(api.maps[mapId].map); - } - } - }; - useEffect(() => { - const { map } = api.maps[mapId]; - - // listen to map moveend event - map.on('moveend', onMapMoveEnd); + // if mapCenterCoordinates changed, map move end event has been triggered + const unsubMapCenterCoord = getGeoViewStore(mapId).subscribe( + (state) => state.mapState.mapCenterCoordinates, + (curCoords, prevCoords) => { + if (curCoords !== prevCoords) { + manageArrow(mapElement); + } + }, + { + fireImmediately: true, + } + ); - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_FIX_NORTH, mapFixNorthListenerFunction, mapId); + const unsubMapFixNorth = getGeoViewStore(mapId).subscribe( + (state) => state.mapState.fixNorth, + (curNorth, prevNorth) => { + if (curNorth !== prevNorth) { + isNorthFixedValue.current = curNorth; + manageArrow(mapElement); + } + } + ); return () => { - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_FIX_NORTH, mapId); - map.un('moveend', onMapMoveEnd); + unsubMapCenterCoord(); + unsubMapFixNorth(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [mapId]); + }, []); - return projection === PROJECTION_NAMES.LCC ? ( + return mapProjection.current === PROJECTION_NAMES.LCC ? (
(null); - +export function NorthPoleFlag(): JSX.Element { const mapConfig = useContext(MapContext); - const [mapProjection, setMapProjection] = useState(projection); const { mapId } = mapConfig; + const northPoleId = `${mapId}-northpole`; + const northPoleRef = useRef(null); - const mapviewProjectionChangeListenerFunction = (payload: PayloadBaseClass) => { - if (payloadIsAMapViewProjection(payload)) { - setMapProjection(`EPSG:${payload.projection}`); - } - }; + // get the values from store + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); + const mapProjection = useStore(getGeoViewStore(mapId), (state) => state.mapState.currentProjection); useEffect(() => { - const { map } = api.maps[mapId]; - const projectionPosition = fromLonLat( - [northPolePosition[1], northPolePosition[0]], - api.projection.projections[api.maps[mapId].currentProjection] - ); + const projectionPosition = fromLonLat([northPolePosition[1], northPolePosition[0]], `EPSG:${mapProjection}`); // create overlay for north pole icon const northPoleMarker = new Overlay({ @@ -320,19 +296,16 @@ export function NorthPoleFlag(props: NorthArrowProps): JSX.Element { element: document.getElementById(northPoleId) as HTMLElement, stopEvent: false, }); - map.addOverlay(northPoleMarker); - - // listen to geoview-basemap-panel package change projection event - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapviewProjectionChangeListenerFunction, mapId); - - return () => { - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapId, mapviewProjectionChangeListenerFunction); - }; + mapElement.addOverlay(northPoleMarker); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
+
); diff --git a/packages/geoview-core/src/core/components/notifications/notifications-api.ts b/packages/geoview-core/src/core/components/notifications/notifications-api.ts deleted file mode 100644 index 7025ce16b68..00000000000 --- a/packages/geoview-core/src/core/components/notifications/notifications-api.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { api } from '@/app'; - -import { EVENT_NAMES } from '@/api/events/event-types'; - -import { notificationPayload, NotificationType } from '@/api/events/payloads'; - -export type NotificationDetailsType = { - notificationType: NotificationType; - message: string; - description?: string; -}; - -/** - * API to manage notifications in the notification component - * - * @exports - * @class - */ -export class NotificationsApi { - mapId!: string; - - // array that hold added tabs - notificationsList: NotificationDetailsType[] = []; - - /** - * initialize the notifications api - * - * @param mapId the id of the map this notifications belongs to - */ - constructor(mapId: string) { - this.mapId = mapId; - } - - /** - * Create a tab on the notifications - * - * @param {NotificationDetailsType} notificationProps the properties of the notification to be added - * - */ - addNotification = (notificationProps: NotificationDetailsType) => { - if (notificationProps) { - // add the new tab to the notifications array - this.notificationsList.push(notificationProps); - - // trigger an event that a new tab has been created - api.event.emit( - notificationPayload( - EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_ADD, - this.mapId, - notificationProps.notificationType, - notificationProps.message - ) - ); - } - }; - - /** - * Remove notification by index - * - * @param {NotificationDetailsType} notificationProps the properties of the notification to be removed - */ - removeNotification = (notificationProps: NotificationDetailsType): void => { - // find the notification to be removed - const notifToRemove = this.notificationsList.find( - (notif) => notif.message === notificationProps.message && notificationProps.notificationType === notif.notificationType - ); - - if (notifToRemove) { - // remove the notification from the notifications array - this.notificationsList = this.notificationsList.filter( - (notif) => !(notif.message === notificationProps.message && notificationProps.notificationType === notif.notificationType) - ); - - // trigger an event that a notification has been removed - api.event.emit( - notificationPayload( - EVENT_NAMES.NOTIFICATIONS.NOTIFICATION_REMOVE, - this.mapId, - notificationProps.notificationType, - notificationProps.message - ) - ); - } - }; -} diff --git a/packages/geoview-core/src/core/components/notifications/notifications-style.ts b/packages/geoview-core/src/core/components/notifications/notifications-style.ts new file mode 100644 index 00000000000..c3229d4efe9 --- /dev/null +++ b/packages/geoview-core/src/core/components/notifications/notifications-style.ts @@ -0,0 +1,19 @@ +export const sxClasses = { + notificationPanel: { + display: 'flex', + flexDirection: 'column', + padding: '10px 20px', + width: '400px', + maxHeight: '500px', + overflowY: 'hidden', + gap: '8px', + }, + notificationItem: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '8px', + borderBottom: '1px solid #474747', + padding: '10px 0px', + }, +}; diff --git a/packages/geoview-core/src/core/components/notifications/notifications.tsx b/packages/geoview-core/src/core/components/notifications/notifications.tsx new file mode 100644 index 00000000000..1b9717d03f4 --- /dev/null +++ b/packages/geoview-core/src/core/components/notifications/notifications.tsx @@ -0,0 +1,143 @@ +import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useStore } from 'zustand'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; + +import { MapContext } from '@/core/app-start'; +import { NotificationType } from '@/api/events/payloads'; +import { + Box, + Popover, + InfoIcon, + ErrorIcon, + WarningIcon, + CheckCircleIcon, + CloseIcon, + IconButton, + NotificationsIcon, + Badge, + Typography, +} from '@/ui'; +import { sxClasses } from './notifications-style'; + +export type NotificationDetailsType = { + key: string; + notificationType: NotificationType; + message: string; + description?: string; +}; + +/** + * Notification PNG Button component + * + * @returns {JSX.Element} the notification button + */ +export default function Notifications(): JSX.Element { + const mapConfig = useContext(MapContext); + const { mapId } = mapConfig; + + const { t } = useTranslation(); + + // internal state + const [anchorEl, setAnchorEl] = useState(null); + + // get values from the store + const notifications = useStore(getGeoViewStore(mapId), (state) => state.notificationState.notifications); + const notificationsCount = notifications.length; + + // handle open/close + const open = Boolean(anchorEl); + const handleOpenPopover = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClosePopover = () => { + setAnchorEl(null); + }; + + /** + * Remove a notification + */ + const handleRemoveNotificationClick = (notification: NotificationDetailsType) => { + const origNotifications = [...notifications]; + const index = origNotifications.findIndex((notif) => notif.key === notification.key); + if (index > -1) { + origNotifications.splice(index, 1); + getGeoViewStore(mapId).setState(() => ({ + notificationState: { + notifications: origNotifications, + }, + })); + } + }; + + function getNotificationIcon(notification: NotificationDetailsType) { + switch (notification.notificationType) { + case 'success': + return ; + case 'info': + return ; + case 'warning': + return ; + default: + return ; + } + } + + function renderNotification(notification: NotificationDetailsType, index: number) { + return ( + + {getNotificationIcon(notification)} + {notification.message} + handleRemoveNotificationClick(notification)}> + + + + ); + } + + return ( + <> + + + + + + + + + {t('appbar.notifications')} + +
+
+ + {notifications.length > 0 ? ( + notifications.map((notification, index) => renderNotification(notification, index)) + + ) : ( + {t('appbar.no_notifications_available')} + )} + +
+
+ + ); +} diff --git a/packages/geoview-core/src/core/components/overview-map/overview-map-toggle.tsx b/packages/geoview-core/src/core/components/overview-map/overview-map-toggle.tsx index 88b3da905b6..e65d9b851fb 100644 --- a/packages/geoview-core/src/core/components/overview-map/overview-map-toggle.tsx +++ b/packages/geoview-core/src/core/components/overview-map/overview-map-toggle.tsx @@ -54,6 +54,7 @@ export function OverviewMapToggle(props: OverviewMapToggleProps): JSX.Element { const divRef = useRef(null); + // TODO: Remove useStyle const classes = useStyles(); useEffect(() => { diff --git a/packages/geoview-core/src/core/components/overview-map/overview-map.tsx b/packages/geoview-core/src/core/components/overview-map/overview-map.tsx index 06083c2c3b7..ce3058b6cf6 100644 --- a/packages/geoview-core/src/core/components/overview-map/overview-map.tsx +++ b/packages/geoview-core/src/core/components/overview-map/overview-map.tsx @@ -10,16 +10,13 @@ import makeStyles from '@mui/styles/makeStyles'; import TileLayer from 'ol/layer/Tile'; import { OverviewMap as OLOverviewMap } from 'ol/control'; +import { useStore } from 'zustand'; import { getGeoViewStore } from '@/core/stores/stores-managers'; -import { OverviewMapToggle } from './overview-map-toggle'; -import { EVENT_NAMES } from '@/api/events/event-types'; -import { PayloadBaseClass, api } from '@/app'; import { MapContext } from '@/core/app-start'; - -import { payloadIsAMapViewProjection } from '@/api/events/payloads'; - import { cgpvTheme } from '@/ui/style/theme'; +import { OverviewMapToggle } from './overview-map-toggle'; +import { api } from '@/app'; /** * Size of the overview map container @@ -102,35 +99,27 @@ const useStyles = makeStyles((theme) => ({ */ export function OverviewMap(): JSX.Element { const mapConfig = useContext(MapContext); - const { mapId } = mapConfig; - const { map, mapFeaturesConfig } = api.maps[mapId]; - const hideOnZoom = mapFeaturesConfig.overviewMap?.hideOnZoom || 0; + + // get the values from store + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); + const hideOnZoom = useStore(getGeoViewStore(mapId), (state) => state.mapState.overviewMapHideZoom); + const displayLanguage = useStore(getGeoViewStore(mapId), (state) => state.displayLanguage); // TODO: remove useStyle const classes = useStyles(); - const handleProjectionChange = (payload: PayloadBaseClass) => { - if (payloadIsAMapViewProjection(payload)) { - const overviewMap = map - .getControls() - .getArray() - .filter((item) => { - return item instanceof OLOverviewMap; - })[0] as OLOverviewMap; - - // collapse the overview map, if not projection throw an error - overviewMap.setCollapsed(true); - overviewMap.setMap(null); - - // wait for the view change then set the map and open the overview - // TODO: look for better options then Timeout - setTimeout(() => { - overviewMap.setMap(api.maps[mapId].map); - - setTimeout(() => overviewMap.setCollapsed(false), 500); - }, 2000); - } + /** + * Return the map overview map + * @returns {OLOverviewMap} the OL overview map + */ + const getOverViewMap = (): OLOverviewMap => { + return mapElement + .getControls() + .getArray() + .filter((item) => { + return item instanceof OLOverviewMap; + })[0] as OLOverviewMap; }; useEffect(() => { @@ -139,30 +128,43 @@ export function OverviewMap(): JSX.Element { (state) => state.mapState.zoom, (curZoom, prevZoom) => { if (curZoom !== prevZoom) { - const overviewMap = map - .getControls() - .getArray() - .filter((item) => { - return item instanceof OLOverviewMap; - })[0] as OLOverviewMap; - if (curZoom! < hideOnZoom) overviewMap.setMap(null); - else overviewMap.setMap(api.maps[mapId].map); + if (curZoom! < hideOnZoom) getOverViewMap().setMap(null); + else getOverViewMap().setMap(mapElement); } } ); - // listen to geoview-basemap-panel package change projection event - api.event.on(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, handleProjectionChange, mapId); + // if projection change + const unsubMapCurrentProjection = getGeoViewStore(mapId).subscribe( + (state) => state.mapState.currentProjection, + (curProj, prevProj) => { + if (curProj !== prevProj) { + const overviewMap = getOverViewMap(); + + // collapse the overview map, if not projection throw an error + overviewMap.setCollapsed(true); + overviewMap.setMap(null); + + // wait for the view change then set the map and open the overview + // TODO: look for better options then Timeout + setTimeout(() => { + overviewMap.setMap(mapElement); + setTimeout(() => overviewMap.setCollapsed(false), 500); + }, 2000); + } + } + ); return () => { unsubMapZoom(); - api.event.off(EVENT_NAMES.MAP.EVENT_MAP_VIEW_PROJECTION_CHANGE, mapId, handleProjectionChange); + unsubMapCurrentProjection(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [mapId]); useEffect(() => { // get default overview map + // TODO: store basemap info in the store... const defaultBasemap = api.maps[mapId].basemap.overviewMap; const toggleButton = document.createElement('div'); @@ -188,13 +190,13 @@ export function OverviewMap(): JSX.Element { tipLabel: '', }); - map.addControl(overviewMapControl); - if (api.maps[mapId].map.getView().getZoom() && api.maps[mapId].map.getView().getZoom()! < hideOnZoom) overviewMapControl.setMap(null); + mapElement.addControl(overviewMapControl); + if (mapElement.getView().getZoom() && mapElement.getView().getZoom()! < hideOnZoom) overviewMapControl.setMap(null); // need to recreate the i18n instance as the overviewmap is a new map inside the main map const i18nInstance = i18n.cloneInstance({ - lng: mapFeaturesConfig.displayLanguage, - fallbackLng: mapFeaturesConfig.displayLanguage, + lng: displayLanguage, + fallbackLng: displayLanguage, }); const root = createRoot(toggleButton!); diff --git a/packages/geoview-core/src/core/components/scale/scale.tsx b/packages/geoview-core/src/core/components/scale/scale.tsx index c358c30e451..214b7b69244 100644 --- a/packages/geoview-core/src/core/components/scale/scale.tsx +++ b/packages/geoview-core/src/core/components/scale/scale.tsx @@ -8,7 +8,6 @@ import makeStyles from '@mui/styles/makeStyles'; import { useStore } from 'zustand'; import { getGeoViewStore } from '@/core/stores/stores-managers'; -import { api } from '@/app'; import { MapContext } from '@/core/app-start'; import { CheckIcon, Tooltip, Box } from '@/ui'; @@ -78,8 +77,9 @@ export function Scale(): JSX.Element { const [scaleGraphic, setScaleGraphic] = useState(''); const [scaleNumeric, setScaleNumeric] = useState(''); - // get the expand or collapse from store + // get the values from store const expanded = useStore(getGeoViewStore(mapId), (state) => state.footerBarState.expanded); + const mapElement = useStore(getGeoViewStore(mapId), (state) => state.mapState.mapElement); // TODO: remove make style const classes = useStyles(); @@ -106,8 +106,6 @@ export function Scale(): JSX.Element { ]; useEffect(() => { - const { map } = api.maps[mapId]; - const scaleBar = new ScaleLine({ units: 'metric', target: document.getElementById(`${mapId}-scaleControlBar`) as HTMLElement, @@ -120,14 +118,14 @@ export function Scale(): JSX.Element { target: document.getElementById(`${mapId}-scaleControlLine`) as HTMLElement, }); - map.addControl(scaleLine); - map.addControl(scaleBar); + mapElement.addControl(scaleLine); + mapElement.addControl(scaleBar); // if mapCenterCoordinates changed, map move end event has been triggered const unsubMapCenterCoord = getGeoViewStore(mapId).subscribe( (state) => state.mapState.mapCenterCoordinates, - (curCenterCoord, prevCenterCoord) => { - if (curCenterCoord !== prevCenterCoord) { + (curCoords, prevCoords) => { + if (curCoords !== prevCoords) { setLineWidth( (document.getElementById(`${mapId}-scaleControlLine`)?.querySelector('.ol-scale-line-inner') as HTMLElement)?.style .width as string @@ -135,12 +133,15 @@ export function Scale(): JSX.Element { setScaleGraphic(document.getElementById(`${mapId}-scaleControlLine`)?.querySelector('.ol-scale-line-inner')?.innerHTML as string); setScaleNumeric(document.getElementById(`${mapId}-scaleControlBar`)?.querySelector('.ol-scale-text')?.innerHTML as string); } + }, + { + fireImmediately: true, } ); return () => { - map.removeControl(scaleLine); - map.removeControl(scaleBar); + mapElement.removeControl(scaleLine); + mapElement.removeControl(scaleBar); unsubMapCenterCoord(); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/geoview-core/src/core/containers/shell.tsx b/packages/geoview-core/src/core/containers/shell.tsx index e2b1d3a6de7..e619818b5b7 100644 --- a/packages/geoview-core/src/core/containers/shell.tsx +++ b/packages/geoview-core/src/core/containers/shell.tsx @@ -5,20 +5,22 @@ import { useTranslation } from 'react-i18next'; import FocusTrap from 'focus-trap-react'; +import { useStore } from 'zustand'; import makeStyles from '@mui/styles/makeStyles'; +import { getGeoViewStore } from '@/core/stores/stores-managers'; -import { Map } from '../components/map/map'; -import { Appbar } from '../components/app-bar/app-bar'; -import { Navbar } from '../components/nav-bar/nav-bar'; -import { FooterTabs } from '../components/footer-tabs/footer-tabs'; -import { Geolocator } from '../components/geolocator/geolocator'; +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'; +import { FooterTabs } from '@/core/components/footer-tabs/footer-tabs'; +import { Geolocator } from '@/core/components/geolocator/geolocator'; import { FocusTrapDialog } from './focus-trap'; import { api } from '@/app'; import { EVENT_NAMES } from '@/api/events/event-types'; -import { Modal, Snackbar } from '@/ui'; +import { CircularProgress, Modal, Snackbar } from '@/ui'; import { PayloadBaseClass, mapConfigPayload, @@ -96,6 +98,9 @@ export function Shell(props: ShellProps): JSX.Element { const [update, setUpdate] = useState(0); + // get values from the store + const mapLoaded = useStore(getGeoViewStore(mapFeaturesConfig.mapId), (state) => state.mapState.mapLoaded); + /** * Set the focus trap * @param {boolean} dialogTrap the callback value from dialog trap @@ -169,6 +174,7 @@ export function Shell(props: ShellProps): JSX.Element { return (
+ {t('keyboardnav.start')} diff --git a/packages/geoview-core/src/core/stores/geoview-store.ts b/packages/geoview-core/src/core/stores/geoview-store.ts index 3ef37cedf13..925fe21121d 100644 --- a/packages/geoview-core/src/core/stores/geoview-store.ts +++ b/packages/geoview-core/src/core/stores/geoview-store.ts @@ -7,28 +7,36 @@ import debounce from 'lodash/debounce'; Argument of type 'import("C:/Users/jolevesq/Sites/geoview/common/temp/node_modules/.pnpm/ol@7.5.2/node_modules/ol/Map").default' is not assignable to parameter of type 'import("C:/Users/jolevesq/Sites/geoview/common/temp/node_modules/.pnpm/ol@7.5.1/node_modules/ol/Map").default'. Types of property 'on' are incompatible. */ -import { Map as OLMap, MapEvent, MapBrowserEvent } from 'ol'; +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 { TypeMapFeaturesConfig } from '@/core/types/global-types'; +import { TypeMapFeaturesConfig, TypeValidMapProjectionCodes } from '@/core/types/global-types'; import { TypeLegendItemProps } from '../components/legend-2/types'; import { TypeMapMouseInfo } from '@/api/events/payloads'; -import { TypeInteraction } from '@/geo/map/map-schema-types'; +import { TypeDisplayLanguage, TypeInteraction } from '@/geo/map/map-schema-types'; +import { NotificationDetailsType } from '@/core/types/cgpv-types'; export interface IMapState { - currentProjection: number; + currentProjection: TypeValidMapProjectionCodes; + fixNorth: boolean; + interaction: TypeInteraction; pointerPosition: TypeMapMouseInfo | undefined; mapCenterCoordinates: Coordinate; mapClickCoordinates: TypeMapMouseInfo | undefined; - mapElement?: OLMap; + mapElement: OLMap; mapLoaded: boolean; + mapRotation: number; + northArrow: boolean; + overviewMap: boolean; + overviewMapHideZoom: number; zoom?: number | undefined; onMapMoveEnd: (event: MapEvent) => void; onMapPointerMove: (event: MapEvent) => void; + onMapRotation: (event: ObjectEvent) => void; onMapSingleClick: (event: MapEvent) => void; onMapZoomEnd: (event: ObjectEvent) => void; } @@ -46,7 +54,9 @@ export interface IAppBarState { // export interface INavBarState {} -// export interface INotificationsState {} +export interface INotificationsState { + notifications: Array; +} // export interface IMapDataTableState {} @@ -59,16 +69,16 @@ export interface ILegendState { } export interface IGeoViewState { + displayLanguage: TypeDisplayLanguage; + isCrosshairsActive: boolean; mapId: string; mapConfig: TypeMapFeaturesConfig | undefined; - interaction: TypeInteraction; - mapState: IMapState; - footerBarState: IFooterBarState; appBarState: IAppBarState; + footerBarState: IFooterBarState; legendState: ILegendState; - - isCrosshairsActive: boolean; + mapState: IMapState; + notificationState: INotificationsState; setMapConfig: (config: TypeMapFeaturesConfig) => void; onMapLoaded: (mapElem: OLMap) => void; @@ -82,23 +92,27 @@ export const geoViewStoreDefinition = ( get: () => IGeoViewState ) => ({ + displayLanguage: 'en', mapId: '', mapConfig: undefined, - interaction: 'dynamic', isCrosshairsActive: false, mapState: { + fixNorth: false, mapLoaded: false, mapCenterCoordinates: [0, 0] as Coordinate, + mapRotation: 0, + overviewMapHideZoom: 0, pointerPosition: undefined, currentProjection: 3857, - onMapMoveEnd: (event: MapEvent) => { + zoom: undefined, + onMapMoveEnd: debounce((event: MapEvent) => { set({ mapState: { ...get().mapState, mapCenterCoordinates: event.map.getView().getCenter()!, }, }); - }, + }, 100), onMapPointerMove: debounce((event: MapEvent) => { set({ mapState: { @@ -112,6 +126,14 @@ export const geoViewStoreDefinition = ( }, }); }, 10), + onMapRotation: debounce((event: ObjectEvent) => { + set({ + mapState: { + ...get().mapState, + mapRotation: (event.target as View).getRotation(), + }, + }); + }, 100), onMapSingleClick: (event: MapEvent) => { set({ mapState: { @@ -125,14 +147,14 @@ export const geoViewStoreDefinition = ( }, }); }, - onMapZoomEnd: (event: ObjectEvent) => { + onMapZoomEnd: debounce((event: ObjectEvent) => { set({ mapState: { ...get().mapState, zoom: event.target.getZoom()!, }, }); - }, + }, 100), }, footerBarState: { expanded: false, @@ -143,9 +165,12 @@ export const geoViewStoreDefinition = ( legendState: { selectedItem: undefined, }, + notificationState: { + notifications: [], + }, setMapConfig: (config: TypeMapFeaturesConfig) => { - set({ mapConfig: config, mapId: config.mapId }); + set({ mapConfig: config, mapId: config.mapId, displayLanguage: config.displayLanguage }); }, onMapLoaded: (mapElem: OLMap) => { diff --git a/packages/geoview-core/src/core/stores/stores-managers.ts b/packages/geoview-core/src/core/stores/stores-managers.ts index 103bc8493e6..2160f013035 100644 --- a/packages/geoview-core/src/core/stores/stores-managers.ts +++ b/packages/geoview-core/src/core/stores/stores-managers.ts @@ -18,6 +18,19 @@ export const addGeoViewStore = (config: TypeMapFeaturesConfig) => { } const geoViewStore = create()(geoViewStoreDefinitionWithSubscribeSelector); geoViewStore.getState().setMapConfig(config); + + // initialize static initial value from config before the event processor (config has been validated already) + geoViewStore.setState({ + mapState: { + ...geoViewStore.getState().mapState, + currentProjection: config.map.viewSettings.projection, + interaction: config.map.interaction, + northArrow: config.components!.indexOf('north-arrow') > -1, + overviewMap: config.components!.indexOf('overview-map') > -1, + overviewMapHideZoom: config.overviewMap !== undefined ? config.overviewMap.hideOnZoom : 0, + }, + }); + initializeEventProcessors(geoViewStore); useStoresManager.setState((state) => ({ diff --git a/packages/geoview-core/src/geo/map/map.ts b/packages/geoview-core/src/geo/map/map.ts index e8e38a50f43..07601b522d3 100644 --- a/packages/geoview-core/src/geo/map/map.ts +++ b/packages/geoview-core/src/geo/map/map.ts @@ -20,7 +20,6 @@ import { EVENT_NAMES } from '@/api/events/event-types'; import { AppbarButtons } from '@/core/components/app-bar/app-bar-buttons'; import { NavbarButtons } from '@/core/components/nav-bar/nav-bar-buttons'; import { FooterTabsApi } from '@/core/components/footer-tabs/footer-tabs-api'; -import { NotificationsApi } from '@/core/components/notifications/notifications-api'; import { LegendApi } from '@/core/components/legend/legend-api'; import { Legend2Api } from '@/core/components/legend-2/legend-api'; import { DetailsAPI } from '@/core/components/details/details-api'; @@ -82,9 +81,6 @@ export class MapViewer { // used to access the footer tabs api footerTabs!: FooterTabsApi; - // used to access the notifications api - notifications!: NotificationsApi; - // used to access the legend api legend!: LegendApi; @@ -157,7 +153,7 @@ export class MapViewer { this.currentProjection = mapFeaturesConfig.map.viewSettings.projection; this.i18nInstance = i18instance; this.currentZoom = mapFeaturesConfig.map.viewSettings.zoom; - this.mapCenterCoordinates = [mapFeaturesConfig.map.viewSettings.center[0], mapFeaturesConfig.map.viewSettings.center[1]]; + this.mapCenterCoordinates = [0, 0]; // [mapFeaturesConfig.map.viewSettings.center[0], mapFeaturesConfig.map.viewSettings.center[1]]; this.singleClickedPosition = { pixel: [], lnglat: [], projected: [], dragging: false }; this.pointerPosition = { pixel: [], lnglat: [], projected: [], dragging: false }; @@ -402,19 +398,29 @@ export class MapViewer { this.map.on('pointermove', store.getState().mapState.onMapPointerMove); this.map.on('singleclick', store.getState().mapState.onMapSingleClick); this.map.getView().on('change:resolution', store.getState().mapState.onMapZoomEnd); + this.map.getView().on('change:rotation', store.getState().mapState.onMapRotation); // initialize map state store.setState({ mapState: { ...store.getState().mapState, - currentProjection: this.currentProjection, - mapCenterCoordinates: this.map.getView().getCenter()!, mapLoaded: true, mapElement: this.map, zoom: this.map.getView().getZoom(), }, }); + // when map is just created, some controls (i.e. scale) are not fully initialized + // trigger the store component update after a small latency + setTimeout(() => { + store.setState({ + mapState: { + ...store.getState().mapState, + mapCenterCoordinates: this.map.getView().getCenter()!, + }, + }); + }, 100); + clearInterval(layerInterval); } } diff --git a/packages/geoview-core/src/ui/circular-progress/circular-progress-style.ts b/packages/geoview-core/src/ui/circular-progress/circular-progress-style.ts new file mode 100644 index 00000000000..c0a622f47c1 --- /dev/null +++ b/packages/geoview-core/src/ui/circular-progress/circular-progress-style.ts @@ -0,0 +1,27 @@ +import { Theme } from '@mui/material/styles'; + +export const getSxClasses = (theme: Theme) => ({ + loading: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + top: '0px', + bottom: '0px', + left: '0px', + right: '0px', + zIndex: 10000, + backgroundColor: '#000000', + textAlign: 'center', + transition: theme.transitions.create(['visibility', 'opacity'], { + delay: theme.transitions.duration.shortest, + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.splash, + }), + }, + progress: { + width: '100px !important', + height: '100px !important', + position: 'absolute', + }, +}); diff --git a/packages/geoview-core/src/ui/circular-progress/circular-progress.tsx b/packages/geoview-core/src/ui/circular-progress/circular-progress.tsx index aaab3b6444a..20ea1fe18a5 100644 --- a/packages/geoview-core/src/ui/circular-progress/circular-progress.tsx +++ b/packages/geoview-core/src/ui/circular-progress/circular-progress.tsx @@ -1,14 +1,13 @@ /* eslint-disable react/require-default-props */ import { CSSProperties } from 'react'; -import { CircularProgress as MaterialCircularProgress, CircularProgressProps, Box } from '@mui/material'; - +import { CircularProgress as MaterialCircularProgress, CircularProgressProps, Box, useTheme } from '@mui/material'; +import { getSxClasses } from './circular-progress-style'; /** * Circular Progress Properties */ interface TypeCircularProgressProps extends CircularProgressProps { - className?: string; - style?: CSSProperties; isLoaded: boolean; + style?: CSSProperties; } /** @@ -18,11 +17,14 @@ interface TypeCircularProgressProps extends CircularProgressProps { * @returns {JSX.Element} the created Circular Progress element */ export function CircularProgress(props: TypeCircularProgressProps): JSX.Element { - const { className, style = {}, isLoaded } = props; + const { style = {}, isLoaded } = props; + + const theme = useTheme(); + const sxClasses = getSxClasses(theme); - return isLoaded ? ( - - + return !isLoaded ? ( + + ) : ( // eslint-disable-next-line react/jsx-no-useless-fragment