Skip to content

[BUG] "Unable to zoom, the provided extent is invalid" when using viewSettings.initialView.layerIds with a GeoJSON laye #3475

@jolevesq

Description

@jolevesq

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

  1. Create a GeoJSON layer config with a single point (or narrow extent) data source
  2. Set viewSettings.initialView.layerIds to reference that layer path
  3. Set viewSettings.projection to 3978
  4. Load the map
  5. Observe the error notification: "Unable to zoom, the provided extent is invalid"
  6. 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)

  1. 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.

  2. 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).

  3. 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.

  4. 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.tszoomToExtent() validation logic, zoomToInitialExtent() Infinity guard
  • packages/geoview-core/src/geo/map/map-viewer.ts#zoomOnLayerIdsMaybe() Infinity guard
  • packages/geoview-core/src/geo/utils/utilities.tsvalidateExtent() behavior, bufferExtent() candidate for reuse
  • packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.tsonGetBounds() where source extent is validated
  • packages/geoview-core/src/core/stores/states/layer-state.tssetStoreLayerBoundsForLayerAndParentsAndForget() 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions