From 25fe557e6a21d564055b156c40223696750bb278 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Thu, 20 Feb 2025 16:18:29 -0500 Subject: [PATCH 1/8] fix(ui): Set 20 as default max zoom and update geolocator Closes #2755 --- packages/geoview-core/schema.json | 4 +- .../types/classes/map-feature-config.ts | 4 +- .../src/api/config/types/config-constants.ts | 2 +- .../types/config-validation-schema.json | 4 +- .../src/api/config/types/map-schema-types.ts | 4 +- .../core/components/geolocator/geo-list.tsx | 91 +++++------- .../geolocator/geolocator-result.tsx | 136 +++++------------- .../core/components/geolocator/geolocator.tsx | 69 ++++----- .../core/components/geolocator/utilities.ts | 128 +++++++++++++++++ 9 files changed, 232 insertions(+), 210 deletions(-) create mode 100644 packages/geoview-core/src/core/components/geolocator/utilities.ts 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..59f35fd44d3 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 @@ -237,11 +237,11 @@ 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 >= 0 && minZoom <= 20 ? 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 >= 0 && maxZoom <= 20 ? 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..418e21eb3fb 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -171,7 +171,7 @@ export const CV_DEFAULT_MAP_FEATURE_CONFIG = Cast({ enableRotation: true, rotation: 0, minZoom: 0, - maxZoom: 50, + maxZoom: 20, 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..c554ec379ce 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 @@ -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/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-result.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx index 3dd0d274e22..381a26142b8 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,8 +14,10 @@ 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'; @@ -34,121 +35,60 @@ interface GeolocatorFiltersType { * @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,8 +140,8 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc )} - {!!data.length && } - {(!data.length || error) && ( + {!!memoFilteredData.length && } + {(!memoFilteredData.length || error) && ( {t('geolocator.errorMessage')} {searchValue} diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index 0c2cd1d254d..c290e03afaa 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -3,16 +3,16 @@ import { useTranslation } from 'react-i18next'; 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 { 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 { StyledInputField, getSxClasses } from '@/core/components/geolocator/geolocator-style'; +import { cleanPostalCode, getDecimalDegreeItem } from '@/core/components/geolocator/utilities'; 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 { logger } from '@/core/utils/logger'; export interface GeoListItem { key: string; @@ -24,69 +24,38 @@ 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'); + // Hooks const { t } = useTranslation(); - const theme = useTheme(); - const mapId = useGeoViewMapId(); const sxClasses = useMemo(() => getSxClasses(theme), [theme]); - // internal state + // State const [data, setData] = useState(); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [searchValue, setSearchValue] = useState(''); - // get store values + // Store + const mapId = useGeoViewMapId(); const displayLanguage = useAppDisplayLanguage(); const geolocatorServiceURL = useAppGeolocatorServiceURL(); const { setActiveAppBarTab } = useUIStoreActions(); const { tabGroup, isOpen } = useUIActiveAppBarTab(); const activeTrapGeoView = useUIActiveTrapGeoView(); + // Refs const displayLanguageRef = useRef(displayLanguage); const geolocatorRef = useRef(); const abortControllerRef = useRef(null); const fetchTimerRef = useRef(); const searchInputRef = useRef(); - const MIN_SEARCH_LENGTH = 3; - - /** - * 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. @@ -96,6 +65,9 @@ export function Geolocator(): JSX.Element { const getGeolocations = useCallback( async (searchTerm: string): Promise => { try { + // eslint-disable-next-line no-param-reassign + searchTerm = cleanPostalCode(searchTerm); + setIsLoading(true); // Abort any pending requests if (abortControllerRef.current) { @@ -164,7 +136,7 @@ export function Geolocator(): JSX.Element { // Log logger.logPromiseFailed('getGeolocations in deRequest in Geolocator', errorInside); }); - }, OL_ZOOM_DURATION); + }, DEBOUNCE_DELAY); /** * Debounce the get geolocation service request @@ -229,7 +201,7 @@ export function Geolocator(): JSX.Element { return () => { geolocator.removeEventListener('keydown', handleGeolocatorEscapeKey); }; - }, [mapId, resetSearch]); + }, [resetSearch]); useEffect(() => { return () => { @@ -266,7 +238,14 @@ export function Geolocator(): JSX.Element { // Update the ref whenever displayLanguage changes useEffect(() => { + logger.logTraceUseEffect('GEOLOCATOR - change language', displayLanguage, searchValue); + + // Set language and redo request displayLanguageRef.current = displayLanguage; + doRequest(searchValue); + + // Only listen to change in language to request new bvalue with updated language + // eslint-disable-next-line react-hooks/exhaustive-deps }, [displayLanguage]); return ( 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..3f3b100800f --- /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, + }, + })), + ]; +}; From 731f2d2c72b7d27f2d63995917f933d1cbc387ca Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 21 Feb 2025 15:46:15 -0500 Subject: [PATCH 2/8] Improve geolocator, add sample maxExtent, repair zoon layersId --- .../navigator/04-a-max-extent-override.json | 36 ++ .../public/templates/demos-navigator.html | 1 + .../src/api/config/types/config-constants.ts | 2 +- .../components/geolocator/geolocator-bar.tsx | 51 ++ .../geolocator/geolocator-result.tsx | 8 +- .../core/components/geolocator/geolocator.tsx | 569 ++++++++++-------- .../geolocator/hooks/use-geolocator.ts | 165 +++++ .../core/components/geolocator/utilities.ts | 256 ++++---- .../geoview-core/src/geo/map/map-viewer.ts | 6 +- 9 files changed, 719 insertions(+), 375 deletions(-) create mode 100644 packages/geoview-core/public/configs/navigator/04-a-max-extent-override.json create mode 100644 packages/geoview-core/src/core/components/geolocator/geolocator-bar.tsx create mode 100644 packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts 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..5a280781a7f --- /dev/null +++ b/packages/geoview-core/public/configs/navigator/04-a-max-extent-override.json @@ -0,0 +1,36 @@ +{ + "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" + ], + "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/templates/demos-navigator.html b/packages/geoview-core/public/templates/demos-navigator.html index 038f57440ea..fe142ef8a05 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

