diff --git a/packages/geoview-core/public/configs/navigator/05-layer-zoom-levels.json b/packages/geoview-core/public/configs/navigator/05-layer-zoom-levels.json new file mode 100644 index 00000000000..394ea4ae369 --- /dev/null +++ b/packages/geoview-core/public/configs/navigator/05-layer-zoom-levels.json @@ -0,0 +1,76 @@ +{ + "map": { + "interaction": "dynamic", + "viewSettings": { + "projection": 3978 + }, + "basemapOptions": { + "basemapId": "transport", + "shaded": false, + "labeled": true + }, + "listOfGeoviewLayerConfig": [ + { + "geoviewLayerId": "wmsLYR1", + "geoviewLayerName": "earthquakes", + "metadataAccessPath": "https://maps-cartes.services.geo.ca/server_serveur/rest/services/NRCan/earthquakes_en/MapServer/", + "geoviewLayerType": "esriDynamic", + "listOfLayerEntryConfig": [ + { + "layerId": "0", + "layerName": "Limited by Scale - Earthquakes 1980 - 1990, by Magnitude", + "maxScale": 10000000 + } + ] + }, + { + "geoviewLayerId": "89d44ba6-7236-48ed-afab-f25a98c846ef", + "geoviewLayerType": "geoCore", + "listOfLayerEntryConfig": [ + { + "layerId": "pub:WHSE_IMAGERY_AND_BASE_MAPS.MOT_CULVERTS_SP", + "layerName": "Limited by Service - BC Minitstry of Transportation Culverts" + } + ] + }, + { + "geoviewLayerId": "uniqueValueId", + "geoviewLayerName": "uniqueValue", + "metadataAccessPath": "https://maps-cartes.ec.gc.ca/arcgis/rest/services/CESI/MapServer/", + "geoviewLayerType": "esriDynamic", + "listOfLayerEntryConfig": [ + { + "layerId": "4", + "layerName": "Limited by Zoom - Water Quality", + "entryType": "group", + "initialSettings": { + "minZoom": 5 + }, + "listOfLayerEntryConfig": [ + { + "layerId": "5", + "initialSettings": { + "minZoom": 7 + } + }, + { + "layerId": "6" + }, + { + "layerId": "7" + } + ] + } + ] + } + ] + }, + "components": ["overview-map"], + "footerBar": { + "tabs": { + "core": ["legend", "layers", "details", "data-table"] + } + }, + "corePackages": [], + "theme": "geo.ca" +} diff --git a/packages/geoview-core/public/configs/navigator/15-xyz-tile.json b/packages/geoview-core/public/configs/navigator/15-xyz-tile.json index 8dea89f19f3..d0166a2f38f 100644 --- a/packages/geoview-core/public/configs/navigator/15-xyz-tile.json +++ b/packages/geoview-core/public/configs/navigator/15-xyz-tile.json @@ -22,7 +22,22 @@ "initialSettings": { "minZoom": 3, "maxZoom": 8 } } ] - } + }, + { + "geoviewLayerId": "xyzTilesLYR2", + "geoviewLayerName": "GNOSIS_Blue_Marble", + "metadataAccessPath": "https://maps.gnosis.earth/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad", + "geoviewLayerType": "xyzTiles", + "listOfLayerEntryConfig": [ + { + "layerId": "blueMarble", + "layerName": "GNOSIS Blue Marble", + "source": { + "dataAccessPath": "https://maps.gnosis.earth/ogcapi/collections/blueMarble/map/tiles/WebMercatorQuad/{z}/{y}/{x}.jpg" + } + } + ] + } ] }, "components": ["overview-map"], diff --git a/packages/geoview-core/public/configs/navigator/24-vector-tile.json b/packages/geoview-core/public/configs/navigator/24-vector-tile.json index 68344d53a34..2dba3eab5da 100644 --- a/packages/geoview-core/public/configs/navigator/24-vector-tile.json +++ b/packages/geoview-core/public/configs/navigator/24-vector-tile.json @@ -12,12 +12,26 @@ "listOfGeoviewLayerConfig": [ { "geoviewLayerId": "vectorTilesLYR1", - "geoviewLayerName": "new basemap", + "geoviewLayerName": "CBCT - French Basemap", "geoviewLayerType": "vectorTiles", "metadataAccessPath": "https://tiles.arcgis.com/tiles/HsjBaDykC1mjhXz9/arcgis/rest/services/CBMT_CBCT_3978_V_OSM/VectorTileServer/", "listOfLayerEntryConfig": [ { "layerId": "CBMT_CBCT_3978_V_OSM", + "styleUrl": "https://nrcan-rncan.maps.arcgis.com/sharing/rest/content/items/88ad9e2ef6e040a19472985e6606a2f9/resources/styles/root.json", + "initialSettings": { "minZoom": 3, "maxZoom": 18 } + } + ] + }, + { + "geoviewLayerId": "vectorTilesLYR2", + "geoviewLayerName": "CBMT - English Basemap", + "geoviewLayerType": "vectorTiles", + "metadataAccessPath": "https://tiles.arcgis.com/tiles/HsjBaDykC1mjhXz9/arcgis/rest/services/CBMT_CBCT_3978_V_OSM/VectorTileServer/", + "listOfLayerEntryConfig": [ + { + "layerId": "CBMT_CBCT_3978_V_OSM", + "styleUrl": "https://nrcan-rncan.maps.arcgis.com/sharing/rest/content/items/708e92c1f00941e3af3dd3c092ae4a0a/resources/styles/root.json", "initialSettings": { "minZoom": 3, "maxZoom": 18 } } ] diff --git a/packages/geoview-core/public/templates/demos-navigator.html b/packages/geoview-core/public/templates/demos-navigator.html index 928c4fa6052..038f57440ea 100644 --- a/packages/geoview-core/public/templates/demos-navigator.html +++ b/packages/geoview-core/public/templates/demos-navigator.html @@ -125,6 +125,7 @@

Configurations Navigator

+ diff --git a/packages/geoview-core/public/templates/sandbox.html b/packages/geoview-core/public/templates/sandbox.html index 97ce867e2f0..09d5fe58db2 100644 --- a/packages/geoview-core/public/templates/sandbox.html +++ b/packages/geoview-core/public/templates/sandbox.html @@ -198,9 +198,10 @@

Sanbox Map

try { // get config and test if JSON is valid const configArea = document.getElementById('configGeoview'); - const configJSON = JSON.parse(configArea.value.replaceAll(`'`, `"`)); + const regexExp = /(?Sanbox Map // TODO: the delete has a timeout so we need to wait before trying to recreate the map... // TO.DOCONT: this should be cleaner setTimeout(() => { - cgpv.api.createMapFromConfig('sandboxMap', document.getElementById('configGeoview').value.replaceAll("'", '"')) + // Regex expression to cature all single quotes (') except those preceded by a backslash (\) + const regexExp = /(? listenToLegendLayerSetChanges('sandboxMap-state', 'sandboxMap')); }, 1500); }); 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 ea684915444..17052c0a063 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -66,7 +66,7 @@ export const CV_CONST_LEAF_LAYER_SCHEMA_PATH: Record = { IMAGE_STATIC: 'https://cgpv/schema#/definitions/ImageStaticLayerEntryConfig', GEOPACKAGE: 'https://cgpv/schema#/definitions/VectorLayerEntryConfig', XYZ_TILES: 'https://cgpv/schema#/definitions/TileLayerEntryConfig', - VECTOR_TILES: 'Thttps://cgpv/schema#/definitions/TileLayerEntryConfig', + VECTOR_TILES: 'https://cgpv/schema#/definitions/TileLayerEntryConfig', OGC_FEATURE: 'https://cgpv/schema#/definitions/VectorLayerEntryConfig', CSV: 'https://cgpv/schema#/definitions/VectorLayerEntryConfig', }; diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts index fe85acd2cc4..bbf6e0e35aa 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts @@ -286,6 +286,8 @@ export class LegendEventProcessor extends AbstractEventProcessor { layerName, layerStatus: legendResultSetEntry.layerStatus, legendQueryStatus: legendResultSetEntry.legendQueryStatus, + maxZoom: layerConfig.initialSettings.maxZoom, + minZoom: layerConfig.initialSettings.minZoom, type: layerConfig.entryType as TypeGeoviewLayerType, canToggle: legendResultSetEntry.data?.type !== CONST_LAYER_TYPES.ESRI_IMAGE, opacity: layerConfig.initialSettings?.states?.opacity ? layerConfig.initialSettings.states.opacity : 1, @@ -328,6 +330,8 @@ export class LegendEventProcessor extends AbstractEventProcessor { layerName, layerStatus: legendResultSetEntry.layerStatus, legendQueryStatus: legendResultSetEntry.legendQueryStatus, + maxZoom: layerConfig.initialSettings.maxZoom, + minZoom: layerConfig.initialSettings.minZoom, styleConfig: legendResultSetEntry.data?.styleConfig, type: legendResultSetEntry.data?.type || (layerConfig.entryType as TypeGeoviewLayerType), canToggle: legendResultSetEntry.data?.type !== CONST_LAYER_TYPES.ESRI_IMAGE, diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts index 475a9fd6857..1a47f6eca29 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts @@ -430,9 +430,23 @@ export class MapEventProcessor extends AbstractEventProcessor { if (getAppCrosshairsActive(mapId)) this.getMapViewer(mapId).emitMapSingleClick(clickCoordinates); } - static setZoom(mapId: string, zoom: number): void { + static setLayerInVisibleRange(mapId: string, layerPath: string, inVisibleRange: boolean): void { + const { orderedLayerInfo } = this.getMapStateProtected(mapId); + const orderedLayer = orderedLayerInfo.find((layer) => layer.layerPath === layerPath); + + if (orderedLayer && orderedLayer.inVisibleRange !== inVisibleRange) { + orderedLayer.inVisibleRange = inVisibleRange; + this.setOrderedLayerInfoWithNoOrderChangeState(mapId, orderedLayerInfo); + } + } + + static setZoom(mapId: string, zoom: number, orderedLayerInfo?: TypeOrderedLayerInfo[]): void { // Save in store this.getMapStateProtected(mapId).setterActions.setZoom(zoom); + + if (orderedLayerInfo) { + this.setOrderedLayerInfoWithNoOrderChangeState(mapId, orderedLayerInfo); + } } static setIsMouseInsideMap(mapId: string, inside: boolean): void { @@ -588,16 +602,17 @@ export class MapEventProcessor extends AbstractEventProcessor { static getMapLegendCollapsedFromOrderedLayerInfo(mapId: string, layerPath: string): boolean { // Get legend status of a layer - const info = this.getMapStateProtected(mapId).orderedLayerInfo; - const pathInfo = info.find((item) => item.layerPath === layerPath); - return pathInfo?.legendCollapsed !== false; + return this.findMapLayerFromOrderedInfo(mapId, layerPath)?.legendCollapsed !== false; } static getMapVisibilityFromOrderedLayerInfo(mapId: string, layerPath: string): boolean { // Get visibility of a layer - const info = this.getMapStateProtected(mapId).orderedLayerInfo; - const pathInfo = info.find((item) => item.layerPath === layerPath); - return pathInfo?.visible !== false; + return this.findMapLayerFromOrderedInfo(mapId, layerPath)?.visible !== false; + } + + static getMapInVisibleRangeFromOrderedLayerInfo(mapId: string, layerPath: string): boolean { + // Get inVisibleRange of a layer + return this.findMapLayerFromOrderedInfo(mapId, layerPath)?.inVisibleRange !== false; } static addHighlightedFeature(mapId: string, feature: TypeFeatureInfoEntry): void { diff --git a/packages/geoview-core/src/app.tsx b/packages/geoview-core/src/app.tsx index 584d2984909..4dffb0a821b 100644 --- a/packages/geoview-core/src/app.tsx +++ b/packages/geoview-core/src/app.tsx @@ -192,8 +192,8 @@ export async function initMapDivFromFunctionCall(mapDiv: HTMLElement, mapConfig: // Create a data-config attribute and set config value on the div const att = document.createAttribute(url ? 'data-config-url' : 'data-config'); - // Clean apostrophes in the config - att.value = mapConfig.replaceAll("'", "\\'"); + // Clean apostrophes in the config if not escaped already + att.value = mapConfig.replaceAll(/(? ({ fontSize: theme.palette.geoViewFontSize.lg, fontWeight: '600', }, - '& .MuiListItem-root': { height: '100%', '& .MuiListItemButton-root': { @@ -93,4 +92,17 @@ export const getSxClasses = (theme: Theme): SxStyles => ({ layersInstructionsBody: { fontSize: theme.palette.geoViewFontSize.default, }, + outOfRange: { + '.layer-panel &.MuiListItemButton-root': { + backgroundColor: `${theme.palette.grey[200]} !important`, + '& .MuiListItemText-primary': { + color: `${theme.palette.grey[600]} !important`, + fontStyle: 'italic', + }, + '& .MuiListItemText-secondary': { + color: theme.palette.grey[500], + fontStyle: 'italic', + }, + }, + }, }); diff --git a/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx b/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx index 03c30786079..eb0f0668c13 100644 --- a/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx +++ b/packages/geoview-core/src/core/components/layers/left-panel/single-layer.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; import { animated } from '@react-spring/web'; -import { Theme } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; +import { getSxClasses } from '../../common/layer-list-style'; import { Collapse, IconButton, @@ -30,6 +31,7 @@ import { useMapStoreActions, useSelectorLayerLegendCollapsed, useSelectorLayerVisibility, + useSelectorLayerInVisibleRange, } from '@/core/stores/store-interface-and-intial-values/map-state'; import { DeleteUndoButton } from './delete-undo-button'; import { LayersList } from './layers-list'; @@ -56,6 +58,9 @@ export function SingleLayer({ depth, layer, showLayerDetailsPanel, isFirst, isLa const { t } = useTranslation(); + const theme = useTheme(); + const sxClasses = useMemo(() => getSxClasses(theme), [theme]); + // Get store states const { setSelectedLayerPath, setSelectedLayerSortingArrowId } = useLayerStoreActions(); const { setOrToggleLayerVisibility, setLegendCollapsed, reorderLayer } = useMapStoreActions(); @@ -70,6 +75,7 @@ export function SingleLayer({ depth, layer, showLayerDetailsPanel, isFirst, isLa useDataTableStoreActions(); const isVisible = useSelectorLayerVisibility(layer.layerPath); + const inVisibleRange = useSelectorLayerInVisibleRange(layer.layerPath); const legendExpanded = !useSelectorLayerLegendCollapsed(layer.layerPath); // TODO: I think we should favor using this pattern here, with the store, instead of working with the whole 'layer' object from the props @@ -250,7 +256,7 @@ export function SingleLayer({ depth, layer, showLayerDetailsPanel, isFirst, isLa sx={{ marginLeft: '0.4rem', height: '1.5rem', - backgroundColor: (theme: Theme) => theme.palette.geoViewColor.bgColor.dark[300], + backgroundColor: theme.palette.geoViewColor.bgColor.dark[300], }} variant="middle" flexItem @@ -446,7 +452,8 @@ export function SingleLayer({ depth, layer, showLayerDetailsPanel, isFirst, isLa void; } @@ -32,8 +39,8 @@ const styles = { // Extracted Header Component const LegendLayerHeader = memo( - ({ layer, isCollapsed, isVisible, tooltip, onExpandClick }: LegendLayerHeaderProps): JSX.Element => ( - + ({ layer, isCollapsed, isVisible, inVisibleRange, tooltip, onExpandClick }: LegendLayerHeaderProps): JSX.Element => ( + diff --git a/packages/geoview-core/src/core/components/legend/legend-styles.ts b/packages/geoview-core/src/core/components/legend/legend-styles.ts index d5e868ac3de..243ae86da59 100644 --- a/packages/geoview-core/src/core/components/legend/legend-styles.ts +++ b/packages/geoview-core/src/core/components/legend/legend-styles.ts @@ -94,6 +94,13 @@ export const getSxClasses = (theme: Theme, isFullScreen?: boolean, footerPanelRe }, }, }, + '& .outOfRange': { + backgroundColor: `${theme.palette.grey[200]}`, + '& .layerTitle': { + color: `${theme.palette.grey[600]}`, + fontStyle: 'italic', + }, + }, }, collapsibleContainer: { width: '100%', 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 31de4921b42..3ca88d233cd 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 @@ -82,6 +82,7 @@ export interface IMapState { resetBasemap: () => Promise; setLegendCollapsed: (layerPath: string, newValue?: boolean) => void; setOrToggleLayerVisibility: (layerPath: string, newValue?: boolean) => boolean; + setLayerInVisibleRange: (layerPath: string, newValue: boolean) => void; setMapKeyboardPanInteractions: (panDelta: number) => void; setZoom: (zoom: number, duration?: number) => void; setInteraction: (interaction: TypeInteraction) => void; @@ -347,6 +348,16 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt return MapEventProcessor.setOrToggleMapLayerVisibility(get().mapId, layerPath, newValue); }, + /** + * Sets or toggles the visibility of a layer. + * @param {string} layerPath - The path of the layer. + * @param {boolean} [newValue] - The new value of visibility. + */ + setLayerInVisibleRange: (layerPath: string, newValue: boolean): void => { + // Redirect to processor + MapEventProcessor.setLayerInVisibleRange(get().mapId, layerPath, newValue); + }, + /** * Sets the map keyboard pan interactions. * @param {number} panDelta - The pan delta value. @@ -889,6 +900,7 @@ export interface TypeOrderedLayerInfo { layerPath: string; queryable?: boolean; visible: boolean; + inVisibleRange: boolean; legendCollapsed: boolean; } @@ -939,6 +951,15 @@ export const useSelectorLayerVisibility = (layerPath: string): boolean => { return MapEventProcessor.findMapLayerFromOrderedInfo(geoviewStore.getState().mapId, layerPath, orderedLayerInfo)?.visible || false; }; +export const useSelectorLayerInVisibleRange = (layerPath: string): boolean => { + // Get the store + const geoviewStore = useGeoViewStore(); + // Hook + const orderedLayerInfo = useStore(geoviewStore, (state) => state.mapState.orderedLayerInfo); + // Redirect + return MapEventProcessor.findMapLayerFromOrderedInfo(geoviewStore.getState().mapId, layerPath, orderedLayerInfo)?.inVisibleRange || false; +}; + export const useSelectorLayerLegendCollapsed = (layerPath: string): boolean => { // Get the store const geoviewStore = useGeoViewStore(); diff --git a/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts b/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts index 20f69c5f0e0..03897308ed4 100644 --- a/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts +++ b/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts @@ -45,6 +45,12 @@ export abstract class ConfigBaseClass { /** It is used to link the layer entry config to the GeoView layer config. */ geoviewLayerConfig = {} as TypeGeoviewLayerConfig; + /** The min scale that can be reach by the layer. */ + minScale?: number; + + /** The max scale that can be reach by the layer. */ + maxScale?: number; + /** * Initial settings to apply to the GeoView layer entry at creation time. Initial settings are inherited from the parent in the * configuration tree. diff --git a/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/vector-tiles-layer-entry-config.ts b/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/vector-tiles-layer-entry-config.ts index 7361f898536..fceace7bc8c 100644 --- a/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/vector-tiles-layer-entry-config.ts +++ b/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/vector-tiles-layer-entry-config.ts @@ -7,6 +7,8 @@ export class VectorTilesLayerEntryConfig extends TileLayerEntryConfig { tileGrid!: TypeTileGrid; + styleUrl?: string; + /** * The class constructor. * @param {VectorTilesLayerEntryConfig} layerConfig - The layer configuration we want to instanciate. @@ -23,9 +25,10 @@ export class VectorTilesLayerEntryConfig extends TileLayerEntryConfig { if (!this.source) this.source = {}; if (!this.source.dataAccessPath) this.source.dataAccessPath = this.geoviewLayerConfig.metadataAccessPath; - if (!this.source.dataAccessPath!.toLowerCase().endsWith('.pbf')) + if (!this.source.dataAccessPath!.toLowerCase().endsWith('.pbf')) { this.source.dataAccessPath = this.source.dataAccessPath!.endsWith('/') - ? `${this.source.dataAccessPath}${this.layerId}` - : `${this.source.dataAccessPath}/${this.layerId}`; + ? `${this.source.dataAccessPath}tile/{z}/{y}/{x}.pbf` + : `${this.source.dataAccessPath}/tile/{z}/{y}/{x}.pbf`; + } } } diff --git a/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config.ts b/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config.ts index 6ec86db84dd..c9b4e8e0b07 100644 --- a/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config.ts +++ b/packages/geoview-core/src/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config.ts @@ -20,7 +20,7 @@ export class XYZTilesLayerEntryConfig extends TileLayerEntryConfig { if (!this.source) this.source = {}; if (!this.source.dataAccessPath) this.source.dataAccessPath = this.geoviewLayerConfig.metadataAccessPath; - if (!this.source.dataAccessPath!.endsWith('{z}/{y}/{x}')) + if (!this.source.dataAccessPath!.includes('{z}/{y}/{x}')) this.source.dataAccessPath = this.source.dataAccessPath!.endsWith('/') ? `${this.source.dataAccessPath}tile/{z}/{y}/{x}` : `${this.source.dataAccessPath}/tile/{z}/{y}/{x}`; diff --git a/packages/geoview-core/src/core/utils/utilities.ts b/packages/geoview-core/src/core/utils/utilities.ts index 0c91d78663c..59eff0c35e6 100644 --- a/packages/geoview-core/src/core/utils/utilities.ts +++ b/packages/geoview-core/src/core/utils/utilities.ts @@ -2,6 +2,7 @@ import { Root, createRoot } from 'react-dom/client'; import sanitizeHtml from 'sanitize-html'; import { TypeDisplayLanguage } from '@config/types/map-schema-types'; +import View from 'ol/View'; import { Cast, TypeJsonArray, TypeJsonObject, TypeJsonValue } from '@/core/types/global-types'; import { logger } from '@/core/utils/logger'; import i18n from '@/core/translation/i18n'; @@ -551,3 +552,57 @@ export function isElementInViewport(el: Element): boolean { rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } + +/** + * Convert a map scale to zoom level + * @param view The view for converting the scale + * @param targetScale The desired scale (e.g. 50000 for 1:50,000) + * @returns number representing the closest zoom level for the given scale + */ +export const getZoomFromScale = (view: View, targetScale: number): number | undefined => { + const projection = view.getProjection(); + const mpu = projection.getMetersPerUnit(); + const dpi = 25.4 / 0.28; // OpenLayers default DPI + + // Calculate resolution from scale + if (!mpu) return undefined; + // Resolution = Scale / ( metersPerUnit * inchesPerMeter * DPI ) + const targetResolution = targetScale / (mpu * 39.37 * dpi); + + // Get the constrained resolution that matches our tile matrix + const constrainedResolution = view.getConstrainedResolution(targetResolution); + + // Convert resolution to zoom + if (!constrainedResolution) return undefined; + return view.getZoomForResolution(constrainedResolution) || undefined; +}; + +/** + * Convert a map scale to zoom level + * @param view The view for converting the zoom + * @param zoom The desired zoom (e.g. 50000 for 1:50,000) + * @returns number representing the closest scale for the given zoom number + */ +export const getScaleFromZoom = (view: View, zoom: number): number | undefined => { + const projection = view.getProjection(); + const mpu = projection.getMetersPerUnit(); + if (!mpu) return undefined; + + const dpi = 25.4 / 0.28; // OpenLayers default DPI + + // Get resolution for zoom level + const resolution = view.getResolutionForZoom(zoom); + + // Calculate scale from resolution + // Scale = Resolution * metersPerUnit * inchesPerMeter * DPI + return resolution * mpu * 39.37 * dpi; +}; + +/** + * Get map scale for Web Mercator or Lambert Conformal Conic projections + * @param view The view to get the current scale from + * @returns number representing scale (e.g. 50000 for 1:50,000) + */ +export const getMapScale = (view: View): number | undefined => { + return getScaleFromZoom(view, view.getZoom() || 0); +}; 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 e7df3c31e5c..e6c5dae2afd 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 @@ -7,7 +7,7 @@ import cloneDeep from 'lodash/cloneDeep'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; import { Cast, TypeJsonArray, TypeJsonObject } from '@/core/types/global-types'; -import { getXMLHttpRequest } from '@/core/utils/utilities'; +import { getXMLHttpRequest, getZoomFromScale } from '@/core/utils/utilities'; import { validateExtent, validateExtentWhenDefined } from '@/geo/utils/utilities'; import { Projection } from '@/geo/utils/projection'; import { TimeDimensionESRI, DateMgt } from '@/core/utils/date-mgt'; @@ -341,8 +341,8 @@ export function commonProcessInitialSettings( if (layerConfig.initialSettings?.states?.visible === undefined) layerConfig.initialSettings!.states = { visible: !!layerMetadata.defaultVisibility }; // GV TODO: The solution implemented in the following two lines is not right. scale and zoom are not the same things. - // GV if (layerConfig.initialSettings?.minZoom === undefined && minScale !== 0) layerConfig.initialSettings.minZoom = minScale; - // GV if (layerConfig.initialSettings?.maxZoom === undefined && maxScale !== 0) layerConfig.initialSettings.maxZoom = maxScale; + if (layerConfig.minScale === undefined && layerMetadata.minScale !== 0) layerConfig.minScale = layerMetadata.minScale as number; + if (layerConfig.maxScale === undefined && layerMetadata.maxScale !== 0) layerConfig.maxScale = layerMetadata.maxScale as number; layerConfig.initialSettings.extent = validateExtentWhenDefined(layerConfig.initialSettings.extent); @@ -365,6 +365,24 @@ export function commonProcessInitialSettings( } } + // Set zoom limits for max / min zooms + // GV Note: minScale is actually the maxZoom and maxScale is actually the minZoom + // GV As the scale gets smaller, the zoom gets larger + const mapView = layer.getMapViewer().getView(); + if (layerConfig.minScale) { + const maxScaleZoomLevel = getZoomFromScale(mapView, layerConfig.minScale); + if (maxScaleZoomLevel && (!layerConfig.initialSettings.maxZoom || maxScaleZoomLevel > layerConfig.initialSettings.maxZoom)) { + layerConfig.initialSettings.maxZoom = maxScaleZoomLevel; + } + } + + if (layerConfig.maxScale) { + const minScaleZoomLevel = getZoomFromScale(mapView, layerConfig.maxScale); + if (minScaleZoomLevel && (!layerConfig.initialSettings.minZoom || minScaleZoomLevel < layerConfig.initialSettings.minZoom)) { + layerConfig.initialSettings.minZoom = minScaleZoomLevel; + } + } + layerConfig.initialSettings!.bounds = validateExtent(layerConfig.initialSettings!.bounds || [-180, -90, 180, 90]); } diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/vector-tiles.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/vector-tiles.ts index 34834f17edf..e8e8253ca87 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/vector-tiles.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/vector-tiles.ts @@ -17,7 +17,7 @@ import { } from '@/geo/map/map-schema-types'; import { TypeJsonObject } from '@/core/types/global-types'; import { validateExtentWhenDefined } from '@/geo/utils/utilities'; -import { api } from '@/app'; +import { api, getZoomFromScale } from '@/app'; import { VectorTilesLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/vector-tiles-layer-entry-config'; import { logger } from '@/core/utils/logger'; import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config'; @@ -200,13 +200,18 @@ export class VectorTiles extends AbstractGeoViewRaster { // TODO: Refactor - Layers refactoring. What is this doing? See how we can do this in the new layers. Can it be done before? const resolutions = sourceOptions.tileGrid.getResolutions(); - if (this.metadata?.defaultStyles) - applyStyle(olLayer, `${this.metadataAccessPath}${this.metadata.defaultStyles}/root.json`, { + let appliedStyle = layerConfig.styleUrl || (this.metadata?.defaultStyles as string); + + if (appliedStyle) { + if (!appliedStyle.endsWith('/root.json')) appliedStyle = `${appliedStyle}/root.json`; + + applyStyle(olLayer, appliedStyle, { resolutions: resolutions?.length ? resolutions : [], }).catch((error) => { // Log logger.logPromiseFailed('applyStyle in processOneLayerEntry in VectorTiles', error); }); + } return Promise.resolve(olLayer); } @@ -222,22 +227,57 @@ export class VectorTiles extends AbstractGeoViewRaster { // GV Layers Refactoring - Obsolete (in config?) protected override processLayerMetadata(layerConfig: AbstractBaseLayerEntryConfig): Promise { // Instance check - if (!(layerConfig instanceof VectorTilesLayerEntryConfig)) throw new Error('Invalid layer configuration type provided'); + const updatedLayerConfig = layerConfig; + if (!(updatedLayerConfig instanceof VectorTilesLayerEntryConfig)) throw new Error('Invalid layer configuration type provided'); if (this.metadata) { - const { tileInfo, fullExtent } = this.metadata; + const { tileInfo, fullExtent, minScale, maxScale, minZoom, maxZoom } = this.metadata; const newTileGrid: TypeTileGrid = { extent: [fullExtent.xmin as number, fullExtent.ymin as number, fullExtent.xmax as number, fullExtent.ymax as number], origin: [tileInfo.origin.x as number, tileInfo.origin.y as number], resolutions: (tileInfo.lods as Array).map(({ resolution }) => resolution as number), tileSize: [tileInfo.rows as number, tileInfo.cols as number], }; - // eslint-disable-next-line no-param-reassign - layerConfig.source!.tileGrid = newTileGrid; + updatedLayerConfig.source!.tileGrid = newTileGrid; - // eslint-disable-next-line no-param-reassign - layerConfig.initialSettings.extent = validateExtentWhenDefined(layerConfig.initialSettings.extent); + updatedLayerConfig.initialSettings.extent = validateExtentWhenDefined(updatedLayerConfig.initialSettings.extent); + + // Set zoom levels. Vector tiles may be unique as they can have both scale and zoom level properties + // First set the min/max scales based on the service / config + // * Infinity and -Infinity are used as extreme zoom level values in case the value is undefined + if (minScale !== undefined) { + updatedLayerConfig.minScale = Math.min(updatedLayerConfig.minScale ?? Infinity, minScale as number); + } + + if (maxScale !== undefined) { + updatedLayerConfig.maxScale = Math.max(updatedLayerConfig.maxScale ?? -Infinity, maxScale as number); + } + + // Second, set the min/max zoom levels based on the service / config + if (minZoom !== undefined) { + updatedLayerConfig.initialSettings.minZoom = Math.min(updatedLayerConfig.initialSettings.minZoom ?? Infinity, minZoom as number); + } + + if (maxZoom !== undefined) { + updatedLayerConfig.initialSettings.maxZoom = Math.max(updatedLayerConfig.initialSettings.maxZoom ?? -Infinity, maxZoom as number); + } + + // Third, use the now set scale and zoom levels to determine the actual max / min zoom based on both + const mapView = this.getMapViewer().getView(); + if (updatedLayerConfig.minScale) { + const maxScaleZoomLevel = getZoomFromScale(mapView, updatedLayerConfig.minScale); + if (maxScaleZoomLevel) { + updatedLayerConfig.initialSettings.maxZoom = Math.max(updatedLayerConfig.initialSettings.maxZoom ?? -Infinity, maxScaleZoomLevel); + } + } + + if (updatedLayerConfig.maxScale) { + const minScaleZoomLevel = getZoomFromScale(mapView, updatedLayerConfig.maxScale); + if (minScaleZoomLevel) { + updatedLayerConfig.initialSettings.minZoom = Math.min(updatedLayerConfig.initialSettings.minZoom ?? Infinity, minScaleZoomLevel); + } + } } - return Promise.resolve(layerConfig); + return Promise.resolve(updatedLayerConfig); } } diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts index 4f7cdfd91b6..b6bf0b5fcf6 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts @@ -13,7 +13,7 @@ import { AbstractGeoViewRaster } from '@/geo/layer/geoview-layers/raster/abstrac import { TypeLayerEntryConfig, TypeGeoviewLayerConfig, CONST_LAYER_ENTRY_TYPES, layerEntryIsGroupLayer } from '@/geo/map/map-schema-types'; import { DateMgt } from '@/core/utils/date-mgt'; import { validateExtent, validateExtentWhenDefined } from '@/geo/utils/utilities'; -import { api, WMS_PROXY_URL } from '@/app'; +import { api, WMS_PROXY_URL, getZoomFromScale } from '@/app'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; import { logger } from '@/core/utils/logger'; import { OgcWmsLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/ogc-wms-layer-entry-config'; @@ -379,8 +379,6 @@ export class WMS extends AbstractGeoViewRaster { if (layer.BoundingBox === undefined) layer.BoundingBox = parentLayer.BoundingBox; if (layer.Dimension === undefined) layer.Dimension = parentLayer.Dimension; if (layer.Attribution === undefined) layer.Attribution = parentLayer.Attribution; - if (layer.MaxScaleDenominator === undefined) layer.MaxScaleDenominator = parentLayer.MaxScaleDenominator; - if (layer.MaxScaleDenominator === undefined) layer.MaxScaleDenominator = parentLayer.MaxScaleDenominator; // Table 7 — Inheritance of Layer properties specified in the standard with 'add' behaviour. // AuthorityURL inheritance is not implemented in the following code. if (parentLayer.Style) { @@ -636,17 +634,45 @@ export class WMS extends AbstractGeoViewRaster { } if (!layerConfig.source.featureInfo) layerConfig.source.featureInfo = { queryable: !!layerCapabilities.queryable }; MapEventProcessor.setMapLayerQueryable(this.mapId, layerConfig.layerPath, layerConfig.source.featureInfo.queryable); - // TODO: The solution implemented in the following lines is not right. scale and zoom are not the same things. - // if (layerConfig.initialSettings?.minZoom === undefined && layerCapabilities.MinScaleDenominator !== undefined) - // layerConfig.initialSettings.minZoom = layerCapabilities.MinScaleDenominator as number; - // if (layerConfig.initialSettings?.maxZoom === undefined && layerCapabilities.MaxScaleDenominator !== undefined) - // layerConfig.initialSettings.maxZoom = layerCapabilities.MaxScaleDenominator as number; + + // TODO Add Scale and Zoom level changes to config + // TODO Since web map runs mostly in zoom levels, may not need Scale limits + // Set Min/Max Scale Limits + if ( + layerCapabilities.MinScaleDenominator !== undefined && + (layerConfig.minScale === undefined || layerConfig.minScale > (layerCapabilities.MinScaleDenominator as unknown as number)) + ) + layerConfig.minScale = layerCapabilities.MinScaleDenominator as number; + if ( + layerCapabilities.MaxScaleDenominator !== undefined && + (layerConfig.maxScale === undefined || layerConfig.maxScale < (layerCapabilities.MaxScaleDenominator as unknown as number)) + ) + layerConfig.maxScale = layerCapabilities.MaxScaleDenominator as number; layerConfig.initialSettings.extent = validateExtentWhenDefined(layerConfig.initialSettings.extent); if (!layerConfig.initialSettings?.bounds && layerCapabilities.EX_GeographicBoundingBox) layerConfig.initialSettings!.bounds = validateExtent(layerCapabilities.EX_GeographicBoundingBox as Extent); + // Set zoom limits for max / min zooms + // GV Note: minScale is actually the maxZoom and maxScale is actually the minZoom + // GV As the scale gets smaller, the zoom gets larger + const mapView = this.getMapViewer().getView(); + if (layerConfig.minScale) { + const maxScaleZoomLevel = getZoomFromScale(mapView, layerConfig.minScale); + if (maxScaleZoomLevel && (!layerConfig.initialSettings.maxZoom || maxScaleZoomLevel > layerConfig.initialSettings.maxZoom)) { + layerConfig.initialSettings.maxZoom = maxScaleZoomLevel; + } + } + + if (layerConfig.maxScale) { + const minScaleZoomLevel = getZoomFromScale(mapView, layerConfig.maxScale); + if (minScaleZoomLevel && (!layerConfig.initialSettings.minZoom || minScaleZoomLevel < layerConfig.initialSettings.minZoom)) { + layerConfig.initialSettings.minZoom = minScaleZoomLevel; + } + } + + // Set time dimension if (layerCapabilities.Dimension) { const temporalDimension: TypeJsonObject | undefined = (layerCapabilities.Dimension as TypeJsonArray).find( (dimension) => dimension.name === 'time' diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/xyz-tiles.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/xyz-tiles.ts index b5439e41f5a..662ac430fea 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/xyz-tiles.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/xyz-tiles.ts @@ -13,10 +13,11 @@ import { TypeGeoviewLayerConfig, layerEntryIsGroupLayer, } from '@/geo/map/map-schema-types'; -import { Cast, toJsonObject } from '@/core/types/global-types'; +import { Cast, toJsonObject, TypeJsonArray, TypeJsonObject } from '@/core/types/global-types'; import { validateExtentWhenDefined } from '@/geo/utils/utilities'; import { XYZTilesLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config'; import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config'; +import { getZoomFromScale } from '@/app'; // ? Do we keep this TODO ? Dynamic parameters can be placed on the dataAccessPath and initial settings can be used on xyz-tiles. // TODO: Implement method to validate XYZ tile service @@ -142,6 +143,22 @@ export class XYZTiles extends AbstractGeoViewRaster { return; } + // ESRI MapServer Implementation + if (Array.isArray(this.metadata?.layers)) { + const metadataLayerList = this.metadata.layers; + const foundEntry = metadataLayerList.find((layerMetadata) => layerMetadata.id.toString() === layerConfig.layerId); + if (!foundEntry) { + this.layerLoadError.push({ + layer: layerPath, + loggerMessage: `XYZ layer not found (mapId: ${this.mapId}, layerPath: ${layerPath})`, + }); + // eslint-disable-next-line no-param-reassign + layerConfig.layerStatus = 'error'; + return; + } + return; + } + throw new Error( `Invalid GeoJSON metadata (listOfLayerEntryConfig) prevent loading of layer (mapId: ${this.mapId}, layerPath: ${layerPath})` ); @@ -214,20 +231,57 @@ export class XYZTiles extends AbstractGeoViewRaster { protected override processLayerMetadata(layerConfig: AbstractBaseLayerEntryConfig): Promise { // Instance check if (!(layerConfig instanceof XYZTilesLayerEntryConfig)) throw new Error('Invalid layer configuration type provided'); + const newLayerConfig = layerConfig; + // TODO Need to see why the metadata isn't handled properly for ESRI XYZ tiles. + // GV Possibly caused by a difference between OGC and ESRI XYZ Tiles, but only have ESRI XYZ Tiles as example currently + // GV Also, might be worth checking out OGCMapTile for this? https://openlayers.org/en/latest/examples/ogc-map-tiles-geographic.html + // GV Seems like it can deal with less specificity in the url and can handle the x y z internally? if (this.metadata) { - const metadataLayerConfigFound = Cast(this.metadata?.listOfLayerEntryConfig).find( - (metadataLayerConfig) => metadataLayerConfig.layerId === layerConfig.layerId - ); + let metadataLayerConfigFound: XYZTilesLayerEntryConfig | TypeJsonObject | undefined; + if (this.metadata?.listOfLayerEntryConfig) { + metadataLayerConfigFound = Cast(this.metadata?.listOfLayerEntryConfig).find( + (metadataLayerConfig) => metadataLayerConfig.layerId === newLayerConfig.layerId + ); + } + + // For ESRI MapServer XYZ Tiles + if (this.metadata?.layers) { + metadataLayerConfigFound = (this.metadata?.layers as TypeJsonArray).find( + (metadataLayerConfig) => metadataLayerConfig.id.toString() === newLayerConfig.layerId + ); + } + // metadataLayerConfigFound can not be undefined because we have already validated the config exist - this.setLayerMetadata(layerConfig.layerPath, toJsonObject(metadataLayerConfigFound)); - // eslint-disable-next-line no-param-reassign - layerConfig.source = defaultsDeep(layerConfig.source, metadataLayerConfigFound!.source); - // eslint-disable-next-line no-param-reassign - layerConfig.initialSettings = defaultsDeep(layerConfig.initialSettings, metadataLayerConfigFound!.initialSettings); - // eslint-disable-next-line no-param-reassign - layerConfig.initialSettings.extent = validateExtentWhenDefined(layerConfig.initialSettings.extent); + this.setLayerMetadata(newLayerConfig.layerPath, toJsonObject(metadataLayerConfigFound)); + newLayerConfig.source = defaultsDeep(newLayerConfig.source, metadataLayerConfigFound!.source); + newLayerConfig.initialSettings = defaultsDeep(newLayerConfig.initialSettings, metadataLayerConfigFound!.initialSettings); + newLayerConfig.initialSettings.extent = validateExtentWhenDefined(newLayerConfig.initialSettings.extent); + + // Set zoom limits for max / min zooms + newLayerConfig.maxScale = + (metadataLayerConfigFound?.maxScale as number) || ((metadataLayerConfigFound as TypeJsonObject)?.maxScaleDenominator as number); + + newLayerConfig.minScale = + (metadataLayerConfigFound?.minScale as number) || ((metadataLayerConfigFound as TypeJsonObject)?.minScaleDenominator as number); + + // GV Note: minScale is actually the maxZoom and maxScale is actually the minZoom + // GV As the scale gets smaller, the zoom gets larger + const mapView = this.getMapViewer().getView(); + if (newLayerConfig?.minScale) { + const maxScaleZoomLevel = getZoomFromScale(mapView, newLayerConfig.minScale as number); + if (maxScaleZoomLevel && (!newLayerConfig.initialSettings.maxZoom || maxScaleZoomLevel > newLayerConfig.initialSettings.maxZoom)) { + newLayerConfig.initialSettings.maxZoom = maxScaleZoomLevel; + } + } + + if (newLayerConfig?.maxScale) { + const minScaleZoomLevel = getZoomFromScale(mapView, newLayerConfig.maxScale as number); + if (minScaleZoomLevel && (!newLayerConfig.initialSettings.minZoom || minScaleZoomLevel < newLayerConfig.initialSettings.minZoom)) { + newLayerConfig.initialSettings.minZoom = minScaleZoomLevel; + } + } } - return Promise.resolve(layerConfig); + return Promise.resolve(newLayerConfig); } } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts index 3602ed48c7e..30823fc0e8b 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts @@ -216,15 +216,34 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { return this.#externalFragmentsOrder; } + /** + * Gets the in visible range value + * @returns {boolean} true if the layer is in visible range + */ + getInVisibleRange(): boolean { + const mapZoom = this.getMapViewer().getView().getZoom(); + return mapZoom! > this.getMinZoom() && mapZoom! <= this.getMaxZoom(); + } + /** * Overridable method called when the layer has been loaded correctly */ protected onLoaded(): void { + const layerConfig = this.getLayerConfig(); // Set the layer config status to loaded to keep mirroring the AbstractGeoViewLayer for now - this.getLayerConfig().layerStatus = 'loaded'; + layerConfig.layerStatus = 'loaded'; // Now that the layer is loaded, set its visibility correctly (had to be done in the loaded event, not before, per prior note in pre-refactor) - this.setVisible(this.getLayerConfig().initialSettings?.states?.visible !== false); + this.setVisible(layerConfig.initialSettings?.states?.visible !== false); + + // Set the zoom levels here to prevent the layer being stuck endlessly loading + if (layerConfig.initialSettings.maxZoom) { + this.setMaxZoom(layerConfig.initialSettings.maxZoom); + } + + if (layerConfig.initialSettings.minZoom) { + this.setMinZoom(layerConfig.initialSettings.minZoom); + } // Emit event this.#emitIndividualLayerLoaded({ layerPath: this.getLayerPath() }); @@ -666,10 +685,6 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { // eslint-disable-next-line no-param-reassign if (layerConfig.initialSettings?.extent !== undefined) layerOptions.extent = layerConfig.initialSettings.extent; // eslint-disable-next-line no-param-reassign - if (layerConfig.initialSettings?.maxZoom !== undefined) layerOptions.maxZoom = layerConfig.initialSettings.maxZoom; - // eslint-disable-next-line no-param-reassign - if (layerConfig.initialSettings?.minZoom !== undefined) layerOptions.minZoom = layerConfig.initialSettings.minZoom; - // eslint-disable-next-line no-param-reassign if (layerConfig.initialSettings?.states?.opacity !== undefined) layerOptions.opacity = layerConfig.initialSettings.states.opacity; } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/gv-group-layer.ts b/packages/geoview-core/src/geo/layer/gv-layers/gv-group-layer.ts index cd09cc8e80f..227091b964c 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/gv-group-layer.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/gv-group-layer.ts @@ -10,6 +10,12 @@ import { GroupLayerEntryConfig } from '@/core/utils/config/validation-classes/gr * @class GVGroupLayer */ export class GVGroupLayer extends AbstractBaseLayer { + /** Max zoom constant */ + static readonly MAX_ZOOM = 50; + + /** Min zoom constant */ + static readonly MIN_ZOOM = 0; + /** * Constructs a Group layer to manage an OpenLayer Group Layer. * @param {string} mapId - The map id @@ -19,6 +25,10 @@ export class GVGroupLayer extends AbstractBaseLayer { public constructor(mapId: string, olLayerGroup: LayerGroup, layerConfig: GroupLayerEntryConfig) { super(mapId, layerConfig); this.olLayer = olLayerGroup; + + // Set extreme zoom settings to group layer so sub layers can load + this.olLayer.setMaxZoom(GVGroupLayer.MAX_ZOOM); + this.olLayer.setMinZoom(GVGroupLayer.MIN_ZOOM); } /** diff --git a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts index 38ba62cb1e1..d6924050c3e 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts @@ -435,6 +435,16 @@ export abstract class AbstractLayerSet { return !((layer.getLayerConfig() as AbstractBaseLayerEntryConfig)?.initialSettings?.states?.queryable === false); } + /** + * Checks if the layer is in visible range. + * @param {AbstractGVLayer} layer - The layer + * @returns {boolean} True if the state is queryable or undefined + */ + protected static isInVisibleRange(layer: AbstractGVLayer): boolean { + // Return false when false or undefined + return layer.getInVisibleRange() ?? false; + } + /** * Align records with informatiom provided by OutFields from layer config. * This will update fields in and delete unwanted fields from the arrayOfRecords diff --git a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts index 9f4671195bc..4a906d61a2f 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts @@ -137,6 +137,9 @@ export class FeatureInfoLayerSet extends AbstractLayerSet { // If state is not queryable if (!AbstractLayerSet.isStateQueryable(layer)) return; + // If state is not in visible range + if (!AbstractLayerSet.isInVisibleRange(layer)) return; + // Flag processing this.resultSet[layerPath].features = undefined; this.resultSet[layerPath].queryStatus = 'processing'; diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index d977a289a16..b6ed75524ab 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -100,7 +100,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { */ #getOrderedLayerPaths(): string[] { // Get the map layer order - const mapLayerOrder = this.layerApi.mapViewer.getMapLayerOrderInfo(); + const mapLayerOrder = this.layerApi.mapViewer.getMapLayerOrderInfo().filter((layer) => layer.inVisibleRange); const resultSetLayers = new Set(Object.keys(this.resultSet)); // Filter and order the layers that are in our resultSet diff --git a/packages/geoview-core/src/geo/layer/layer.ts b/packages/geoview-core/src/geo/layer/layer.ts index c8867330024..ca23b725ec8 100644 --- a/packages/geoview-core/src/geo/layer/layer.ts +++ b/packages/geoview-core/src/geo/layer/layer.ts @@ -311,6 +311,7 @@ export class LayerApi { const addSubLayerPathToLayerOrder = (layerEntryConfig: TypeLayerEntryConfig, layerPath: string): void => { const subLayerPath = layerPath.endsWith(`/${layerEntryConfig.layerId}`) ? layerPath : `${layerPath}/${layerEntryConfig.layerId}`; + const layerInfo: TypeOrderedLayerInfo = { layerPath: subLayerPath, visible: layerEntryConfig.initialSettings?.states?.visible !== false, @@ -321,6 +322,7 @@ export class LayerApi { layerEntryConfig.initialSettings?.states?.legendCollapsed !== undefined ? layerEntryConfig.initialSettings?.states?.legendCollapsed : false, + inVisibleRange: true, }; newOrderedLayerInfos.push(layerInfo); if (layerEntryConfig.listOfLayerEntryConfig?.length) { @@ -342,6 +344,7 @@ export class LayerApi { ? geoviewLayerConfig.initialSettings?.states?.legendCollapsed : false, visible: geoviewLayerConfig.initialSettings?.states?.visible !== false, + inVisibleRange: true, }; newOrderedLayerInfos.push(layerInfo); (geoviewLayerConfig as TypeGeoviewLayerConfig).listOfLayerEntryConfig.forEach((layerEntryConfig) => { @@ -589,6 +592,7 @@ export class LayerApi { queryable: true, hoverable: true, legendCollapsed: false, + inVisibleRange: true, }; // GV: This is here as a placeholder so that the layers will appear in the proper order, @@ -996,6 +1000,17 @@ export class LayerApi { return gvGroupLayer; } + #setLayerInVisibleRange(layerConfig: TypeLayerEntryConfig): void { + if (layerConfig.listOfLayerEntryConfig) { + layerConfig.listOfLayerEntryConfig.forEach((subLayerConfig) => this.#setLayerInVisibleRange(subLayerConfig)); + } + + const zoom = this.mapViewer.getView().getZoom(); + const { maxZoom, minZoom } = layerConfig.initialSettings; + const inVisibleRange = (!maxZoom || zoom! <= maxZoom) && (!minZoom || zoom! > minZoom); + MapEventProcessor.setLayerInVisibleRange(this.getMapId(), layerConfig.layerPath, inVisibleRange); + } + /** * Continues the addition of the geoview layer. * Adds the layer to the map if valid. If not (is a string) emits an error. @@ -1023,6 +1038,9 @@ export class LayerApi { if (!geoviewLayer.allLayerStatusAreGreaterThanOrEqualTo('error')) { // Add the OpenLayers layer to the map officially this.mapViewer.map.addLayer(geoviewLayer.olRootLayer!); + + // Set in visible range property for all newly added layers + geoviewLayer.listOfLayerEntryConfig.forEach((layer) => this.#setLayerInVisibleRange(layer)); } // Log diff --git a/packages/geoview-core/src/geo/map/map-schema-types.ts b/packages/geoview-core/src/geo/map/map-schema-types.ts index 65382b66a70..4ba3308ea25 100644 --- a/packages/geoview-core/src/geo/map/map-schema-types.ts +++ b/packages/geoview-core/src/geo/map/map-schema-types.ts @@ -385,6 +385,10 @@ export type TypeGeoviewLayerConfig = { */ initialSettings?: TypeLayerInitialSettings; + /** Min and max scales */ + minScale?: number; + maxScale?: number; + /** The layer entries to use from the GeoView layer. */ listOfLayerEntryConfig: TypeLayerEntryConfig[]; }; diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index 9fefa600284..fa53d9984e4 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -455,8 +455,20 @@ export class MapViewer { // Read the zoom value const zoom = this.getView().getZoom()!; + // Get new inVisibleRange values for all layers + const newOrderedLayerInfo = this.getMapLayerOrderInfo(); + + // GV Used the configs since group GV layers have fake max/min zoom levels + // GV The group levels are also missing the zoom level getters, so this worked out well anyway + this.layer.getLayerEntryConfigs().forEach((config) => { + const { minZoom, maxZoom } = config.initialSettings; + const inVisibleRange = (!minZoom || zoom! > minZoom) && (!maxZoom || zoom! <= maxZoom); + const foundLayer = newOrderedLayerInfo.find((info) => info.layerPath === config.layerPath); + if (foundLayer) foundLayer.inVisibleRange = inVisibleRange; + }); + // Save in the store - MapEventProcessor.setZoom(this.mapId, zoom); + MapEventProcessor.setZoom(this.mapId, zoom, newOrderedLayerInfo); // Emit to the outside this.#emitMapZoomEnd({ zoom });