diff --git a/packages/geoview-core/public/configs/navigator/03-projection-WM.json b/packages/geoview-core/public/configs/navigator/03-projection-WM.json
index e685a9a6772..bbf8e189bd5 100644
--- a/packages/geoview-core/public/configs/navigator/03-projection-WM.json
+++ b/packages/geoview-core/public/configs/navigator/03-projection-WM.json
@@ -2,9 +2,6 @@
"map": {
"interaction": "dynamic",
"viewSettings": {
- "initialView": {
- "extent": [-90, 45, -65, 70]
- },
"projection": 3857
},
"basemapOptions": {
diff --git a/packages/geoview-core/public/configs/navigator/04-a-max-extent-override.json b/packages/geoview-core/public/configs/navigator/04-a-max-extent-override.json
new file mode 100644
index 00000000000..881928da873
--- /dev/null
+++ b/packages/geoview-core/public/configs/navigator/04-a-max-extent-override.json
@@ -0,0 +1,37 @@
+{
+ "map": {
+ "interaction": "dynamic",
+ "viewSettings": {
+ "maxExtent": [-180, -50, 180, 89],
+ "projection": 3857
+ },
+ "basemapOptions": {
+ "basemapId": "transport",
+ "shaded": true,
+ "labeled": true
+ },
+ "listOfGeoviewLayerConfig": [
+ {
+ "geoviewLayerId": "472ef86d-7f7c-423b-a7d2-b6f92b79fd6d",
+ "geoviewLayerType": "geoCore"
+ }
+ ]
+ },
+ "components": [
+ "overview-map",
+ "north-arrow"
+ ],
+ "navBar": ["home", "projection"],
+ "footerBar": {
+ "tabs": {
+ "core": [
+ "legend",
+ "layers",
+ "details",
+ "data-table"
+ ]
+ }
+ },
+ "corePackages": [],
+ "theme": "geo.ca"
+}
\ No newline at end of file
diff --git a/packages/geoview-core/public/img/guide/navigation/basemapSelect.svg b/packages/geoview-core/public/img/guide/navigation/basemapSelect.svg
new file mode 100644
index 00000000000..3bdf176584e
--- /dev/null
+++ b/packages/geoview-core/public/img/guide/navigation/basemapSelect.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/packages/geoview-core/public/img/guide/navigation/projection.svg b/packages/geoview-core/public/img/guide/navigation/projection.svg
new file mode 100644
index 00000000000..71099d535b0
--- /dev/null
+++ b/packages/geoview-core/public/img/guide/navigation/projection.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/geoview-core/public/locales/en/guide.md b/packages/geoview-core/public/locales/en/guide.md
index 0939fdf52e7..91151e4e84d 100644
--- a/packages/geoview-core/public/locales/en/guide.md
+++ b/packages/geoview-core/public/locales/en/guide.md
@@ -13,6 +13,8 @@ The following navigation controls can be found in the bottom right corner of the
| | Zoom out | Zoom out one level on the map to see less detailed content - bound to Minus key (-) |
| | Geolocation | Zoom and pan to your current geographical location |
| | Initial extent | Zoom and pan map such that initial extent is visible |
+| | Change Basemap | Change the basemap
+| | Change Projection | Change the map projection between Web Mercator and LCC
You can also pan the map by using your left, right, up and down arrow keys, or by click-holding on the map and dragging. Using the mouse scroll wheel while hovering over the map will zoom the map in/out.
diff --git a/packages/geoview-core/public/locales/en/translation.json b/packages/geoview-core/public/locales/en/translation.json
index 48a8518b0c7..1a9c32ad35a 100644
--- a/packages/geoview-core/public/locales/en/translation.json
+++ b/packages/geoview-core/public/locales/en/translation.json
@@ -26,7 +26,8 @@
"zoomOut": "Zoom out",
"coordinates": "Toggle coordinates format",
"scale": "Toggle between scale and resolution",
- "location": "Zoom to my location"
+ "location": "Zoom to my location",
+ "projection": "Change map projection"
},
"basemaps": {
"select": "Select a basemap",
@@ -60,7 +61,7 @@
"appbar": {
"export": "Download map",
"notifications": "Notification",
- "no_notifications_available": "No notifications available",
+ "noNotificationsAvailable": "No notifications available",
"layers": "Layers",
"share": "Share",
"version": "About GeoView",
@@ -106,7 +107,7 @@
"layerSelect": "Select layer(s)",
"errorEmpty": "cannot be empty",
"errorNone": "No file or source added",
- "errorFile": "Only geoJSON, CSV and GeoPackage files can be used",
+ "errorFile": "Only geoJSON and CSV files can be used",
"errorServer": "source is not valid",
"errorNotLoaded": "An error occured when loading the layer",
"errorProj": "does not support current map projection",
@@ -160,15 +161,15 @@
},
"validation": {
"layer": {
- "loadfailed": "Layer [__param__] failed to load on map __param__.",
- "notfound": "The sublayer __param__ of the layer __param__ does not exist on the server",
- "createtwice": "Can not execute the createGeoViewRasterLayers method twice for the layer __param__ on map __param__",
- "usedtwice": "Duplicate use of layer identifier [__param__] on map __param__",
+ "loadfailed": "Layer __param__ failed to load on map.",
+ "notfound": "The sublayer __param__ of the layer __param__ does not exist on the server",
+ "createtwice": "Can not execute the createGeoViewRasterLayers method twice for the layer __param__ on map",
+ "usedtwice": "Duplicate use of layer identifier __param__ on map",
"multipleUUID": "GeoCore layers may only have one GeoCore id per layer"
},
"schema": {
"notFound": "A schema error was found, check the console to see what is wrong.",
- "wrongPath": "Cannot find schema ([__param__])"
+ "wrongPath": "Cannot find schema (__param__)"
},
"changeDisplayLanguageLayers": "Layers can not be relaoded because the configuration does not support this language",
"changeDisplayLanguage": "Only 'en' and 'fr' are supported",
@@ -187,7 +188,7 @@
"geolocator": {
"title": "Geolocator",
"search": "Search",
- "errorMessage": "No matches found for",
+ "noResult": "No matches found for",
"province": "Province",
"category": "Category",
"clearFilters": "Clear filters",
@@ -227,5 +228,24 @@
"footerBar": {
"resizeTooltip": "Resize",
"noTab": "No tab"
+ },
+ "error": {
+ "metadata": {
+ "unableRead": "Unable to read metadata",
+ "empty": "Value returned is empty",
+ "capability": "Value returned doesn't contain Capability, Layer or is empty"
+ },
+ "layer": {
+ "createGroup": "Unable to create group layer",
+ "emptyGroup": "Empty layer group",
+ "esriId": "ESRI layerId must be a number",
+ "esriIdNotFound": "ESRI layerId not found"
+ },
+ "geocore": {
+ "noLayer": "No layers returned by GeoCore service"
+ },
+ "geolocator": {
+ "noService": "Geolocator service not available"
+ }
}
}
\ No newline at end of file
diff --git a/packages/geoview-core/public/locales/fr/guide.md b/packages/geoview-core/public/locales/fr/guide.md
index ef3e047612d..e1d10d9bf04 100644
--- a/packages/geoview-core/public/locales/fr/guide.md
+++ b/packages/geoview-core/public/locales/fr/guide.md
@@ -13,6 +13,8 @@ On trouve les commandes suivantes dans le coin inférieur droit de la carte 
| | Zoom arrière| Permet de faire un zoom arrière d’un niveau à la fois pour voir le contenu moins en détail; fonctionne aussi avec la touche de soustraction du clavier (-).|
| | Géolocalisation| Permet de zoomer et de déplacer la carte sur votre position géographique.|
| | Vue initiale| Permet de zoomer et de déplacer la carte pour retourner à la vue initiale.|
+| | Changer la carte de base | Changer la carte de base
+| | Changer la projection | Changer la projection de la carte entre Web Mercator et LCC
Vous pouvez aussi déplacer la carte avec les touches fléchées vers la gauche, la droite, le haut et le bas, ou en cliquant sur la carte, puis en la glissant. Lorsque le pointeur est sur la carte, la molette de la souris permet de faire un zoom avant et arrière.
diff --git a/packages/geoview-core/public/locales/fr/translation.json b/packages/geoview-core/public/locales/fr/translation.json
index 09f55172a6c..7f2b9bbe059 100644
--- a/packages/geoview-core/public/locales/fr/translation.json
+++ b/packages/geoview-core/public/locales/fr/translation.json
@@ -26,7 +26,8 @@
"zoomOut": "Zoom arrière",
"coordinates": "Basculer le format des coordonnées",
"scale": "Basculer entre l'échelle et la résolution",
- "location": "Zoom sur ma position"
+ "location": "Zoom sur ma position",
+ "projection": "Changer la projection de la carte"
},
"basemaps": {
"select": "Choisir une carte de base",
@@ -60,7 +61,7 @@
"appbar": {
"export": "Télécharger la carte",
"notifications": "Notification",
- "no_notifications_available": "Aucune notification disponible",
+ "noNotificationsAvailable": "Aucune notification disponible",
"layers": "Couches",
"share": "Partager",
"version": "À propos de GéoView",
@@ -106,11 +107,11 @@
"layerSelect": "Sélectionner couche(s)",
"errorEmpty": "ne peut être vide",
"errorNone": "Pas de fichier ou de source ajouté",
- "errorFile": "Seuls les fichiers geoJSON, CSV et GeoPackage peuvent être utilisés",
+ "errorFile": "Seuls les fichiers geoJSON et CSV peuvent être utilisés",
"errorServer": "source n'est pas valide",
"errorNotLoaded": "Une erreur s'est produite lors du chargement de la couche",
"errorProj": "ne prend pas en charge la projection cartographique actuelle",
- "errorImageLoad": "Erreur de chargement de l'image source pour le couche: __param__ au niveau de zoom __param__",
+ "errorImageLoad": "Erreur de chargement de l'image source pour la couche: __param__ au niveau de zoom __param__",
"only": "seulement",
"opacity": "Opacité",
"opacityMax": "Maximum du parent",
@@ -120,9 +121,9 @@
"toggleAllVisibility": "Basculer toute les visibilités",
"toggleCollapse": "Basculer la fermeture",
"querying": "Requête en cours",
- "layerAdded": "Couche __param__ ajoutée",
+ "layerAdded": "Couche __param__ ajoutée",
"layerAddedAndLoading": "Couche __param__ ajoutée et en chargement",
- "layerAddedWithError": "Couche __param__ en erreur",
+ "layerAddedWithError": "Couche __param__ en erreur",
"instructionsNoLayersTitle": "Aucune couche visible",
"instructionsNoLayersBody": "Ajoutez des couches visibles sur la carte."
},
@@ -160,15 +161,15 @@
},
"validation": {
"layer": {
- "loadfailed": "Le chargement de la couche [__param__] a échoué sur la carte __param__.",
+ "loadfailed": "Le chargement de la couche __param__ a échoué sur la carte.",
"notfound": "La sous couche __param__ de la couche __param__ n'existe pas sur le sereur",
- "createtwice": "On ne peut exécuter deux fois la méthode createGeoViewRasterLayers pour la couche __param__ sur la carte __param__",
- "usedtwice": "Utilisation en double de l'identifiant de couche [__param__] sur la carte __param__",
+ "createtwice": "On ne peut exécuter deux fois la méthode createGeoViewRasterLayers pour la couche __param__ sur la carte",
+ "usedtwice": "Utilisation en double de l'identifiant de couche __param__ sur la carte",
"multipleUUID": "Les couches GeoCore ne peuvent avoir qu'un seul identifiant par couche."
},
"schema": {
"notFound": "Une erreur de schéma a été trouvée, vérifiez la console pour voir ce qui ne va pas.",
- "wrongPath": "Impossible de trouver le schéma ([__param__])"
+ "wrongPath": "Impossible de trouver le schéma (__param__)"
},
"changeDisplayLanguageLayers": "Les couches ne peuvent être chargée(s) de nouveau car la configuration ne supporte pas ce langage",
"changeDisplayLanguage": "Seulement 'en' et 'fr' sont supporées",
@@ -187,7 +188,7 @@
"geolocator": {
"title": "Géolocalisation",
"search": "Texte à rechercher",
- "errorMessage": "Aucun résultat correspondant à",
+ "noResult": "Aucun résultat correspondant à",
"province": "Province",
"category": "Catégorie",
"clearFilters": "Effacer les filtres",
@@ -226,5 +227,24 @@
"footerBar": {
"resizeTooltip": "Redimensionner",
"noTab": "Pas d'onglet"
+ },
+ "error": {
+ "metadata": {
+ "unableRead": "Impossible de lire les métadonnées",
+ "empty": "La valeur renvoyée est vide",
+ "capability": "La valeur renvoyée ne contient pas de Capability, de Layer ou est vide"
+ },
+ "layer": {
+ "createGroup": "Impossible de créer une couche de groupe",
+ "emptyGroup": "Groupe de couches vide",
+ "esriId": "Le layerId ESRI doit être un nombre",
+ "esriIdNotFound": "Le layerId ESRI introuvable"
+ },
+ "geocore": {
+ "noLayer": "Aucune couche renvoyée par le service GeoCore"
+ },
+ "geolocator": {
+ "noService": "Service de géolocalisation non disponible"
+ }
}
}
\ No newline at end of file
diff --git a/packages/geoview-core/public/templates/add-layers.html b/packages/geoview-core/public/templates/add-layers.html
index 8e0237d01d9..a9716aa486c 100644
--- a/packages/geoview-core/public/templates/add-layers.html
+++ b/packages/geoview-core/public/templates/add-layers.html
@@ -97,6 +97,7 @@
1. Default Configuration
},
'corePackages': [],
'externalPackages': [],
+ 'navBar': ['home', 'basemap-select', 'projection', 'fullscreen'],
'appBar': {
'tabs': {
'core': ['legend', 'layers', 'details', 'data-table', 'geochart']
@@ -168,7 +169,7 @@ Add Layer Examples
- GeoPackage Layer
+ GeoPackage Layer - DEPRECATED
https://canadian-geospatial-platform.github.io/geoview/public/datasets/geopackages/rivers.gpkg
diff --git a/packages/geoview-core/public/templates/demos-navigator.html b/packages/geoview-core/public/templates/demos-navigator.html
index 038f57440ea..df3f64a1cab 100644
--- a/packages/geoview-core/public/templates/demos-navigator.html
+++ b/packages/geoview-core/public/templates/demos-navigator.html
@@ -124,6 +124,7 @@
Configurations Navigator
Basemap WM
Restricted zoom [4, 8]
+
Unrestricted zoom map extent for WM zoom north
Zoom on layer extent
Layer Max and Min Zoom Settings
Basic map with footer
@@ -151,7 +152,7 @@
Configurations Navigator
Layer GeoView - GeoCore -
Layer GeoView - GeoCore Custom Inline Config -
Layer GeoView - GeoCore Custom Config -
-
Layer GeoView - GeoPackages -
+
Layer GeoView - GeoPackages DEPRECATED-
diff --git a/packages/geoview-core/schema.json b/packages/geoview-core/schema.json
index 3c2c437c6b6..2465646c22c 100644
--- a/packages/geoview-core/schema.json
+++ b/packages/geoview-core/schema.json
@@ -1739,13 +1739,13 @@
"type": "integer",
"description": "The minimum zoom level used to determine the resolution constraint. If not set, will use default from basemap.",
"minimum": 0,
- "maximum": 50
+ "maximum": 20
},
"maxZoom": {
"type": "integer",
"description": "The maximum zoom level used to determine the resolution constraint. If not set, will use default from basemap.",
"minimum": 0,
- "maximum": 50
+ "maximum": 20
},
"projection": {
"$ref": "#/definitions/TypeValidMapProjectionCodes"
diff --git a/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts b/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
index 2c51be1269b..5823a01fa1c 100644
--- a/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
@@ -21,6 +21,7 @@ import {
ACCEPTED_SCHEMA_VERSIONS,
VALID_PROJECTION_CODES,
CV_MAP_CENTER,
+ CV_VALID_ZOOM_LEVELS,
} from '@config/types/config-constants';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import {
@@ -237,11 +238,15 @@ export class MapFeatureConfig {
: CV_DEFAULT_MAP_FEATURE_CONFIG.schemaVersionUsed!;
const minZoom = this.map.viewSettings.minZoom!;
this.map.viewSettings.minZoom =
- !Number.isNaN(minZoom) && minZoom >= 0 && minZoom <= 50 ? minZoom : CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.minZoom;
+ !Number.isNaN(minZoom) && minZoom >= CV_VALID_ZOOM_LEVELS[0] && minZoom <= CV_VALID_ZOOM_LEVELS[1]
+ ? minZoom
+ : CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.minZoom;
const maxZoom = this.map.viewSettings.maxZoom!;
this.map.viewSettings.maxZoom =
- !Number.isNaN(maxZoom) && maxZoom >= 0 && maxZoom <= 50 ? maxZoom : CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.maxZoom;
+ !Number.isNaN(maxZoom) && maxZoom >= CV_VALID_ZOOM_LEVELS[0] && maxZoom <= CV_VALID_ZOOM_LEVELS[1]
+ ? maxZoom
+ : CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.maxZoom;
if (this.map.viewSettings.initialView!.zoomAndCenter) this.#validateMaxExtent();
this.#logModifs(providedMapConfig);
diff --git a/packages/geoview-core/src/api/config/types/config-constants.ts b/packages/geoview-core/src/api/config/types/config-constants.ts
index 17052c0a063..4da7ea16658 100644
--- a/packages/geoview-core/src/api/config/types/config-constants.ts
+++ b/packages/geoview-core/src/api/config/types/config-constants.ts
@@ -136,14 +136,17 @@ export const CV_VALID_MAP_CENTER: Record = {
- 3857: [-170, 35, -20, 84],
+ 3857: [-180, 0, 80, 84],
3978: [-135, 25, -50, 89],
};
export const CV_MAP_CENTER: Record = {
- 3857: [-90, 55],
+ 3857: [-90, 67],
3978: [-90, 60],
};
+// valid zoom levels from each projection
+export const CV_VALID_ZOOM_LEVELS: number[] = [0, 20];
+
/**
* Definition of the MapFeatureConfig default values. All the default values that applies to the map feature configuration are
* defined here.
@@ -170,8 +173,8 @@ export const CV_DEFAULT_MAP_FEATURE_CONFIG = Cast({
},
enableRotation: true,
rotation: 0,
- minZoom: 0,
- maxZoom: 50,
+ minZoom: CV_VALID_ZOOM_LEVELS[0],
+ maxZoom: CV_VALID_ZOOM_LEVELS[1],
maxExtent: CV_MAP_EXTENTS[3978],
projection: 3978,
},
diff --git a/packages/geoview-core/src/api/config/types/config-validation-schema.json b/packages/geoview-core/src/api/config/types/config-validation-schema.json
index e18d936356e..cbf315dc555 100644
--- a/packages/geoview-core/src/api/config/types/config-validation-schema.json
+++ b/packages/geoview-core/src/api/config/types/config-validation-schema.json
@@ -735,13 +735,13 @@
"description": "The minimum view zoom level (exclusive) above which this layer will be visible.",
"type": "integer",
"minimum": 0,
- "maximum": 50
+ "maximum": 20
},
"maxZoom": {
"description": "The maximum view zoom level (inclusive) above which this layer will be visible.",
"type": "integer",
"minimum": 0,
- "maximum": 50
+ "maximum": 20
},
"className": {
"description": "A CSS class name to set to the layer element.",
diff --git a/packages/geoview-core/src/api/config/types/map-schema-types.ts b/packages/geoview-core/src/api/config/types/map-schema-types.ts
index f429a691933..cd857d76072 100644
--- a/packages/geoview-core/src/api/config/types/map-schema-types.ts
+++ b/packages/geoview-core/src/api/config/types/map-schema-types.ts
@@ -23,7 +23,7 @@ export { MapFeatureConfig } from '@config/types/classes/map-feature-config';
export type TypeDisplayTheme = 'dark' | 'light' | 'geo.ca';
/** Valid values for the navBar array. */
-export type TypeValidNavBarProps = 'zoom' | 'fullscreen' | 'home' | 'location' | 'basemap-select';
+export type TypeValidNavBarProps = 'zoom' | 'fullscreen' | 'home' | 'location' | 'basemap-select' | 'projection';
/** Controls available on the navigation bar. Default = ['zoom', 'fullscreen', 'home', 'basemap-select]. */
export type TypeNavBarProps = TypeValidNavBarProps[];
@@ -194,12 +194,12 @@ export type TypeViewSettings = {
maxExtent?: Extent;
/**
* The minimum zoom level used to determine the resolution constraint. If not set, will use default from basemap.
- * Domain = [0..50].
+ * Domain = [0..20].
*/
minZoom?: number;
/**
* The maximum zoom level used to determine the resolution constraint. If not set, will use default from basemap.
- * Domain = [0..50].
+ * Domain = [0..20].
*/
maxZoom?: number;
/**
diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/app-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/app-event-processor.ts
index 884d954cc41..53fb992ec40 100644
--- a/packages/geoview-core/src/api/event-processors/event-processor-children/app-event-processor.ts
+++ b/packages/geoview-core/src/api/event-processors/event-processor-children/app-event-processor.ts
@@ -59,24 +59,25 @@ export class AppEventProcessor extends AbstractEventProcessor {
}
/**
- * Adds a snackbar message.
+ * Adds a snackbar message (optional add to notification).
* @param {SnackbarType} type - The type of message.
* @param {string} message - The message.
* @param {string} param - Optional param to replace in the string if it is a key
+ * @param {boolean} notification - True if we add the message to notification panel (default false)
*/
- static addMessage(mapId: string, type: SnackbarType, message: string, param?: string[]): void {
+ static addMessage(mapId: string, type: SnackbarType, message: string, param?: string[], notification = false): void {
switch (type) {
case 'info':
- api.maps[mapId].notifications.showMessage(message, param, false);
+ api.maps[mapId].notifications.showMessage(message, param, notification);
break;
case 'success':
- api.maps[mapId].notifications.showSuccess(message, param, false);
+ api.maps[mapId].notifications.showSuccess(message, param, notification);
break;
case 'warning':
- api.maps[mapId].notifications.showWarning(message, param, false);
+ api.maps[mapId].notifications.showWarning(message, param, notification);
break;
case 'error':
- api.maps[mapId].notifications.showError(message, param, false);
+ api.maps[mapId].notifications.showError(message, param, notification);
break;
default:
break;
@@ -84,7 +85,7 @@ export class AppEventProcessor extends AbstractEventProcessor {
}
static async addNotification(mapId: string, notif: NotificationDetailsType): Promise {
- // because notification is called before map is created, we use the async
+ // Because notification is called before map is created, we use the async
// version of getAppStateAsync
const appState = await this.getAppStateAsync(mapId);
const curNotifications = appState.notifications;
@@ -130,6 +131,9 @@ export class AppEventProcessor extends AbstractEventProcessor {
// load guide in new language
const promiseSetGuide = AppEventProcessor.setGuide(mapId);
+ // Remove all previous notifications to ensure there is no mix en and fr
+ AppEventProcessor.removeAllNotifications(mapId);
+
// When all promises are done
Promise.all([promiseChangeLanguage, promiseResetBasemap, promiseSetGuide])
.then(() => {
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 abf8ab98fb6..d1727e7e964 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
@@ -11,7 +11,7 @@ import {
IconButtonPropsExtend,
QuestionMarkIcon,
InfoOutlinedIcon,
- HubOutlinedIcon,
+ LegendIcon,
StorageIcon,
SearchIcon,
LayersOutlinedIcon,
@@ -111,7 +111,7 @@ export function AppBar(props: AppBarProps): JSX.Element {
geolocator: { icon: , content: },
guide: { icon: , content: },
details: { icon: , content: },
- legend: { icon: , content: },
+ legend: { icon: , content: },
layers: { icon: , content: },
'data-table': { icon: , content: },
} as unknown as Record;
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 791adcc9a7c..7d2d950cff6 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
@@ -22,7 +22,7 @@ import { AbstractPlugin } from '@/api/plugin/abstract-plugin';
import { useGeoViewConfig, useGeoViewMapId } from '@/core/stores/geoview-store';
// default tabs icon and class
-import { HubOutlinedIcon, InfoOutlinedIcon, LayersOutlinedIcon, StorageIcon, QuestionMarkIcon } from '@/ui/icons';
+import { LegendIcon, InfoOutlinedIcon, LayersOutlinedIcon, StorageIcon, QuestionMarkIcon } from '@/ui/icons';
import { Legend } from '@/core/components/legend/legend';
import { LayersPanel } from '@/core/components/layers/layers-panel';
import { DetailsPanel } from '@/core/components/details/details-panel';
@@ -103,7 +103,7 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null {
logger.logTraceUseMemo('FOOTER-BAR - memoTabs');
return {
- legend: { icon: , content: },
+ legend: { icon: , content: },
layers: { icon: , content: },
details: { icon: , content: },
'data-table': { icon: , content: },
diff --git a/packages/geoview-core/src/core/components/geolocator/geo-list.tsx b/packages/geoview-core/src/core/components/geolocator/geo-list.tsx
index 48d649d524b..8bd37cfbe1a 100644
--- a/packages/geoview-core/src/core/components/geolocator/geo-list.tsx
+++ b/packages/geoview-core/src/core/components/geolocator/geo-list.tsx
@@ -1,8 +1,9 @@
import { useCallback, useMemo } from 'react';
import { useTheme } from '@mui/material';
import { Box, ListItemButton, Grid, Tooltip, Typography, ListItem } from '@/ui';
-import { GeoListItem } from './geolocator';
-import { getSxClassesList } from './geolocator-style';
+import { GeoListItem } from '@/core/components/geolocator/geolocator';
+import { getSxClassesList } from '@/core/components/geolocator/geolocator-style';
+import { getBoldListTitle, getTooltipTitle } from '@/core/components/geolocator/utilities';
import { useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state';
import { UseHtmlToReact } from '@/core/components/common/hooks/use-html-to-react';
import { logger } from '@/core/utils/logger';
@@ -12,73 +13,47 @@ type GeoListProps = {
searchValue: string;
};
-type tooltipProp = Pick;
-
/**
* Create list of items to display under search.
- * @param {GeoListItem[]} geoListItems - items to display
- * @param {string} searchValue - search text
+ * @param {GeoListItem[]} geoListItems - The items to display
+ * @param {string} searchValue - The search text
* @returns {JSX.Element} React JSX element
*/
-export default function GeoList({ geoListItems, searchValue }: GeoListProps): JSX.Element {
- const { zoomToGeoLocatorLocation } = useMapStoreActions();
+export function GeoList({ geoListItems, searchValue }: GeoListProps): JSX.Element {
+ // Log
+ logger.logTraceRender('components/geolocator/geo-list');
+
+ // Hooks
const theme = useTheme();
const sxClassesList = useMemo(() => getSxClassesList(theme), [theme]);
- /**
- * Get the title for tooltip
- * @param {name} - name of the geo item
- * @param {province} - province of the geo item
- * @param {category} - category of the geo item
- * @returns {string} - tooltip title
- */
- const getTooltipTitle = useCallback(({ name, province, category }: tooltipProp): string => {
- // Log
- // NOTE: Commenting out because it fires too often and leads console pollution.
- // logger.logTraceUseCallback('GEOLOCATOR - geolist - getTooltipTitle', name, province, category);
-
- let title = name;
- if (category && category !== 'null') {
- title += `, ${category}`;
- }
-
- if (province && province !== 'null') {
- title += `, ${province}`;
- }
+ // Store
+ const { zoomToGeoLocatorLocation } = useMapStoreActions();
- return title;
- }, []);
+ // Handle the zoom to geolocation
+ const handleZoomToGeoLocator = useCallback(
+ (latLng: [number, number], bbox: [number, number, number, number]): void => {
+ zoomToGeoLocatorLocation(latLng, bbox).catch((error) => {
+ logger.logPromiseFailed('Failed to zoomToGeoLocatorLocation in GeoList', error);
+ });
+ },
+ [zoomToGeoLocatorLocation]
+ );
/**
- * Transform the search value in search result with bold css.
- * @param {string} title list title in search result
- * @param {string} searchValue value that user search
- * @param {string} province province of the list title in search result
- * @returns {JSX.Element}
+ * Transforms a title string into a JSX element with bold highlighting for search matches
+ * @param {string} title - The original title text to transform
+ * @param {string} searchTerm - The search term to highlight in the title
+ * @param {string} province - The province to append to the title
+ * @returns {JSX.Element} A span element containing the formatted title with bold highlights and province
+ *
+ * @note It's a render-related transformation function who takes direct parameters.
*/
- const transformListTitle = useCallback((_title: string, _searchValue: string, province: string): JSX.Element | string => {
- // Log
- // NOTE: Commenting out because it fires too often and leads console pollution.
- // logger.logTraceUseCallback('GEOLOCATOR - geolist - transformListTitle', _title, _searchValue, province);
-
- const searchPattern = `${_searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`.replace(/\s+/g, '[ ,]*');
- const regex = new RegExp(searchPattern, 'i');
-
- let title = _title;
- if (regex.test(_title)) {
- // make matched substring bold.
- title = _title.replace(regex, '$& ');
- }
-
- return ;
- }, []);
-
- const handleZoomToGeoLocator = (latLng: [number, number], bbox: [number, number, number, number]): void => {
- // Zoom to location
- zoomToGeoLocatorLocation(latLng, bbox).catch((error) => {
- // Log
- logger.logPromiseFailed('Failed to triggerGetAllFeatureInfo in data-panel.GeoList.handleZoomToGeoLocator', error);
- });
+ const transformListTitle = (title: string, searchTerm: string, province: string): JSX.Element => {
+ const newTitle = getBoldListTitle(title, searchTerm);
+ return (
+
+ );
};
return (
diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator-bar.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator-bar.tsx
new file mode 100644
index 00000000000..2329f7a418a
--- /dev/null
+++ b/packages/geoview-core/src/core/components/geolocator/geolocator-bar.tsx
@@ -0,0 +1,51 @@
+import { ChangeEvent } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useTheme } from '@mui/material';
+import { CloseIcon, SearchIcon, AppBarUI, Box, Divider, IconButton, Toolbar } from '@/ui';
+import { StyledInputField } from '@/core/components/geolocator/geolocator-style';
+import { logger } from '@/core/utils/logger';
+
+interface GeolocatorBarProps {
+ /** Current search input value */
+ searchValue: string;
+ /** Called when search input changes */
+ onChange: (event: ChangeEvent) => void;
+ /** Called when search is triggered (via button or form submit) */
+ onSearch: () => void;
+ /** Called when reset/clear button is clicked */
+ onReset: () => void;
+ /** Loading state to disable search while fetching */
+ isLoading: boolean;
+}
+
+export function GeolocatorBar({ searchValue, onChange, onSearch, onReset, isLoading }: GeolocatorBarProps): JSX.Element {
+ logger.logTraceRender('components/geolocator/geolocator-bar');
+
+ // Hooks
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx
index 3dd0d274e22..b94d762745f 100644
--- a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx
+++ b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx
@@ -1,7 +1,6 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
import { SelectChangeEvent, useTheme } from '@mui/material';
import { useTranslation } from 'react-i18next';
-import { getSxClasses } from './geolocator-style';
import {
Box,
Divider,
@@ -15,140 +14,81 @@ import {
TypeMenuItemProps,
Typography,
} from '@/ui';
-import { GeoListItem } from './geolocator';
-import GeoList from './geo-list';
+import { GeoListItem } from '@/core/components/geolocator/geolocator';
+import { GeoList } from '@/core/components/geolocator/geo-list';
+import { createMenuItems } from '@/core/components/geolocator/utilities';
+import { getSxClasses } from '@/core/components/geolocator/geolocator-style';
import { useMapSize } from '@/core/stores/store-interface-and-intial-values/map-state';
import { logger } from '@/core/utils/logger';
interface GeolocatorFiltersType {
geoLocationData: GeoListItem[];
searchValue: string;
- error: Error | null;
+ error: boolean;
}
/**
* Component to display filters and geo location result.
- * @param {GeoListItem[]} geoLocationData data to be displayed in result
- * @param {string} searchValue search value entered by the user.
- * @param {Error} error error thrown api call.
+ * @param {GeoListItem[]} geoLocationData - The data to be displayed in result
+ * @param {string} searchValue - The search value entered by the user.
+ * @param {boolean} error - If there is an error thrown api call.
* @returns {JSX.Element}
*/
export function GeolocatorResult({ geoLocationData, searchValue, error }: GeolocatorFiltersType): JSX.Element {
+ // Log
+ logger.logTraceRender('components/geolocator/geolocator-result');
+
+ // Hooks
const { t } = useTranslation();
const theme = useTheme();
const sxClasses = useMemo(() => getSxClasses(theme), [theme]);
+ // State
const [province, setProvince] = useState('');
const [category, setCategory] = useState('');
- const [data, setData] = useState(geoLocationData);
- // get store values
+ // Store
+ // TODO: style - we should not base length on map size value, parent should adjust
const mapSize = useMapSize();
/**
* Clear all filters.
*/
const handleClearFilters = (): void => {
- if (province || category) {
- setProvince('');
- setCategory('');
- setData(geoLocationData);
- }
+ setProvince('');
+ setCategory('');
};
/**
- * Reduce provinces from api response data i.e. geoLocationData and return transform into MenuItem
+ * Reduce provinces from api response data i.e. geoLocationData
*/
- const provinces: TypeMenuItemProps[] = useMemo(() => {
- // Log
+ const memoProvinces: TypeMenuItemProps[] = useMemo(() => {
logger.logTraceUseMemo('GEOLOCATOR-RESULT - provinces', geoLocationData);
-
- const provincesList = geoLocationData
- .reduce((acc, curr) => {
- if (curr.province && !acc.includes(curr.province)) {
- acc.push(curr.province);
- }
- return acc;
- }, [] as string[])
- .sort();
- // added empty string for resetting the filter
- return ['', ...new Set(provincesList)].map((typeItem: string) => {
- return {
- type: 'item',
- item: { value: !typeItem.length ? '' : typeItem, children: !typeItem.length ? t('geolocator.noFilter') : typeItem },
- };
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [geoLocationData]);
+ return createMenuItems(geoLocationData, 'province', t('geolocator.noFilter'));
+ }, [geoLocationData, t]);
/**
* Reduce categories from api response data i.e. geoLocationData
*/
- const categories: TypeMenuItemProps[] = useMemo(() => {
- // Log
+ const memoCategories: TypeMenuItemProps[] = useMemo(() => {
logger.logTraceUseMemo('GEOLOCATOR-RESULT - categories', geoLocationData);
-
- const locationData = geoLocationData
- .reduce((acc, curr) => {
- if (curr.category) {
- acc.push(curr.category);
- }
- return acc;
- }, [] as string[])
- .sort();
- // added empty string for resetting the filter
- return ['', ...new Set(locationData)].map((typeItem: string) => {
- return {
- type: 'item',
- item: { value: !typeItem.length ? '' : typeItem, children: !typeItem.length ? t('geolocator.noFilter') : typeItem },
- };
+ return createMenuItems(geoLocationData, 'category', t('geolocator.noFilter'));
+ }, [geoLocationData, t]);
+
+ // Filter data with memo
+ const memoFilteredData = useMemo(() => {
+ logger.logTraceUseMemo('GEOLOCATOR-RESULT - filtering data', {
+ total: geoLocationData.length,
+ province,
+ category,
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [geoLocationData]);
-
- // Cache the filter data
- const memoFilterData = useMemo(() => {
- // Log
- logger.logTraceUseMemo('GEOLOCATOR-RESULT - memoFilterData', geoLocationData, province, category);
return geoLocationData.filter((item) => {
- let result = true;
- if (province.length && !category.length) {
- result = item.province.toLowerCase() === province.toLowerCase();
- } else if (province.length && category.length) {
- result = item.province.toLowerCase() === province.toLowerCase() && item.category.toLowerCase() === category.toLowerCase();
- } else if (!province.length && category.length) {
- result = item.category.toLowerCase() === category.toLowerCase();
- }
- return result;
+ const matchProvince = !province || item.province === province;
+ const matchCategory = !category || item.category === category;
+ return matchProvince && matchCategory;
});
- }, [category, geoLocationData, province]);
-
- useEffect(() => {
- // Log
- logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData', geoLocationData);
-
- setData(geoLocationData);
- }, [geoLocationData]);
-
- useEffect(() => {
- // Log
- logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData province category', memoFilterData);
-
- // update result list after setting the province and type.
- setData(memoFilterData);
- }, [memoFilterData]);
-
- useEffect(() => {
- // Log
- logger.logTraceUseEffect('GEOLOCATOR-RESULT - geoLocationData reset', geoLocationData);
-
- // Reset the filters when no result found.
- if (!geoLocationData.length) {
- setProvince('');
- setCategory('');
- }
- }, [geoLocationData]);
+ }, [geoLocationData, province, category]);
return (
@@ -161,10 +101,10 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc
id="provinceGeolocatorFilters"
fullWidth
value={province ?? ''}
- onChange={(e: SelectChangeEvent) => setProvince(e.target.value as string)}
+ onChange={(event: SelectChangeEvent) => setProvince(event.target.value as string)}
label={t('geolocator.province')}
inputLabel={{ id: 'geolocationProvinceFilter' }}
- menuItems={provinces}
+ menuItems={memoProvinces}
disabled={!geoLocationData.length}
variant="standard"
/>
@@ -176,10 +116,10 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc
formControlProps={{ variant: 'standard', size: 'small' }}
value={category ?? ''}
fullWidth
- onChange={(e: SelectChangeEvent) => setCategory(e.target.value as string)}
+ onChange={(event: SelectChangeEvent) => setCategory(event.target.value as string)}
label={t('geolocator.category')}
inputLabel={{ id: 'geolocationCategoryFilter' }}
- menuItems={categories}
+ menuItems={memoCategories}
disabled={!geoLocationData.length}
variant="standard"
/>
@@ -200,11 +140,16 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc
)}
- {!!data.length && }
- {(!data.length || error) && (
+ {error && (
+
+ {t('error.geolocator.noService')}
+
+ )}
+ {!!memoFilteredData.length && }
+ {!memoFilteredData.length && searchValue.length >= 3 && (
- {t('geolocator.errorMessage')} {searchValue}
+ {t('geolocator.noResult')} {searchValue}
{!!(province.length || category.length) && (
diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx
index 0c2cd1d254d..d77ddd100bb 100644
--- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx
+++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx
@@ -1,18 +1,17 @@
-import { ChangeEvent, useCallback, useRef, useState, useEffect, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
+import { ChangeEvent, useCallback, useEffect, useMemo } from 'react';
import debounce from 'lodash/debounce';
import { useTheme } from '@mui/material';
-import { CloseIcon, SearchIcon, AppBarUI, Box, Divider, IconButton, ProgressBar, Toolbar } from '@/ui';
-import { StyledInputField, getSxClasses } from './geolocator-style';
-import { OL_ZOOM_DURATION } from '@/core/utils/constant';
+import { Box, ProgressBar } from '@/ui';
import { useUIActiveAppBarTab, useUIActiveTrapGeoView, useUIStoreActions } from '@/core/stores/store-interface-and-intial-values/ui-state';
-import { useAppGeolocatorServiceURL, useAppDisplayLanguage } from '@/core/stores/store-interface-and-intial-values/app-state';
-import { GeolocatorResult } from './geolocator-result';
-import { logger } from '@/core/utils/logger';
+import { GeolocatorResult } from '@/core/components/geolocator/geolocator-result';
+import { getSxClasses } from '@/core/components/geolocator/geolocator-style';
import { CV_DEFAULT_APPBAR_CORE } from '@/api/config/types/config-constants';
import { FocusTrapContainer } from '@/core/components/common';
import { useGeoViewMapId } from '@/core/stores/geoview-store';
-import { handleEscapeKey } from '@/core/utils/utilities';
+// import { handleEscapeKey } from '@/core/utils/utilities';
+import { logger } from '@/core/utils/logger';
+import { useGeolocator } from '@/core/components/geolocator/hooks/use-geolocator';
+import { GeolocatorBar } from '@/core/components/geolocator/geolocator-bar';
export interface GeoListItem {
key: string;
@@ -24,250 +23,67 @@ export interface GeoListItem {
category: string;
}
+const MIN_SEARCH_LENGTH = 3;
+const DEBOUNCE_DELAY = 500;
+
export function Geolocator(): JSX.Element {
- // Log
logger.logTraceRender('components/geolocator/geolocator');
- const { t } = useTranslation();
-
+ // Hooks
const theme = useTheme();
- const mapId = useGeoViewMapId();
const sxClasses = useMemo(() => getSxClasses(theme), [theme]);
- // internal state
- const [data, setData] = useState();
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [searchValue, setSearchValue] = useState('');
-
- // get store values
- const displayLanguage = useAppDisplayLanguage();
- const geolocatorServiceURL = useAppGeolocatorServiceURL();
+ // Store
+ const mapId = useGeoViewMapId();
const { setActiveAppBarTab } = useUIStoreActions();
const { tabGroup, isOpen } = useUIActiveAppBarTab();
const activeTrapGeoView = useUIActiveTrapGeoView();
- const displayLanguageRef = useRef(displayLanguage);
- const geolocatorRef = useRef();
- const abortControllerRef = useRef(null);
- const fetchTimerRef = useRef();
- const searchInputRef = useRef();
- const MIN_SEARCH_LENGTH = 3;
+ // Custom geolocator hook
+ const { data, isLoading, searchValue, error, setSearchValue, getGeolocations, resetState } = useGeolocator();
- /**
- * Checks if search term is decimal degree coordinate and return geo list item.
- * @param {string} searchTerm search term user searched.
- * @returns GeoListItem | null
- */
- const getDecimalDegreeItem = (searchTerm: string): GeoListItem | null => {
- const latLngRegDD = /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/;
-
- if (!latLngRegDD.test(searchTerm)) {
- return null;
- }
-
- // remove extra spaces and delimiters (the filter). convert string numbers to floaty numbers
- const coords = searchTerm
- .split(/[\s|,|;|]/)
- .filter((n) => !Number.isNaN(n) && n !== '')
- .map((n) => parseFloat(n));
-
- // apply buffer to create bbox from point coordinates
- const buff = 0.015; // degrees
- const boundingBox: [number, number, number, number] = [coords[1] - buff, coords[0] - buff, coords[1] + buff, coords[0] + buff];
-
- // prep the lat/long result that needs to be generated along with name based results
- return {
- key: 'coordinates',
- name: `${coords[0]},${coords[1]}`,
- lat: coords[0],
- lng: coords[1],
- bbox: boundingBox,
- province: '',
- category: 'Latitude/Longitude',
- };
- };
-
- /**
- * Send fetch call to the service for given search term.
- * @param {string} searchTerm the search term entered by the user
- * @returns {Promise}
- */
- const getGeolocations = useCallback(
- async (searchTerm: string): Promise => {
- try {
- setIsLoading(true);
- // Abort any pending requests
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- clearTimeout(fetchTimerRef.current);
- }
-
- // Create new abort controller
- const newAbortController = new AbortController();
- abortControllerRef.current = newAbortController;
-
- // Use the current value from the ref
- const currentUrl = `${geolocatorServiceURL}&lang=${displayLanguageRef.current}`;
-
- const response = await fetch(`${currentUrl}&q=${encodeURIComponent(`${searchTerm}*`)}`, {
- signal: abortControllerRef.current.signal,
- });
- if (!response.ok) {
- throw new Error('Error');
- }
- const result = (await response.json()) as GeoListItem[];
- const ddSupport = getDecimalDegreeItem(searchTerm);
-
- if (ddSupport) {
- // insert at the top of array.
- result.unshift(ddSupport);
+ // Create debounced version of getGeolocations
+ const debouncedRequest = useMemo(
+ () =>
+ debounce((value: string) => {
+ if (value.length >= MIN_SEARCH_LENGTH) {
+ getGeolocations(value);
}
-
- setData(result);
- setError(null);
- setIsLoading(false);
- clearTimeout(fetchTimerRef?.current);
- } catch (err) {
- setError(err as Error);
- }
- },
- [geolocatorServiceURL]
+ }, DEBOUNCE_DELAY),
+ [getGeolocations]
);
- /**
- * Reset loading and data state and clear fetch timer.
- */
- const resetGeoLocatorState = (): void => {
- setIsLoading(false);
- setData([]);
- clearTimeout(fetchTimerRef.current);
- };
+ const handleSearch = useCallback(() => {
+ if (searchValue.length >= MIN_SEARCH_LENGTH) {
+ debouncedRequest(searchValue);
+ }
+ }, [searchValue, debouncedRequest]);
- /**
- * Reset search component values when close icon is clicked.
- * @returns void
- */
- const resetSearch = useCallback(() => {
+ const handleReset = useCallback(() => {
setSearchValue('');
- setData(undefined);
setActiveAppBarTab(`${mapId}AppbarPanelButtonGeolocator`, CV_DEFAULT_APPBAR_CORE.GEOLOCATOR, false, false);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [setActiveAppBarTab]);
+ }, [mapId, setActiveAppBarTab, setSearchValue]);
- /**
- * Do service request after debouncing.
- * @returns void
- */
- const doRequest = debounce((searchTerm: string) => {
- getGeolocations(searchTerm).catch((errorInside) => {
- // Log
- logger.logPromiseFailed('getGeolocations in deRequest in Geolocator', errorInside);
- });
- }, OL_ZOOM_DURATION);
+ const handleChange = (event: ChangeEvent): void => {
+ const { value } = event.target;
+ setSearchValue(value);
- /**
- * Debounce the get geolocation service request
- * @param {string} searchTerm value to be searched
- * @returns void
- */
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const debouncedRequest = useCallback((searchTerm: string) => doRequest(searchTerm), []);
+ if (!value.length || value.length < MIN_SEARCH_LENGTH) {
+ resetState();
+ return;
+ }
- /**
- * onChange handler for search input field
- * NOTE: search will fire only when user enter atleast 3 characters.
- * when less 3 characters while doing search, list will be cleared out.
- * @param {ChangeEvent} e HTML Change event handler
- * @returns void
- */
- const onChange = (e: ChangeEvent): void => {
- const { value } = e.target;
- setSearchValue(value);
- // do fetch request when user enter at least 3 characters.
if (value.length >= MIN_SEARCH_LENGTH) {
debouncedRequest(value);
}
- // clear geo list when search term cleared from input field.
- if (!value.length || value.length < MIN_SEARCH_LENGTH) {
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- }
- resetGeoLocatorState();
- doRequest.cancel();
- setData(undefined);
- }
};
- /**
- * Geo location handler.
- * @returns void
- */
- const handleGetGeolocations = useCallback(() => {
- if (searchValue.length >= MIN_SEARCH_LENGTH) {
- getGeolocations(searchValue).catch((errorInside) => {
- // Log
- logger.logPromiseFailed('getGeolocations in Geolocator', errorInside);
- });
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchValue]);
-
+ // Cleanup debounce on unmount
useEffect(() => {
- // Log
- logger.logTraceUseEffect('GEOLOCATOR - mount');
-
- if (!geolocatorRef?.current) return () => {};
-
- const geolocator = geolocatorRef.current;
- const handleGeolocatorEscapeKey = (event: KeyboardEvent): void => {
- handleEscapeKey(event.key, '', false, () => resetSearch());
- };
- geolocator.addEventListener('keydown', handleGeolocatorEscapeKey);
-
- // Cleanup function to remove event listener
return () => {
- geolocator.removeEventListener('keydown', handleGeolocatorEscapeKey);
+ debouncedRequest.cancel();
};
- }, [mapId, resetSearch]);
-
- useEffect(() => {
- return () => {
- // Cleanup function to abort any pending requests
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- clearTimeout(fetchTimerRef.current);
- }
- };
- }, []);
-
- useEffect(() => {
- // Set the focus on search field when geolocator is opened.
- if (isOpen && tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && searchInputRef.current) {
- searchInputRef.current.querySelector('input')?.focus();
- }
- }, [isOpen, tabGroup]);
-
- /**
- * Effect that will track fetch call, so that after 15 seconds if no response comes back,
- * Error will be displayed.
- */
- useEffect(() => {
- if (isLoading) {
- fetchTimerRef.current = setTimeout(() => {
- resetGeoLocatorState();
- setError(new Error('No result found.'));
- }, 15000);
- }
- return () => {
- clearTimeout(fetchTimerRef.current);
- };
- }, [isLoading]);
-
- // Update the ref whenever displayLanguage changes
- useEffect(() => {
- displayLanguageRef.current = displayLanguage;
- }, [displayLanguage]);
+ }, [debouncedRequest]);
return (
@@ -275,56 +91,26 @@ export function Geolocator(): JSX.Element {
sx={sxClasses.root}
visibility={tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && isOpen ? 'visible' : 'hidden'}
id="geolocator-search"
- tabIndex={tabGroup === CV_DEFAULT_APPBAR_CORE.GEOLOCATOR && isOpen ? 0 : -1}
- ref={geolocatorRef}
>
-
-
-
-
-
+
+
{isLoading && (
)}
- {!!data && searchValue?.length >= MIN_SEARCH_LENGTH && (
+
+ {(error || (!!data && searchValue?.length >= MIN_SEARCH_LENGTH)) && (
-
+
)}
diff --git a/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts
new file mode 100644
index 00000000000..e924cc0e0f3
--- /dev/null
+++ b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts
@@ -0,0 +1,167 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useAppGeolocatorServiceURL, useAppDisplayLanguage } from '@/core/stores/store-interface-and-intial-values/app-state';
+import { cleanPostalCode, getDecimalDegreeItem } from '@/core/components/geolocator/utilities';
+import { GeoListItem } from '@/core/components/geolocator/geolocator';
+import { logger } from '@/core/utils/logger';
+
+interface UseGeolocatorReturn {
+ /** Array of geolocation results */
+ data: GeoListItem[] | undefined;
+ /** Loading state during requests */
+ isLoading: boolean;
+ /** Current search input value */
+ searchValue: string;
+ /** Error value */
+ error: boolean;
+ /** Function to update search value */
+ setSearchValue: (value: string) => void;
+ /** Function to trigger geolocation search */
+ getGeolocations: (searchTerm: string) => void;
+ /** Function to reset the hook state */
+ resetState: () => void;
+}
+
+const TIMEOUT_DELAY = 15000;
+
+export const useGeolocator = (): UseGeolocatorReturn => {
+ logger.logTraceCore('GEOLOCATOR - useGeolocator');
+
+ // States
+ const [data, setData] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(false);
+ const [searchValue, setSearchValue] = useState('');
+
+ // Store
+ const displayLanguage = useAppDisplayLanguage();
+ const geolocatorServiceURL = useAppGeolocatorServiceURL();
+
+ // Refs
+ const displayLanguageRef = useRef(displayLanguage);
+ const abortControllerRef = useRef(null);
+ const fetchTimerRef = useRef();
+
+ // Reset state helper
+ const resetState = useCallback(() => {
+ setData(undefined);
+ setError(false);
+ setIsLoading(false);
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ // Cleanup function uses the captured timer reference
+ if (fetchTimerRef.current) {
+ clearTimeout(fetchTimerRef.current);
+ }
+ }, []);
+
+ // Handle timeout effect
+ useEffect(() => {
+ if (isLoading) {
+ // Store the current timer reference
+ const timer = setTimeout(() => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ setData(undefined);
+ setError(true);
+ setIsLoading(false);
+ logger.logError('GEOLOCATOR - search timeout error');
+ }, TIMEOUT_DELAY);
+
+ fetchTimerRef.current = timer;
+
+ // Cleanup function uses the captured timer reference
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+
+ return () => {};
+ }, [isLoading, resetState]);
+
+ // Component unmount cleanup
+ useEffect(() => {
+ return () => {
+ resetState();
+ };
+ }, [resetState]);
+
+ const fetchGeolocations = useCallback(
+ async (searchTerm: string): Promise => {
+ logger.logTraceUseCallback('GEOLOCATOR use-geolocator fetchGeolocations', searchTerm);
+
+ try {
+ // Check if it is a postal code and return clean term
+ const cleanSearchTerm = cleanPostalCode(searchTerm);
+ setIsLoading(true);
+
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ clearTimeout(fetchTimerRef.current);
+ }
+
+ const newAbortController = new AbortController();
+ abortControllerRef.current = newAbortController;
+
+ const currentUrl = `${geolocatorServiceURL}&lang=${displayLanguageRef.current}`;
+ const response = await fetch(`${currentUrl}&q=${encodeURIComponent(`${cleanSearchTerm}*`)}`, {
+ signal: abortControllerRef.current.signal,
+ });
+
+ if (!response.ok) throw new Error('Error');
+
+ const result = (await response.json()) as GeoListItem[];
+
+ // If cleanSearchTerm is a coordinate, add it to the list
+ const ddSupport = getDecimalDegreeItem(cleanSearchTerm);
+ if (ddSupport) result.unshift(ddSupport);
+
+ setData(result);
+ } finally {
+ setIsLoading(false);
+ clearTimeout(fetchTimerRef.current);
+ }
+
+ return Promise.resolve();
+ },
+ [geolocatorServiceURL]
+ );
+
+ // Public function that handles the Promise
+ const getGeolocations = useCallback(
+ (searchTerm: string): void => {
+ fetchGeolocations(searchTerm).catch((err) => {
+ // Handle or log any errors here if needed
+ if (err.name !== 'AbortError') {
+ setError(true);
+ setData(undefined);
+ logger.logError('GEOLOCATOR - search failed', err);
+ }
+ });
+ },
+ [fetchGeolocations]
+ );
+
+ useEffect(() => {
+ logger.logTraceUseEffect('GEOLOCATOR - change language', displayLanguage, searchValue);
+
+ // Set language and redo request
+ displayLanguageRef.current = displayLanguage;
+ getGeolocations(searchValue);
+
+ // Only listen to change in language and getGeolocations to request new value with updated language
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [displayLanguage, getGeolocations]);
+
+ return {
+ data,
+ isLoading,
+ searchValue,
+ error,
+ setSearchValue,
+ getGeolocations,
+ resetState,
+ };
+};
diff --git a/packages/geoview-core/src/core/components/geolocator/utilities.ts b/packages/geoview-core/src/core/components/geolocator/utilities.ts
new file mode 100644
index 00000000000..bac4dc309e0
--- /dev/null
+++ b/packages/geoview-core/src/core/components/geolocator/utilities.ts
@@ -0,0 +1,128 @@
+import { GeoListItem } from '@/core/components/geolocator/geolocator';
+import { TypeMenuItemProps } from '@/ui';
+
+export type tooltipProp = Pick;
+
+export const SEARCH_PATTERNS = {
+ SPACES_AND_COMMAS: /[ ,]*/,
+ SPECIAL_CHARS: /[.*+?^${}()|[\]\\]/g,
+ POSTAL_CODE: /^[A-Z][0-9][A-Z]\s?[0-9][A-Z][0-9]$/i,
+ LAT_LONG: /^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)[\s,;|]\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/,
+ LAT_LONG_DELIMITER: /[\s,;|]+/,
+} as const;
+
+/**
+ * Remove spaces if term is a postal code
+ * @param {string} searchTerm - The search term user searched
+ * @returns {string} The currated postal code
+ */
+export const cleanPostalCode = (searchTerm: string): string => {
+ // Clean the input
+ const cleanTerm = searchTerm.trim().toUpperCase();
+
+ // Check if it's a valid postal code
+ if (SEARCH_PATTERNS.POSTAL_CODE.test(cleanTerm)) {
+ // Remove any spaces and return uppercase
+ return cleanTerm.replace(/\s+/g, '');
+ }
+
+ return searchTerm;
+};
+
+/**
+ * Checks if search term is decimal degree coordinate and return geo list item.
+ * @param {string} searchTerm - The search term user searched
+ * @returns GeoListItem | null
+ */
+export const getDecimalDegreeItem = (searchTerm: string): GeoListItem | null => {
+ if (!SEARCH_PATTERNS.LAT_LONG.test(searchTerm)) return null;
+
+ // Remove extra spaces and delimiters (the filter) then convert string numbers to float numbers
+ const coords = searchTerm
+ .split(SEARCH_PATTERNS.LAT_LONG_DELIMITER)
+ .filter((n) => !Number.isNaN(n) && n !== '')
+ .map((n) => parseFloat(n));
+
+ // Apply buffer (degree) to create bbox from point coordinates
+ const buff = 0.015;
+ const boundingBox: [number, number, number, number] = [coords[1] - buff, coords[0] - buff, coords[1] + buff, coords[0] + buff];
+
+ // Return the lat/long result that needs to be generated along with name based results
+ return {
+ key: 'coordinates',
+ name: `${coords[0]},${coords[1]}`,
+ lat: coords[0],
+ lng: coords[1],
+ bbox: boundingBox,
+ province: '',
+ category: 'Latitude/Longitude',
+ };
+};
+
+/**
+ * Get the title for tooltip
+ * @param {string} name - The name of the geo item
+ * @param {string} province - The province of the geo item
+ * @param {category} category - The category of the geo item
+ * @returns {string} The tooltip title
+ */
+export const getTooltipTitle = ({ name, province, category }: tooltipProp): string => {
+ return [name, category !== 'null' && category, province !== 'null' && province].filter(Boolean).join(', ');
+};
+
+/**
+ * Makes matching text bold in a title string.
+ * @param {string} title - The list title in search result
+ * @param {string} searchValue - The value that user search
+ * @returns {string} The bolded title string
+ */
+export const getBoldListTitle = (title: string, searchValue: string): string => {
+ if (!searchValue || !title) return title;
+
+ // Check pattern
+ const searchPattern = `${searchValue.replace(SEARCH_PATTERNS.SPECIAL_CHARS, '\\$&')}`.replace(
+ /\s+/g,
+ SEARCH_PATTERNS.SPACES_AND_COMMAS.source
+ );
+ const pattern = new RegExp(searchPattern, 'i');
+
+ return pattern.test(title) ? title.replace(pattern, '$& ') : title;
+};
+
+/**
+ * Creates menu items from a list of unique values from geoLocationData
+ * @param {GeoListItem[]} geoLocationData - The source data array
+ * @param {string} field - The field to extract values from ('province' | 'category')
+ * @param {string} noFilterText - The text to display for the empty filter option
+ * @returns {TypeMenuItemProps[]} Array of menu items
+ */
+export const createMenuItems = (
+ geoLocationData: GeoListItem[],
+ field: 'province' | 'category',
+ noFilterText: string
+): TypeMenuItemProps[] => {
+ // Use Set for unique values
+ const uniqueValues = new Set(
+ geoLocationData
+ .map((item) => item[field])
+ .filter((value): value is string => Boolean(value))
+ .sort()
+ );
+
+ return [
+ {
+ type: 'item' as const,
+ item: {
+ value: '',
+ children: noFilterText,
+ },
+ },
+ ...Array.from(uniqueValues).map((value) => ({
+ type: 'item' as const,
+ item: {
+ value,
+ children: value,
+ },
+ })),
+ ];
+};
diff --git a/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx b/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx
new file mode 100644
index 00000000000..5931585f521
--- /dev/null
+++ b/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx
@@ -0,0 +1,98 @@
+import { createElement, ReactNode, useCallback } from 'react';
+import { useMapProjection, useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state';
+import { logger } from '@/core/utils/logger';
+import NavbarPanelButton from '@/core/components/nav-bar/nav-bar-panel-button';
+import { TypeValidMapProjectionCodes } from '@/api/config/types/map-schema-types';
+import { TypePanelProps } from '@/ui/panel/panel-types';
+import { IconButtonPropsExtend, IconButton } from '@/ui/icon-button/icon-button';
+import { List, ListItem } from '@/ui/list';
+import { ProjectionIcon, PublicIcon } from '@/ui/icons';
+
+const projectionChoiceOptions: {
+ [key: string]: {
+ code: TypeValidMapProjectionCodes;
+ name: string;
+ };
+} = {
+ '3857': { code: 3857, name: 'Web Mercator' },
+ '3978': { code: 3978, name: 'LCC' },
+};
+
+/**
+ * Create a projection select button to open the select panel, and set panel content
+ * @returns {JSX.Element} the created basemap select button
+ */
+export default function Projection(): JSX.Element {
+ // Log
+ logger.logTraceRender('components/nav-bar/buttons/projection');
+
+ // Store
+ const projection = useMapProjection();
+ const { setProjection } = useMapStoreActions();
+
+ /**
+ * Handles map projection choice
+ * @param {TypeValidMapProjectionCodes} projectionCode the projection code to switch to
+ */
+ const handleChoice = useCallback(
+ (projectionCode: TypeValidMapProjectionCodes): void => {
+ setProjection(projectionCode);
+ },
+ [setProjection]
+ );
+
+ /**
+ * Render buttons in navbar panel.
+ * @returns ReactNode
+ */
+ const renderButtons = (): ReactNode => {
+ return (
+
+
+ handleChoice(projectionChoiceOptions['3857'].code)}
+ disabled={projection === 3857}
+ >
+
+ {projectionChoiceOptions['3857'].name}
+
+
+
+ handleChoice(projectionChoiceOptions['3978'].code)}
+ disabled={projection === 3978}
+ >
+
+ {projectionChoiceOptions['3978'].name}
+
+
+
+ );
+ };
+
+ // Set up props for nav bar panel button
+ const button: IconButtonPropsExtend = {
+ tooltip: 'mapnav.projection',
+ children: createElement(ProjectionIcon),
+ tooltipPlacement: 'left',
+ };
+
+ const panel: TypePanelProps = {
+ title: 'Projection',
+ icon: createElement(ProjectionIcon),
+ content: renderButtons(),
+ width: 'flex',
+ };
+
+ return ;
+}
diff --git a/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx b/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx
index ec1caa19dec..de903630221 100644
--- a/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx
+++ b/packages/geoview-core/src/core/components/nav-bar/nav-bar-panel-button.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { ClickAwayListener } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
@@ -24,19 +24,22 @@ export default function NavbarPanelButton({ buttonPanel }: NavbarPanelButtonType
// Log
logger.logTraceRender('components/nav-bar/nav-bar-panel-button');
+ // Hooks
const { t } = useTranslation();
-
const theme = useTheme();
- const sxClasses = getSxClasses(theme);
+ const sxClasses = useMemo(() => getSxClasses(theme), [theme]);
+ // Store
const mapId = useGeoViewMapId();
const geoviewElement = useAppGeoviewHTMLElement();
- const shellContainer = geoviewElement.querySelector(`[id^="shell-${mapId}"]`) as HTMLElement;
-
+ // States
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
+ const shellContainer = geoviewElement.querySelector(`[id^="shell-${mapId}"]`) as HTMLElement;
+
+ // Handlers
const handleClick = (event: React.MouseEvent): void => {
if (open) {
setOpen(false);
diff --git a/packages/geoview-core/src/core/components/nav-bar/nav-bar.tsx b/packages/geoview-core/src/core/components/nav-bar/nav-bar.tsx
index b07d4381dd2..227b63836b6 100644
--- a/packages/geoview-core/src/core/components/nav-bar/nav-bar.tsx
+++ b/packages/geoview-core/src/core/components/nav-bar/nav-bar.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState, Fragment } from 'react';
+import { useCallback, useEffect, useRef, useState, Fragment, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,6 +10,7 @@ import ZoomOut from './buttons/zoom-out';
import Fullscreen from './buttons/fullscreen';
import Home from './buttons/home';
import Location from './buttons/location';
+import Projection from './buttons/projection';
import { ButtonGroup, Box, IconButton } from '@/ui';
import { TypeButtonPanel } from '@/ui/panel/panel-types';
import { getSxClasses } from './nav-bar-style';
@@ -22,10 +23,23 @@ type NavBarProps = {
api: NavBarApi;
};
-type DefaultNavbar = 'fullScreen' | 'location' | 'home' | 'zoomIn' | 'zoomOut' | 'basemapSelect';
+type DefaultNavbar = 'fullScreen' | 'location' | 'home' | 'zoomIn' | 'zoomOut' | 'basemapSelect' | 'projection';
type NavbarButtonGroup = Record;
type NavButtonGroups = Record;
+const defaultNavbar: Record = {
+ fullScreen: ,
+ location: ,
+ home: ,
+ basemapSelect: ,
+ projection: ,
+ zoomIn: ,
+ zoomOut: ,
+};
+const defaultButtonGroups: NavButtonGroups = {
+ zoom: { zoomIn: 'zoomIn', zoomOut: 'zoomOut' },
+};
+
/**
* Create a nav-bar with buttons that can call functions or open custom panels
*/
@@ -35,28 +49,18 @@ export function NavBar(props: NavBarProps): JSX.Element {
const { api: navBarApi } = props;
+ // Hooks
const { t } = useTranslation();
-
const theme = useTheme();
- const sxClasses = getSxClasses(theme);
+ const sxClasses = useMemo(() => getSxClasses(theme), [theme]);
- // get the expand or collapse from store
+ // Store
const navBarComponents = useUINavbarComponents();
- const defaultNavbar: Record = {
- fullScreen: ,
- location: ,
- home: ,
- basemapSelect: ,
- zoomIn: ,
- zoomOut: ,
- };
-
- // internal state
+ // Ref
const navBarRef = useRef(null);
- const defaultButtonGroups: NavButtonGroups = {
- zoom: { zoomIn: 'zoomIn', zoomOut: 'zoomOut' },
- };
+
+ // State
const [buttonPanelGroups, setButtonPanelGroups] = useState(defaultButtonGroups);
useEffect(() => {
@@ -80,6 +84,10 @@ export function NavBar(props: NavBarProps): JSX.Element {
displayButtons = { ...displayButtons, basemapSelect: 'basemapSelect' };
}
+ if (navBarComponents.includes('projection')) {
+ displayButtons = { ...displayButtons, projection: 'projection' };
+ }
+
setButtonPanelGroups({
...{ display: displayButtons },
...buttonPanelGroups,
diff --git a/packages/geoview-core/src/core/components/notifications/notifications.tsx b/packages/geoview-core/src/core/components/notifications/notifications.tsx
index 8af386101dd..7452804295d 100644
--- a/packages/geoview-core/src/core/components/notifications/notifications.tsx
+++ b/packages/geoview-core/src/core/components/notifications/notifications.tsx
@@ -281,7 +281,7 @@ export default memo(function Notifications(): JSX.Element {
notificationsList
) : (
- {t('appbar.no_notifications_available')}
+ {t('appbar.noNotificationsAvailable')}
)}
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 1ac107590c8..eb9c504e995 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
@@ -84,6 +84,7 @@ export interface IMapState {
setLegendCollapsed: (layerPath: string, newValue?: boolean) => void;
setOrToggleLayerVisibility: (layerPath: string, newValue?: boolean) => boolean;
setMapKeyboardPanInteractions: (panDelta: number) => void;
+ setProjection: (projectionCode: TypeValidMapProjectionCodes) => void;
setZoom: (zoom: number, duration?: number) => void;
setInteraction: (interaction: TypeInteraction) => void;
setRotation: (rotation: number) => void;
@@ -359,6 +360,17 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt
MapEventProcessor.setMapKeyboardPanInteractions(get().mapId, panDelta);
},
+ /**
+ * Sets the projection of the map.
+ * @param {TypeValidMapProjectionCodes} projectionCode - The projection.
+ */
+ setProjection: (projectionCode: TypeValidMapProjectionCodes): void => {
+ // Redirect to processor
+ MapEventProcessor.setProjection(get().mapId, projectionCode).catch((error) => {
+ logger.logError('Map-State Failed to set projection', error);
+ });
+ },
+
/**
* Sets the zoom level.
* @param {number} zoom - The zoom level.
diff --git a/packages/geoview-core/src/geo/layer/basemap/basemap.ts b/packages/geoview-core/src/geo/layer/basemap/basemap.ts
index d10a14b72ee..40f08d0ce70 100644
--- a/packages/geoview-core/src/geo/layer/basemap/basemap.ts
+++ b/packages/geoview-core/src/geo/layer/basemap/basemap.ts
@@ -247,7 +247,7 @@ export class Basemap {
): Promise {
const resolutions: number[] = [];
let minZoom = 0;
- let maxZoom = 17;
+ let maxZoom = 23;
let extent: Extent = [0, 0, 0, 0];
let origin: number[] = [];
let urlProj = 0;
@@ -384,7 +384,7 @@ export class Basemap {
let defaultExtent: Extent | undefined;
let defaultResolutions: number[] | undefined;
let minZoom = 0;
- let maxZoom = 17;
+ let maxZoom = 23;
// Check if projection is provided for the basemap creation
const projectionCode = projection === undefined ? MapEventProcessor.getMapState(this.mapId).currentProjection : projection;
diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/abstract-geoview-layers.ts b/packages/geoview-core/src/geo/layer/geoview-layers/abstract-geoview-layers.ts
index 943473c39a5..e9f771570eb 100644
--- a/packages/geoview-core/src/geo/layer/geoview-layers/abstract-geoview-layers.ts
+++ b/packages/geoview-core/src/geo/layer/geoview-layers/abstract-geoview-layers.ts
@@ -89,7 +89,7 @@ export abstract class AbstractGeoViewLayer {
initialSettings?: TypeLayerInitialSettings;
/** layers of listOfLayerEntryConfig that did not load. */
- layerLoadError: { layer: string; loggerMessage: string }[] = [];
+ layerLoadError: { layer: string; layerName?: string | undefined; loggerMessage: string }[] = [];
/** The OpenLayer root layer representing this GeoView Layer. */
olRootLayer?: BaseLayer;
@@ -467,8 +467,9 @@ export abstract class AbstractGeoViewLayer {
const arrayOfLayerConfigs = await Promise.all(promisedAllLayerDone);
arrayOfLayerConfigs.forEach((layerConfig) => {
if (layerConfig.layerStatus === 'error') {
+ // TODO: refactor - create meaningful message and centralize dispatch for layer - config
+ // We do not log the error here, it will be trapped in setAllLayerStatusTo
const message = `Error while loading layer path ${layerConfig.layerPath} on map ${this.mapId}`;
- this.layerLoadError.push({ layer: layerConfig.layerPath, loggerMessage: message });
throw new Error(message);
} else {
// When we get here, we know that the metadata (if the service provide some) are processed.
@@ -564,6 +565,7 @@ export abstract class AbstractGeoViewLayer {
}
this.layerLoadError.push({
layer: listOfLayerEntryConfig[0].layerPath,
+ layerName: listOfLayerEntryConfig[0].layerName,
loggerMessage: `Unable to create group layer ${listOfLayerEntryConfig[0].layerPath} on map ${this.mapId}`,
});
return undefined;
@@ -584,6 +586,7 @@ export abstract class AbstractGeoViewLayer {
}
this.layerLoadError.push({
layer: listOfLayerEntryConfig[0].layerPath,
+ layerName: listOfLayerEntryConfig[0].layerName,
loggerMessage: `Unable to create layer ${listOfLayerEntryConfig[0].layerPath} on map ${this.mapId}`,
});
this.getLayerConfig(layerPath)!.layerStatus = 'error';
@@ -624,6 +627,7 @@ export abstract class AbstractGeoViewLayer {
} else {
this.layerLoadError.push({
layer: listOfLayerEntryConfig[i].layerPath,
+ layerName: listOfLayerEntryConfig[i].layerName,
loggerMessage: `Unable to create ${
layerEntryIsGroupLayer(listOfLayerEntryConfig[i]) ? CONST_LAYER_ENTRY_TYPES.GROUP : ''
} layer ${listOfLayerEntryConfig[i].layerPath} on map ${this.mapId}`,
@@ -707,9 +711,11 @@ export abstract class AbstractGeoViewLayer {
if (layerConfig.layerStatus === 'error') return;
layerConfig.layerStatus = newStatus;
if (newStatus === 'error') {
- const { layerPath } = layerConfig;
+ const { layerPath, layerName } = layerConfig;
+ const useLayerName = layerName === undefined ? layerConfig.geoviewLayerConfig.geoviewLayerName : layerName;
this.layerLoadError.push({
layer: layerPath,
+ layerName: useLayerName || layerPath,
loggerMessage: `${errorMessage} for layer ${layerPath} of map ${this.mapId}`,
});
}
diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts b/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts
index df06f4ab68b..d1c47dcbaa9 100644
--- a/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts
+++ b/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts
@@ -85,7 +85,7 @@ export function commonValidateListOfLayerEntryConfig(
): void {
listOfLayerEntryConfig.forEach((layerConfig: TypeLayerEntryConfig, i) => {
if (layerConfig.layerStatus === 'error') return;
- const { layerPath } = layerConfig;
+ const { layerPath, layerName } = layerConfig;
if (layerEntryIsGroupLayer(layerConfig)) {
// Use the layer name from the metadata if it exists and there is no existing name.
@@ -99,6 +99,7 @@ export function commonValidateListOfLayerEntryConfig(
if (!(layerConfig as GroupLayerEntryConfig).listOfLayerEntryConfig.length) {
layer.layerLoadError.push({
layer: layerPath,
+ layerName: layerName || layerConfig.geoviewLayerConfig.geoviewLayerName,
loggerMessage: `Empty layer group (mapId: ${layer.mapId}, layerPath: ${layerPath})`,
});
layerConfig.layerStatus = 'error';
@@ -113,6 +114,7 @@ export function commonValidateListOfLayerEntryConfig(
if (Number.isNaN(esriIndex)) {
layer.layerLoadError.push({
layer: layerPath,
+ layerName: layerName || layerConfig.geoviewLayerConfig.geoviewLayerName,
loggerMessage: `ESRI layerId must be a number (mapId: ${layer.mapId}, layerPath: ${layerPath})`,
});
layerConfig.layerStatus = 'error';
@@ -126,6 +128,7 @@ export function commonValidateListOfLayerEntryConfig(
if (esriIndex === -1) {
layer.layerLoadError.push({
layer: layerPath,
+ layerName: layerName || layerConfig.geoviewLayerConfig.geoviewLayerName,
loggerMessage: `ESRI layerId not found (mapId: ${layer.mapId}, layerPath: ${layerPath})`,
});
layerConfig.layerStatus = 'error';
diff --git a/packages/geoview-core/src/geo/layer/layer.ts b/packages/geoview-core/src/geo/layer/layer.ts
index ca23b725ec8..f643a110503 100644
--- a/packages/geoview-core/src/geo/layer/layer.ts
+++ b/packages/geoview-core/src/geo/layer/layer.ts
@@ -426,7 +426,6 @@ export class LayerApi {
if (error instanceof GeoViewLayerCreatedTwiceError) {
this.mapViewer.notifications.showError('validation.layer.createtwice', [
(error as GeoViewLayerCreatedTwiceError).geoviewLayerId,
- this.getMapId(),
]);
} else {
this.mapViewer.notifications.showError('validation.layer.genericError', [this.getMapId()]);
@@ -488,7 +487,7 @@ export class LayerApi {
*/
#printDuplicateGeoviewLayerConfigError(mapConfigLayerEntry: MapConfigLayerEntry): void {
// TODO: find a more centralized way to trap error and display message
- api.maps[this.getMapId()].notifications.showError('validation.layer.usedtwice', [mapConfigLayerEntry.geoviewLayerId, this.getMapId()]);
+ api.maps[this.getMapId()].notifications.showError('validation.layer.usedtwice', [mapConfigLayerEntry.geoviewLayerId]);
// Log
logger.logError(`Duplicate use of geoview layer identifier ${mapConfigLayerEntry.geoviewLayerId} on map ${this.getMapId()}`);
@@ -1022,13 +1021,13 @@ export class LayerApi {
// do not add the layer to the map
if (geoviewLayer.layerLoadError.length !== 0) {
geoviewLayer.layerLoadError.forEach((loadError) => {
- const { layer, loggerMessage } = loadError;
+ const { layer, layerName, loggerMessage } = loadError;
// Log the details in the console
logger.logError(loggerMessage);
// TODO: find a more centralized way to trap error and display message
- api.maps[this.getMapId()].notifications.showError('validation.layer.loadfailed', [layer, this.getMapId()]);
+ api.maps[this.getMapId()].notifications.showError('validation.layer.loadfailed', [layerName || layer]);
this.#emitLayerError({ layerPath: layer, errorMessage: loggerMessage });
});
diff --git a/packages/geoview-core/src/geo/layer/other/geocore.ts b/packages/geoview-core/src/geo/layer/other/geocore.ts
index be37f66cdb9..7bd74497204 100644
--- a/packages/geoview-core/src/geo/layer/other/geocore.ts
+++ b/packages/geoview-core/src/geo/layer/other/geocore.ts
@@ -8,7 +8,6 @@ import { logger } from '@/core/utils/logger';
import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor';
import { GeoCoreLayerConfig, TypeGeoviewLayerConfig } from '@/geo/map/map-schema-types';
-import { TypeJsonValue } from '@/core/types/global-types';
import { api } from '@/app';
/**
@@ -90,7 +89,7 @@ export class GeoCore {
MapEventProcessor.removeOrderedLayerInfo(this.#mapId, uuid, false);
// TODO: find a more centralized way to trap error and display message
- api.maps[this.#mapId].notifications.showError('validation.layer.loadfailed', [error as TypeJsonValue, this.#mapId]);
+ api.maps[this.#mapId].notifications.showError('validation.layer.loadfailed', [`GeoCore - ${uuid}`]);
throw error;
}
}
diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts
index 4afa0a7772c..bb085b882b5 100644
--- a/packages/geoview-core/src/geo/map/map-viewer.ts
+++ b/packages/geoview-core/src/geo/map/map-viewer.ts
@@ -15,6 +15,7 @@ import queryString from 'query-string';
import {
CV_MAP_CENTER,
CV_MAP_EXTENTS,
+ CV_VALID_ZOOM_LEVELS,
VALID_DISPLAY_LANGUAGE,
VALID_DISPLAY_THEME,
VALID_PROJECTION_CODES,
@@ -264,8 +265,8 @@ export class MapViewer {
),
zoom: mapViewSettings.initialView?.zoomAndCenter ? mapViewSettings.initialView?.zoomAndCenter[0] : 3.5,
extent: extentProjected || undefined,
- minZoom: mapViewSettings.minZoom || 0,
- maxZoom: mapViewSettings.maxZoom || 17,
+ minZoom: mapViewSettings.minZoom || CV_VALID_ZOOM_LEVELS[0],
+ maxZoom: mapViewSettings.maxZoom || CV_VALID_ZOOM_LEVELS[1],
rotation: mapViewSettings.rotation || 0,
}),
controls: [],
@@ -615,9 +616,6 @@ export class MapViewer {
logger.logError('Failed in #checkLayerResultSetReady', error);
});
- // Start checking for map layers processed
- this.#checkMapLayersProcessed();
-
// Check how load in milliseconds has it been processing thus far
const elapsedMilliseconds = Date.now() - this.#checkMapReadyStartTime!;
@@ -681,6 +679,9 @@ export class MapViewer {
}
});
}
+
+ // Start checking for map layers processed after the onMapLayersLoaded is define!
+ this.#checkMapLayersProcessed();
}
/**
diff --git a/packages/geoview-core/src/ui/icons/index.ts b/packages/geoview-core/src/ui/icons/index.ts
index 2adef3e2bea..080903d367d 100644
--- a/packages/geoview-core/src/ui/icons/index.ts
+++ b/packages/geoview-core/src/ui/icons/index.ts
@@ -49,7 +49,6 @@ export {
HighlightOutlined as HighlightOutlinedIcon,
Highlight as HighlightIcon,
Home as HomeIcon,
- HubOutlined as HubOutlinedIcon,
Height as HeightIcon,
ImportExport as ReorderIcon,
Info as InfoIcon,
@@ -73,6 +72,7 @@ export {
Menu as MenuIcon,
MoreHoriz as MoreHorizIcon,
MoreVert as MoreVertIcon,
+ MultipleStop as ProjectionIcon,
Opacity as OpacityIcon,
OpenInBrowser as OpenInBrowserIcon,
Pause as PauseIcon,
@@ -106,3 +106,5 @@ export {
ZoomIn as ZoomInSearchIcon,
ZoomOut as ZoomOutSearchIcon,
} from '@mui/icons-material';
+
+export { LegendIcon } from '@/ui/svg/svg-icon';
diff --git a/packages/geoview-core/src/ui/index.ts b/packages/geoview-core/src/ui/index.ts
index d20b3210c97..01ed2c9be79 100644
--- a/packages/geoview-core/src/ui/index.ts
+++ b/packages/geoview-core/src/ui/index.ts
@@ -37,7 +37,7 @@ export * from './slider/slider';
export * from './snackbar/snackbar';
export * from './stepper/stepper';
export * from './style/theme';
-export * from './svg/geo-ca-icon';
+export * from './svg/svg-icon';
export * from './switch/switch';
export * from './table/table';
export * from './tabs/tabs';
diff --git a/packages/geoview-core/src/ui/svg/geo-ca-icon/index.tsx b/packages/geoview-core/src/ui/svg/svg-icon/index.tsx
similarity index 64%
rename from packages/geoview-core/src/ui/svg/geo-ca-icon/index.tsx
rename to packages/geoview-core/src/ui/svg/svg-icon/index.tsx
index 13b8ab20244..c40c177db4d 100644
--- a/packages/geoview-core/src/ui/svg/geo-ca-icon/index.tsx
+++ b/packages/geoview-core/src/ui/svg/svg-icon/index.tsx
@@ -1,3 +1,5 @@
+import { SvgIcon, SvgIconProps } from '@mui/material';
+
// ? I doubt we want to define an explicit type for this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function GeoCaIcon(): any {
@@ -21,3 +23,18 @@ export function GeoCaIcon(): any {
);
}
+
+export function LegendIcon(props: SvgIconProps): JSX.Element {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}