+ 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 418e21eb3fb..3585df5f43f 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -136,7 +136,7 @@ export const CV_VALID_MAP_CENTER: Record = { - 3857: [-170, 35, -20, 84], + 3857: [-180, -35, 120, 84], 3978: [-135, 25, -50, 89], }; export const CV_MAP_CENTER: Record = { 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 ( + + +
{ + e.preventDefault(); + if (!isLoading) onSearch(); + }} + > + + + + + + + + + + + +
+
+ ); +} 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 381a26142b8..ff35250c1b6 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx @@ -24,14 +24,14 @@ 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 { diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index c290e03afaa..d89f4da1490 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 { 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 '@/core/components/geolocator/geolocator-result'; -import { StyledInputField, getSxClasses } from '@/core/components/geolocator/geolocator-style'; -import { cleanPostalCode, getDecimalDegreeItem } from '@/core/components/geolocator/utilities'; +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 './hooks/use-geolocator'; +import { GeolocatorBar } from './geolocator-bar'; export interface GeoListItem { key: string; @@ -27,226 +26,348 @@ export interface GeoListItem { const MIN_SEARCH_LENGTH = 3; const DEBOUNCE_DELAY = 500; +// export function Geolocator(): JSX.Element { +// // Log +// logger.logTraceRender('components/geolocator/geolocator'); + +// // Hooks +// const { t } = useTranslation(); +// const theme = useTheme(); +// const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + +// // State +// const [data, setData] = useState(); +// const [error, setError] = useState(null); +// const [isLoading, setIsLoading] = useState(false); +// const [searchValue, setSearchValue] = useState(''); + +// // Store +// const mapId = useGeoViewMapId(); +// const displayLanguage = useAppDisplayLanguage(); +// const geolocatorServiceURL = useAppGeolocatorServiceURL(); +// const { setActiveAppBarTab } = useUIStoreActions(); +// const { tabGroup, isOpen } = useUIActiveAppBarTab(); +// const activeTrapGeoView = useUIActiveTrapGeoView(); + +// // Refs +// const displayLanguageRef = useRef(displayLanguage); +// const geolocatorRef = useRef(); +// const abortControllerRef = useRef(null); +// const fetchTimerRef = useRef(); +// const searchInputRef = useRef(); + +// /** +// * 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 { +// // eslint-disable-next-line no-param-reassign +// searchTerm = cleanPostalCode(searchTerm); + +// 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); +// } + +// setData(result); +// setError(null); +// setIsLoading(false); +// clearTimeout(fetchTimerRef?.current); +// } catch (err) { +// setError(err as Error); +// } +// }, +// [geolocatorServiceURL] +// ); + +// /** +// * Reset loading and data state and clear fetch timer. +// */ +// const resetGeoLocatorState = (): void => { +// setIsLoading(false); +// setData([]); +// clearTimeout(fetchTimerRef.current); +// }; + +// /** +// * Reset search component values when close icon is clicked. +// * @returns void +// */ +// const resetSearch = useCallback(() => { +// setSearchValue(''); +// setData(undefined); +// setActiveAppBarTab(`${mapId}AppbarPanelButtonGeolocator`, CV_DEFAULT_APPBAR_CORE.GEOLOCATOR, false, false); +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [setActiveAppBarTab]); + +// /** +// * 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); +// }); +// }, DEBOUNCE_DELAY); + +// /** +// * 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), []); + +// /** +// * 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]); + +// 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); +// }; +// }, [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(() => { +// logger.logTraceUseEffect('GEOLOCATOR - change language', displayLanguage, searchValue); + +// // Set language and redo request +// displayLanguageRef.current = displayLanguage; +// doRequest(searchValue); + +// // Only listen to change in language to request new bvalue with updated language +// // eslint-disable-next-line react-hooks/exhaustive-deps +// }, [displayLanguage]); + +// return ( +// +// +// +// +// +//
{ +// // NOTE: so that when enter is pressed, page is not reloaded. +// e.preventDefault(); +// if (!isLoading) { +// handleGetGeolocations(); +// } +// }} +// > +// +// +// +// +// +// +// +// +// +// +// +//
+//
+//
+// {isLoading && ( +// +// +// +// )} +// {!!data && searchValue?.length >= MIN_SEARCH_LENGTH && ( +// +// +// +// )} +//
+//
+// ); +// } + export function Geolocator(): JSX.Element { - // Log logger.logTraceRender('components/geolocator/geolocator'); // Hooks - const { t } = useTranslation(); const theme = useTheme(); const sxClasses = useMemo(() => getSxClasses(theme), [theme]); - // State - const [data, setData] = useState(); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [searchValue, setSearchValue] = useState(''); - // Store const mapId = useGeoViewMapId(); - const displayLanguage = useAppDisplayLanguage(); - const geolocatorServiceURL = useAppGeolocatorServiceURL(); const { setActiveAppBarTab } = useUIStoreActions(); const { tabGroup, isOpen } = useUIActiveAppBarTab(); const activeTrapGeoView = useUIActiveTrapGeoView(); - // Refs - const displayLanguageRef = useRef(displayLanguage); - const geolocatorRef = useRef(); - const abortControllerRef = useRef(null); - const fetchTimerRef = useRef(); - const searchInputRef = useRef(); - - /** - * 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 { - // eslint-disable-next-line no-param-reassign - searchTerm = cleanPostalCode(searchTerm); - - 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; + // Custom geolocator hook + const { data, isLoading, searchValue, error, setSearchValue, getGeolocations, resetState } = useGeolocator(); - // 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]); - - /** - * 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); - }); - }, DEBOUNCE_DELAY); - - /** - * 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), []); - - /** - * 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; + }, [mapId, setActiveAppBarTab, setSearchValue]); + + const handleChange = (event: ChangeEvent): void => { + const { value } = event.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); - }); + if (!value.length || value.length < MIN_SEARCH_LENGTH) { + resetState(); + return; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchValue]); - - 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); - }; - }, [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(); + if (value.length >= MIN_SEARCH_LENGTH) { + debouncedRequest(value); } - }, [isOpen, tabGroup]); + }; - /** - * Effect that will track fetch call, so that after 15 seconds if no response comes back, - * Error will be displayed. - */ + // Cleanup debounce on unmount useEffect(() => { - if (isLoading) { - fetchTimerRef.current = setTimeout(() => { - resetGeoLocatorState(); - setError(new Error('No result found.')); - }, 15000); - } return () => { - clearTimeout(fetchTimerRef.current); + debouncedRequest.cancel(); }; - }, [isLoading]); - - // Update the ref whenever displayLanguage changes - useEffect(() => { - logger.logTraceUseEffect('GEOLOCATOR - change language', displayLanguage, searchValue); - - // Set language and redo request - displayLanguageRef.current = displayLanguage; - doRequest(searchValue); - - // Only listen to change in language to request new bvalue with updated language - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displayLanguage]); + }, [debouncedRequest]); return ( @@ -254,56 +375,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} > - - -
{ - // NOTE: so that when enter is pressed, page is not reloaded. - e.preventDefault(); - if (!isLoading) { - handleGetGeolocations(); - } - }} - > - - - - - - - - - - - -
-
+
+ {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..71dcf1ca461 --- /dev/null +++ b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts @@ -0,0 +1,165 @@ +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}fg&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); + } + }, + [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 to request new value with updated language + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [displayLanguage]); + + 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 index 3f3b100800f..bac4dc309e0 100644 --- a/packages/geoview-core/src/core/components/geolocator/utilities.ts +++ b/packages/geoview-core/src/core/components/geolocator/utilities.ts @@ -1,128 +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, - }, - })), - ]; -}; +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/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index 4afa0a7772c..9675e52b658 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -615,9 +615,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 +678,9 @@ export class MapViewer { } }); } + + // Start checking for map layers processed after the onMapLayersLoaded is define! + this.#checkMapLayersProcessed(); } /** From c54aac876c377c1b0b0d14a9b3c397285271cc93 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Tue, 25 Feb 2025 12:33:37 -0500 Subject: [PATCH 3/8] fix type --- .../navigator/04-a-max-extent-override.json | 1 + .../src/api/config/types/map-schema-types.ts | 2 +- .../components/nav-bar/buttons/projection.tsx | 103 ++++++++++++++++++ .../src/core/components/nav-bar/nav-bar.tsx | 8 +- .../map-state.ts | 12 ++ packages/geoview-core/src/ui/icons/index.ts | 1 + 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx 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 index 5a280781a7f..881928da873 100644 --- 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 @@ -21,6 +21,7 @@ "overview-map", "north-arrow" ], + "navBar": ["home", "projection"], "footerBar": { "tabs": { "core": [ 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 c554ec379ce..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[]; 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..4b049870e3b --- /dev/null +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx @@ -0,0 +1,103 @@ +import { createElement, ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +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, SatelliteIcon, SignpostIcon } from '@/ui/icons'; + +const projectionChoiceOptions: { + [key: string]: { + code: TypeValidMapProjectionCodes; + }; +} = { + '3978': { code: 3978 }, + '3857': { code: 3857 }, +}; + +/** + * 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 BasemapSelect(): JSX.Element { + // Log + logger.logTraceRender('components/nav-bar/buttons/projection'); + + // Hook + const { t } = useTranslation(); + + // Store + const projection = useMapProjection(); + const { setProjection } = useMapStoreActions(); + + /** + * Handles basemap selection and updates basemap + * @returns {JSX.Element} the created basemap select button + */ + const handleChoice = (projectionCode: TypeValidMapProjectionCodes): void => { + // setSelectedBasemap(basemapChoice); + // createBasemapFromOptions(basemapChoice === 'default' ? configBasemapOptions : basemapChoiceOptions[basemapChoice]).catch((error) => { + // // Log + // logger.logPromiseFailed('setBaseMap in basemaps.ts', error); + // }); + setProjection(projectionCode); + }; + + /** + * Render buttons in navbar panel. + * @returns ReactNode + */ + const renderButtons = (): ReactNode => { + return ( + + + handleChoice(projectionChoiceOptions['3857'].code)} + disabled={projection === 3857} + > + + {t('basemaps.transport')} + + + + handleChoice(projectionChoiceOptions['3857'].code)} + disabled={projection === 3978} + > + + {t('basemaps.imagery')} + + + + ); + }; + + // Set up props for nav bar panel button + const button: IconButtonPropsExtend = { + tooltip: 'mapnav.basemap', + 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.tsx b/packages/geoview-core/src/core/components/nav-bar/nav-bar.tsx index b07d4381dd2..43ef9baa9ca 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 @@ -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,7 +23,7 @@ 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; @@ -48,6 +49,7 @@ export function NavBar(props: NavBarProps): JSX.Element { location: , home: , basemapSelect: , + projection: , zoomIn: , zoomOut: , }; @@ -80,6 +82,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/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/ui/icons/index.ts b/packages/geoview-core/src/ui/icons/index.ts index 2adef3e2bea..ad1fc5f730a 100644 --- a/packages/geoview-core/src/ui/icons/index.ts +++ b/packages/geoview-core/src/ui/icons/index.ts @@ -73,6 +73,7 @@ export { Menu as MenuIcon, MoreHoriz as MoreHorizIcon, MoreVert as MoreVertIcon, + MultipleStop as ProjectionIcon, Opacity as OpacityIcon, OpenInBrowser as OpenInBrowserIcon, Pause as PauseIcon, From 98936f6c2a4286c24c10fae0d63b9c2c6af6cc35 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Wed, 26 Feb 2025 11:24:18 -0500 Subject: [PATCH 4/8] message and icon legend --- .../configs/navigator/16-esri-dynamic.json | 2 +- .../img/guide/navigation/basemapSelect.svg | 3 + .../img/guide/navigation/projection.svg | 5 ++ .../geoview-core/public/locales/en/guide.md | 2 + .../public/locales/en/translation.json | 38 +++++++++--- .../geoview-core/public/locales/fr/guide.md | 2 + .../public/locales/fr/translation.json | 42 +++++++++---- .../public/templates/add-layers.html | 3 +- .../public/templates/demos-navigator.html | 2 +- .../app-event-processor.ts | 18 +++--- .../src/core/components/app-bar/app-bar.tsx | 4 +- .../core/components/footer-bar/footer-bar.tsx | 4 +- .../geolocator/geolocator-result.tsx | 9 ++- .../geolocator/hooks/use-geolocator.ts | 2 +- .../components/nav-bar/buttons/projection.tsx | 61 +++++++++---------- .../nav-bar/nav-bar-panel-button.tsx | 13 ++-- .../src/core/components/nav-bar/nav-bar.tsx | 38 ++++++------ .../notifications/notifications.tsx | 2 +- .../src/geo/layer/basemap/basemap.ts | 4 +- .../geoview-layers/abstract-geoview-layers.ts | 12 +++- .../layer/geoview-layers/esri-layer-common.ts | 5 +- packages/geoview-core/src/geo/layer/layer.ts | 7 +-- .../src/geo/layer/other/geocore.ts | 3 +- .../geoview-core/src/geo/map/map-viewer.ts | 2 +- packages/geoview-core/src/ui/icons/index.ts | 2 + packages/geoview-core/src/ui/index.ts | 2 +- .../svg/{geo-ca-icon => svg-icon}/index.tsx | 17 ++++++ 27 files changed, 196 insertions(+), 108 deletions(-) create mode 100644 packages/geoview-core/public/img/guide/navigation/basemapSelect.svg create mode 100644 packages/geoview-core/public/img/guide/navigation/projection.svg rename packages/geoview-core/src/ui/svg/{geo-ca-icon => svg-icon}/index.tsx (64%) diff --git a/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json b/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json index 09d95c9ba7f..ca2db6ac2d0 100644 --- a/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json +++ b/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json @@ -18,7 +18,7 @@ "geoviewLayerType": "esriDynamic", "listOfLayerEntryConfig": [ { - "layerId": "0" + "layerId": "10" } ] }, 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
diff --git a/packages/geoview-core/public/templates/demos-navigator.html b/packages/geoview-core/public/templates/demos-navigator.html index fe142ef8a05..df3f64a1cab 100644 --- a/packages/geoview-core/public/templates/demos-navigator.html +++ b/packages/geoview-core/public/templates/demos-navigator.html @@ -152,7 +152,7 @@

