Current Behavior
When a map config uses viewSettings.initialView.layerIds to zoom to a GeoJSON layer (especially one with a single-point or narrow extent) on EPSG:3978, the map shows the error notification "Unable to zoom, the provided extent is invalid" during initialization. The map falls back to the default Canada extent instead of zooming to the layer.
However, using the "Zoom to layer extent" button in the layers panel for the same layer works correctly — the map zooms to the layer's extent without error.
Expected Behavior
The map should zoom to the GeoJSON layer's extent during initialization when initialView.layerIds is configured, exactly as the "Zoom to layer extent" button does.
Steps To Reproduce
- Create a GeoJSON layer config with a single point (or narrow extent) data source
- Set
viewSettings.initialView.layerIds to reference that layer path
- Set
viewSettings.projection to 3978
- Load the map
- Observe the error notification: "Unable to zoom, the provided extent is invalid"
- After the map loads, click the "Zoom to layer extent" button for the same layer — it works
Config excerpt:
{
"map": {
"viewSettings": {
"initialView": {
"layerIds": ["geojsonLYR1/programsBlank"]
},
"projection": 3978
},
"listOfGeoviewLayerConfig": [
{
"geoviewLayerId": "geojsonLYR1",
"geoviewLayerType": "GeoJSON",
"listOfLayerEntryConfig": [
{
"layerId": "programsBlank",
"source": {
"dataAccessPath": "<url-to-geojson-with-single-point>"
}
}
]
}
]
}
}
Root Cause Analysis
The error is thrown in MapController.zoomToExtent() when the extent fails a JSON.stringify comparison between the original and validated extents:
// map-controller.ts, lines 195-207
const validatedExtent = GeoUtilities.validateExtent(extent, this.getMapViewer().getProjectionEPSG());
if (
!extent.some((number) => {
return (!number && number !== 0) || Number.isNaN(number);
}) &&
JSON.stringify(extent) === JSON.stringify(validatedExtent)
) {
this.getMapViewer().getView().fit(extent, mergedOptions);
// ...
}
// Else → showError('error.map.invalidZoomExtent') + throw InvalidExtentError
Two code paths compared
| Aspect |
Init path (#zoomOnLayerIdsMaybe) |
Button path (zoomToLayerExtent) |
| Trigger |
Auto-fires after #checkMapLayersLoaded() |
User clicks "Zoom to layer extent" |
| Extent source |
getExtentOfMultipleLayers() → getBounds() → source.getExtent() → validateExtent() |
getStoreLayerBounds() (set earlier by fire-and-forget setStoreLayerBoundsForLayerAndParentsAndForget which also calls getBounds()) |
| Passes to |
this.zoomToExtent(layerExtents) (no explicit options) |
mapController.zoomToExtent(bounds, options) with padding & duration |
| Result |
ERROR |
Works |
Both paths call getBounds() → validateExtent() internally, then pass the result to zoomToExtent() which calls validateExtent() again. Since validateExtent() is idempotent (applying it twice produces the same result), both paths should behave identically — but they don't.
Possible causes (need runtime debugging to confirm)
-
Degenerate (zero-area) extent from a single point: For a single-point layer, source.getExtent() returns [x, y, x, y]. While validateExtent() handles this correctly for in-bounds coordinates, the degenerate extent may cause issues with OL's view.fit() or there may be edge cases when coordinates sit exactly at projection boundaries. The existing bufferExtent() utility (GeoUtilities.bufferExtent()) could protect against this but is not used anywhere in the zoom pipeline.
-
Feature coordinates exceeding EPSG:3978 max extents: If GeoJSON features have coordinates (in EPSG:4326) that, when projected to EPSG:3978 (LCC Canada), exceed the hardcoded max extents [-7192737.96, -3004297.73, 5183275.29, 4484204.83], validateExtent() clamps the values, producing a different extent → JSON comparison fails. This is especially likely for features at extreme latitudes/longitudes (Arctic, Pacific).
-
Missing -Infinity guard: #zoomOnLayerIdsMaybe checks layerExtents.includes(Infinity) to fall back to defaults, but does NOT check for -Infinity. Similarly, zoomToInitialExtent (HOME button) has the same gap. If getBounds() somehow returns an extent containing -Infinity, it would bypass the guard and cause the validation error.
-
Timing difference: Although #checkMapLayersLoaded() awaits all layers to reach 'loaded' status before #zoomOnLayerIdsMaybe() fires, the store bounds (setStoreLayerBoundsForLayerAndParentsAndForget()) are set via a fire-and-forget pattern. By the time the user clicks the button, the store has settled. At init time, the direct getBounds() call may encounter a subtly different source state.
Proposed Fix
1. Buffer degenerate extents in zoomToExtent() or #zoomOnLayerIdsMaybe()
Use the existing GeoUtilities.bufferExtent() to expand zero-area (or near-zero-area) extents before passing to view.fit():
// In zoomToExtent() or in #zoomOnLayerIdsMaybe()
const width = extent[2] - extent[0];
const height = extent[3] - extent[1];
if (width === 0 && height === 0) {
extent = GeoUtilities.bufferExtent(extent); // default buffer: 5000 map units
}
2. Fix the -Infinity guard in #zoomOnLayerIdsMaybe() and zoomToInitialExtent()
// Current (misses -Infinity):
if (!layerExtents || layerExtents.includes(Infinity))
// Fixed:
if (!layerExtents || layerExtents.some((v) => !Number.isFinite(v)))
3. Add debug logging in zoomToExtent() when validation fails
Log the raw and validated extents when the JSON comparison fails, to aid future diagnosis:
logger.logWarning(`zoomToExtent validation failed. Raw: ${JSON.stringify(extent)}, Validated: ${JSON.stringify(validatedExtent)}`);
4. Consider using store bounds in #zoomOnLayerIdsMaybe()
Instead of calling getExtentOfMultipleLayers() (which calls getBounds() directly), read the already-settled store bounds — the same source that zoomToLayerExtent() uses successfully.
Affected Files
packages/geoview-core/src/core/controllers/map-controller.ts — zoomToExtent() validation logic, zoomToInitialExtent() Infinity guard
packages/geoview-core/src/geo/map/map-viewer.ts — #zoomOnLayerIdsMaybe() Infinity guard
packages/geoview-core/src/geo/utils/utilities.ts — validateExtent() behavior, bufferExtent() candidate for reuse
packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts — onGetBounds() where source extent is validated
packages/geoview-core/src/core/stores/states/layer-state.ts — setStoreLayerBoundsForLayerAndParentsAndForget() fire-and-forget timing
Additional Context
- The error notification string is
'error.map.invalidZoomExtent', thrown via InvalidExtentError
- The HOME button (
zoomToInitialExtent) has the same code pattern and may also be affected
validateExtent() is idempotent — double-validation produces the same result — which makes the discrepancy between the two paths puzzling without runtime inspection of the actual extent values
- Adding a
console.log of the extent values in zoomToExtent() before the JSON comparison would immediately reveal the root cause
Current Behavior
When a map config uses
viewSettings.initialView.layerIdsto zoom to a GeoJSON layer (especially one with a single-point or narrow extent) on EPSG:3978, the map shows the error notification "Unable to zoom, the provided extent is invalid" during initialization. The map falls back to the default Canada extent instead of zooming to the layer.However, using the "Zoom to layer extent" button in the layers panel for the same layer works correctly — the map zooms to the layer's extent without error.
Expected Behavior
The map should zoom to the GeoJSON layer's extent during initialization when
initialView.layerIdsis configured, exactly as the "Zoom to layer extent" button does.Steps To Reproduce
viewSettings.initialView.layerIdsto reference that layer pathviewSettings.projectionto3978Config excerpt:
{ "map": { "viewSettings": { "initialView": { "layerIds": ["geojsonLYR1/programsBlank"] }, "projection": 3978 }, "listOfGeoviewLayerConfig": [ { "geoviewLayerId": "geojsonLYR1", "geoviewLayerType": "GeoJSON", "listOfLayerEntryConfig": [ { "layerId": "programsBlank", "source": { "dataAccessPath": "<url-to-geojson-with-single-point>" } } ] } ] } }Root Cause Analysis
The error is thrown in
MapController.zoomToExtent()when the extent fails aJSON.stringifycomparison between the original and validated extents:Two code paths compared
#zoomOnLayerIdsMaybe)zoomToLayerExtent)#checkMapLayersLoaded()getExtentOfMultipleLayers()→getBounds()→source.getExtent()→validateExtent()getStoreLayerBounds()(set earlier by fire-and-forgetsetStoreLayerBoundsForLayerAndParentsAndForgetwhich also callsgetBounds())this.zoomToExtent(layerExtents)(no explicit options)mapController.zoomToExtent(bounds, options)with padding & durationBoth paths call
getBounds()→validateExtent()internally, then pass the result tozoomToExtent()which callsvalidateExtent()again. SincevalidateExtent()is idempotent (applying it twice produces the same result), both paths should behave identically — but they don't.Possible causes (need runtime debugging to confirm)
Degenerate (zero-area) extent from a single point: For a single-point layer,
source.getExtent()returns[x, y, x, y]. WhilevalidateExtent()handles this correctly for in-bounds coordinates, the degenerate extent may cause issues with OL'sview.fit()or there may be edge cases when coordinates sit exactly at projection boundaries. The existingbufferExtent()utility (GeoUtilities.bufferExtent()) could protect against this but is not used anywhere in the zoom pipeline.Feature coordinates exceeding EPSG:3978 max extents: If GeoJSON features have coordinates (in EPSG:4326) that, when projected to EPSG:3978 (LCC Canada), exceed the hardcoded max extents
[-7192737.96, -3004297.73, 5183275.29, 4484204.83],validateExtent()clamps the values, producing a different extent → JSON comparison fails. This is especially likely for features at extreme latitudes/longitudes (Arctic, Pacific).Missing
-Infinityguard:#zoomOnLayerIdsMaybecheckslayerExtents.includes(Infinity)to fall back to defaults, but does NOT check for-Infinity. Similarly,zoomToInitialExtent(HOME button) has the same gap. IfgetBounds()somehow returns an extent containing-Infinity, it would bypass the guard and cause the validation error.Timing difference: Although
#checkMapLayersLoaded()awaits all layers to reach'loaded'status before#zoomOnLayerIdsMaybe()fires, the store bounds (setStoreLayerBoundsForLayerAndParentsAndForget()) are set via a fire-and-forget pattern. By the time the user clicks the button, the store has settled. At init time, the directgetBounds()call may encounter a subtly different source state.Proposed Fix
1. Buffer degenerate extents in
zoomToExtent()or#zoomOnLayerIdsMaybe()Use the existing
GeoUtilities.bufferExtent()to expand zero-area (or near-zero-area) extents before passing toview.fit():2. Fix the
-Infinityguard in#zoomOnLayerIdsMaybe()andzoomToInitialExtent()3. Add debug logging in
zoomToExtent()when validation failsLog the raw and validated extents when the JSON comparison fails, to aid future diagnosis:
4. Consider using store bounds in
#zoomOnLayerIdsMaybe()Instead of calling
getExtentOfMultipleLayers()(which callsgetBounds()directly), read the already-settled store bounds — the same source thatzoomToLayerExtent()uses successfully.Affected Files
packages/geoview-core/src/core/controllers/map-controller.ts—zoomToExtent()validation logic,zoomToInitialExtent()Infinity guardpackages/geoview-core/src/geo/map/map-viewer.ts—#zoomOnLayerIdsMaybe()Infinity guardpackages/geoview-core/src/geo/utils/utilities.ts—validateExtent()behavior,bufferExtent()candidate for reusepackages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts—onGetBounds()where source extent is validatedpackages/geoview-core/src/core/stores/states/layer-state.ts—setStoreLayerBoundsForLayerAndParentsAndForget()fire-and-forget timingAdditional Context
'error.map.invalidZoomExtent', thrown viaInvalidExtentErrorzoomToInitialExtent) has the same code pattern and may also be affectedvalidateExtent()is idempotent — double-validation produces the same result — which makes the discrepancy between the two paths puzzling without runtime inspection of the actual extent valuesconsole.logof the extent values inzoomToExtent()before the JSON comparison would immediately reveal the root cause