Configurations Navigator

- +
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/geolocator-result.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx index ff35250c1b6..b94d762745f 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator-result.tsx @@ -140,11 +140,16 @@ export function GeolocatorResult({ geoLocationData, searchValue, error }: Geoloc )} + {error && ( + + {t('error.geolocator.noService')} + + )} {!!memoFilteredData.length && } - {(!memoFilteredData.length || error) && ( + {!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/hooks/use-geolocator.ts b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts index 71dcf1ca461..2e1661239a1 100644 --- a/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts +++ b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts @@ -105,7 +105,7 @@ export const useGeolocator = (): UseGeolocatorReturn => { const newAbortController = new AbortController(); abortControllerRef.current = newAbortController; - const currentUrl = `${geolocatorServiceURL}fg&lang=${displayLanguageRef.current}`; + const currentUrl = `${geolocatorServiceURL}&lang=${displayLanguageRef.current}`; const response = await fetch(`${currentUrl}&q=${encodeURIComponent(`${cleanSearchTerm}*`)}`, { signal: abortControllerRef.current.signal, }); 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 index 4b049870e3b..5931585f521 100644 --- a/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx +++ b/packages/geoview-core/src/core/components/nav-bar/buttons/projection.tsx @@ -1,5 +1,4 @@ -import { createElement, ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; +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'; @@ -7,44 +6,40 @@ 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, SatelliteIcon, SignpostIcon } from '@/ui/icons'; +import { ProjectionIcon, PublicIcon } from '@/ui/icons'; const projectionChoiceOptions: { [key: string]: { code: TypeValidMapProjectionCodes; + name: string; }; } = { - '3978': { code: 3978 }, - '3857': { code: 3857 }, + '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 BasemapSelect(): JSX.Element { +export default function Projection(): JSX.Element { // Log logger.logTraceRender('components/nav-bar/buttons/projection'); - // Hook - const { t } = useTranslation(); - // Store const projection = useMapProjection(); const { setProjection } = useMapStoreActions(); /** - * Handles basemap selection and updates basemap - * @returns {JSX.Element} the created basemap select button + * Handles map projection choice + * @param {TypeValidMapProjectionCodes} projectionCode the projection code to switch to */ - const handleChoice = (projectionCode: TypeValidMapProjectionCodes): void => { - // setSelectedBasemap(basemapChoice); - // createBasemapFromOptions(basemapChoice === 'default' ? configBasemapOptions : basemapChoiceOptions[basemapChoice]).catch((error) => { - // // Log - // logger.logPromiseFailed('setBaseMap in basemaps.ts', error); - // }); - setProjection(projectionCode); - }; + const handleChoice = useCallback( + (projectionCode: TypeValidMapProjectionCodes): void => { + setProjection(projectionCode); + }, + [setProjection] + ); /** * Render buttons in navbar panel. @@ -52,33 +47,33 @@ export default function BasemapSelect(): JSX.Element { */ const renderButtons = (): ReactNode => { return ( - + handleChoice(projectionChoiceOptions['3857'].code)} disabled={projection === 3857} > - - {t('basemaps.transport')} + + {projectionChoiceOptions['3857'].name} handleChoice(projectionChoiceOptions['3857'].code)} + onClick={() => handleChoice(projectionChoiceOptions['3978'].code)} disabled={projection === 3978} > - - {t('basemaps.imagery')} + + {projectionChoiceOptions['3978'].name} @@ -87,13 +82,13 @@ export default function BasemapSelect(): JSX.Element { // Set up props for nav bar panel button const button: IconButtonPropsExtend = { - tooltip: 'mapnav.basemap', + tooltip: 'mapnav.projection', children: createElement(ProjectionIcon), tooltipPlacement: 'left', }; const panel: TypePanelProps = { - title: 'projection', + title: 'Projection', icon: createElement(ProjectionIcon), content: renderButtons(), width: 'flex', 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 43ef9baa9ca..061878e1fb5 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'; @@ -27,6 +27,19 @@ type DefaultNavbar = 'fullScreen' | 'location' | 'home' | 'zoomIn' | 'zoomOut' | 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 */ @@ -36,29 +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: , - projection: , - zoomIn: , - zoomOut: , - }; - - // internal state + // State const navBarRef = useRef(null); - const defaultButtonGroups: NavButtonGroups = { - zoom: { zoomIn: 'zoomIn', zoomOut: 'zoomOut' }, - }; + + // Ref const [buttonPanelGroups, setButtonPanelGroups] = useState(defaultButtonGroups); useEffect(() => { 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/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 9675e52b658..7a9d6c9a570 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -265,7 +265,7 @@ export class MapViewer { zoom: mapViewSettings.initialView?.zoomAndCenter ? mapViewSettings.initialView?.zoomAndCenter[0] : 3.5, extent: extentProjected || undefined, minZoom: mapViewSettings.minZoom || 0, - maxZoom: mapViewSettings.maxZoom || 17, + maxZoom: mapViewSettings.maxZoom || 23, rotation: mapViewSettings.rotation || 0, }), controls: [], diff --git a/packages/geoview-core/src/ui/icons/index.ts b/packages/geoview-core/src/ui/icons/index.ts index ad1fc5f730a..dc73171fa15 100644 --- a/packages/geoview-core/src/ui/icons/index.ts +++ b/packages/geoview-core/src/ui/icons/index.ts @@ -107,3 +107,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 ( + + + + + + + + + + + ); +} From d1a4b030c40a5a9febb17cdefb987606d8930f68 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Wed, 26 Feb 2025 13:11:17 -0500 Subject: [PATCH 5/8] reviewable --- .../configs/navigator/16-esri-dynamic.json | 2 +- .../src/api/config/types/config-constants.ts | 4 +- .../core/components/geolocator/geolocator.tsx | 286 +----------------- 3 files changed, 4 insertions(+), 288 deletions(-) diff --git a/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json b/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json index ca2db6ac2d0..09d95c9ba7f 100644 --- a/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json +++ b/packages/geoview-core/public/configs/navigator/16-esri-dynamic.json @@ -18,7 +18,7 @@ "geoviewLayerType": "esriDynamic", "listOfLayerEntryConfig": [ { - "layerId": "10" + "layerId": "0" } ] }, 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 3585df5f43f..4e70edf28b9 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -136,11 +136,11 @@ export const CV_VALID_MAP_CENTER: Record = { - 3857: [-180, -35, 120, 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], }; diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index d89f4da1490..96d2bfda3e7 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -24,291 +24,7 @@ export interface GeoListItem { } const MIN_SEARCH_LENGTH = 3; -const DEBOUNCE_DELAY = 500; - -// export function Geolocator(): JSX.Element { -// // Log -// logger.logTraceRender('components/geolocator/geolocator'); - -// // Hooks -// const { t } = useTranslation(); -// const theme = useTheme(); -// const sxClasses = useMemo(() => getSxClasses(theme), [theme]); - -// // State -// const [data, setData] = useState(); -// const [error, setError] = useState(null); -// const [isLoading, setIsLoading] = useState(false); -// const [searchValue, setSearchValue] = useState(''); - -// // Store -// const mapId = useGeoViewMapId(); -// const displayLanguage = useAppDisplayLanguage(); -// const geolocatorServiceURL = useAppGeolocatorServiceURL(); -// const { setActiveAppBarTab } = useUIStoreActions(); -// const { tabGroup, isOpen } = useUIActiveAppBarTab(); -// const activeTrapGeoView = useUIActiveTrapGeoView(); - -// // Refs -// const displayLanguageRef = useRef(displayLanguage); -// const geolocatorRef = useRef(); -// const abortControllerRef = useRef(null); -// const fetchTimerRef = useRef(); -// const searchInputRef = useRef(); - -// /** -// * 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 { -// // eslint-disable-next-line no-param-reassign -// searchTerm = cleanPostalCode(searchTerm); - -// 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); -// } - -// setData(result); -// setError(null); -// setIsLoading(false); -// clearTimeout(fetchTimerRef?.current); -// } catch (err) { -// setError(err as Error); -// } -// }, -// [geolocatorServiceURL] -// ); - -// /** -// * Reset loading and data state and clear fetch timer. -// */ -// const resetGeoLocatorState = (): void => { -// setIsLoading(false); -// setData([]); -// clearTimeout(fetchTimerRef.current); -// }; - -// /** -// * Reset search component values when close icon is clicked. -// * @returns void -// */ -// const resetSearch = useCallback(() => { -// setSearchValue(''); -// setData(undefined); -// setActiveAppBarTab(`${mapId}AppbarPanelButtonGeolocator`, CV_DEFAULT_APPBAR_CORE.GEOLOCATOR, false, false); -// // eslint-disable-next-line react-hooks/exhaustive-deps -// }, [setActiveAppBarTab]); - -// /** -// * 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); -// }); -// }, DEBOUNCE_DELAY); - -// /** -// * 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), []); - -// /** -// * 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]); - -// 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); -// }; -// }, [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(() => { -// logger.logTraceUseEffect('GEOLOCATOR - change language', displayLanguage, searchValue); - -// // Set language and redo request -// displayLanguageRef.current = displayLanguage; -// doRequest(searchValue); - -// // Only listen to change in language to request new bvalue with updated language -// // eslint-disable-next-line react-hooks/exhaustive-deps -// }, [displayLanguage]); - -// return ( -// -// -// -// -// -//
{ -// // NOTE: so that when enter is pressed, page is not reloaded. -// e.preventDefault(); -// if (!isLoading) { -// handleGetGeolocations(); -// } -// }} -// > -// -// -// -// -// -// -// -// -// -// -// -//
-//
-//
-// {isLoading && ( -// -// -// -// )} -// {!!data && searchValue?.length >= MIN_SEARCH_LENGTH && ( -// -// -// -// )} -//
-//
-// ); -// } +const DEBOUNCE_DELAY = 500;git add export function Geolocator(): JSX.Element { logger.logTraceRender('components/geolocator/geolocator'); From f3118815224c376c310a6056f9d20f04f8bfac7d Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Wed, 26 Feb 2025 13:12:58 -0500 Subject: [PATCH 6/8] reviewable --- .../geoview-core/src/core/components/geolocator/geolocator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index 96d2bfda3e7..bab7335bbf9 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -24,7 +24,7 @@ export interface GeoListItem { } const MIN_SEARCH_LENGTH = 3; -const DEBOUNCE_DELAY = 500;git add +const DEBOUNCE_DELAY = 500; export function Geolocator(): JSX.Element { logger.logTraceRender('components/geolocator/geolocator'); From 0c28530ba6bef7033838bb059d45c97e4bd9a4ad Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Wed, 26 Feb 2025 14:52:02 -0500 Subject: [PATCH 7/8] review comment --- .../public/configs/navigator/03-projection-WM.json | 3 --- .../src/api/config/types/classes/map-feature-config.ts | 9 +++++++-- .../src/api/config/types/config-constants.ts | 7 +++++-- .../src/core/components/geolocator/geolocator.tsx | 4 ++-- .../core/components/geolocator/hooks/use-geolocator.ts | 6 ++++-- .../geoview-core/src/core/components/nav-bar/nav-bar.tsx | 4 ++-- packages/geoview-core/src/geo/map/map-viewer.ts | 5 +++-- 7 files changed, 23 insertions(+), 15 deletions(-) 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/src/api/config/types/classes/map-feature-config.ts b/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts index 59f35fd44d3..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 <= 20 ? 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 <= 20 ? 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 4e70edf28b9..4da7ea16658 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -144,6 +144,9 @@ export const CV_MAP_CENTER: Record = { 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: 20, + 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/core/components/geolocator/geolocator.tsx b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx index bab7335bbf9..d77ddd100bb 100644 --- a/packages/geoview-core/src/core/components/geolocator/geolocator.tsx +++ b/packages/geoview-core/src/core/components/geolocator/geolocator.tsx @@ -10,8 +10,8 @@ import { FocusTrapContainer } from '@/core/components/common'; import { useGeoViewMapId } from '@/core/stores/geoview-store'; // import { handleEscapeKey } from '@/core/utils/utilities'; import { logger } from '@/core/utils/logger'; -import { useGeolocator } from './hooks/use-geolocator'; -import { GeolocatorBar } from './geolocator-bar'; +import { useGeolocator } from '@/core/components/geolocator/hooks/use-geolocator'; +import { GeolocatorBar } from '@/core/components/geolocator/geolocator-bar'; export interface GeoListItem { key: string; 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 index 2e1661239a1..e924cc0e0f3 100644 --- a/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts +++ b/packages/geoview-core/src/core/components/geolocator/hooks/use-geolocator.ts @@ -123,6 +123,8 @@ export const useGeolocator = (): UseGeolocatorReturn => { setIsLoading(false); clearTimeout(fetchTimerRef.current); } + + return Promise.resolve(); }, [geolocatorServiceURL] ); @@ -149,9 +151,9 @@ export const useGeolocator = (): UseGeolocatorReturn => { displayLanguageRef.current = displayLanguage; getGeolocations(searchValue); - // Only listen to change in language to request new value with updated language + // Only listen to change in language and getGeolocations to request new value with updated language // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displayLanguage]); + }, [displayLanguage, getGeolocations]); return { data, 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 061878e1fb5..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 @@ -57,10 +57,10 @@ export function NavBar(props: NavBarProps): JSX.Element { // Store const navBarComponents = useUINavbarComponents(); - // State + // Ref const navBarRef = useRef(null); - // Ref + // State const [buttonPanelGroups, setButtonPanelGroups] = useState(defaultButtonGroups); useEffect(() => { diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index 7a9d6c9a570..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 || 23, + minZoom: mapViewSettings.minZoom || CV_VALID_ZOOM_LEVELS[0], + maxZoom: mapViewSettings.maxZoom || CV_VALID_ZOOM_LEVELS[1], rotation: mapViewSettings.rotation || 0, }), controls: [], From 70a7ea65536b567d182e6eea9cf0ee3873025330 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Wed, 26 Feb 2025 15:05:09 -0500 Subject: [PATCH 8/8] review comment --- packages/geoview-core/src/ui/icons/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/geoview-core/src/ui/icons/index.ts b/packages/geoview-core/src/ui/icons/index.ts index dc73171fa15..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,