Skip to content

Improve Linked Data Proxy interoperability for WFS 1.1.0 and MultiCur… - #110

Merged
karlbrink merged 8 commits into
masterfrom
fix_linkedDataProxy
Aug 21, 2026
Merged

Improve Linked Data Proxy interoperability for WFS 1.1.0 and MultiCur…#110
karlbrink merged 8 commits into
masterfrom
fix_linkedDataProxy

Conversation

@karlbrink

Copy link
Copy Markdown

…ve geometries

This change hardens the Linked Data Proxy and GML parsing pipeline to work reliably with stricter and non-uniform WFS implementations (especially QGIS/GeoServer-like WFS 1.1.0 services), while keeping behavior generic across services.

Key improvements:

Added robust geometry parsing in GML 3 factory:
namespace-tolerant/local-name based geometry detection
support for Curve and MultiCurve variants
support for embedded LineStringSegment paths in MultiCurve structures
improved fallback geometry discovery when geometry elements are nested or prefixed differently
Improved feature serialization:
output GeoJSON geometry as null (instead of empty string) when no geometry is available
keeps GeoJSON output standards-compliant and avoids downstream parsing issues
Fixed WFS request construction for strict servers:
version-specific request parameters:
WFS 2.x uses TYPENAMES + COUNT
WFS 1.1.0 uses TYPENAME + MAXFEATURES
normalized request key casing (SERVICE/REQUEST/VERSION)
removed duplicate max-features parameter in paging GET requests
Added request-version fallback strategy:
detect service version and retry with compatible versions when needed
improved handling of WFS exception responses before deciding fallback
Improved proxy paging/filter compatibility:
support STARTINDEX as alias parameter
convert FES filter namespace/elements to OGC equivalents for non-2.x services
Reworked GeoJSON bbox handling:
generic recursive bbox calculation for Point/Line/Polygon/Multi*/GeometryCollection
consistent feature-collection bbox accumulation
reduced reliance on brittle geometry-type-specific bbox code
Hardened cached count behavior:
detect implausible cached counts
trigger recount and refresh cache when cached values are stale/invalid
Prevented accidental feature loss in conversion paths:
do not drop features simply because geometry is null
guard geometry-dependent logic (bbox/type extraction) accordingly
Result:
The proxy now handles a broader range of real-world WFS services more reliably, including WFS 1.1.0 endpoints and MultiCurve-based datasets, with improved stability for paging, filtering, geometry conversion, and bbox generation.

…ve geometries

This change hardens the Linked Data Proxy and GML parsing pipeline to work reliably
with stricter and non-uniform WFS implementations (especially QGIS/GeoServer-like
WFS 1.1.0 services), while keeping behavior generic across services.

Key improvements:

Added robust geometry parsing in GML 3 factory:
  namespace-tolerant/local-name based geometry detection
  support for Curve and MultiCurve variants
  support for embedded LineStringSegment paths in MultiCurve structures
  improved fallback geometry discovery when geometry elements are nested or prefixed differently
Improved feature serialization:
  output GeoJSON geometry as null (instead of empty string) when no geometry is available
  keeps GeoJSON output standards-compliant and avoids downstream parsing issues
Fixed WFS request construction for strict servers:
  version-specific request parameters:
    WFS 2.x uses TYPENAMES + COUNT
    WFS 1.1.0 uses TYPENAME + MAXFEATURES
  normalized request key casing (SERVICE/REQUEST/VERSION)
  removed duplicate max-features parameter in paging GET requests
Added request-version fallback strategy:
  detect service version and retry with compatible versions when needed
  improved handling of WFS exception responses before deciding fallback
Improved proxy paging/filter compatibility:
  support STARTINDEX as alias parameter
  convert FES filter namespace/elements to OGC equivalents for non-2.x services
Reworked GeoJSON bbox handling:
  generic recursive bbox calculation for Point/Line/Polygon/Multi*/GeometryCollection
  consistent feature-collection bbox accumulation
  reduced reliance on brittle geometry-type-specific bbox code
Hardened cached count behavior:
  detect implausible cached counts
  trigger recount and refresh cache when cached values are stale/invalid
Prevented accidental feature loss in conversion paths:
  do not drop features simply because geometry is null
  guard geometry-dependent logic (bbox/type extraction) accordingly
Result:
The proxy now handles a broader range of real-world WFS services more reliably,
including WFS 1.1.0 endpoints and MultiCurve-based datasets, with improved stability
for paging, filtering, geometry conversion, and bbox generation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request hardens the Linked Data Proxy’s WFS request/response handling and the GML→GeoJSON pipeline to interoperate better with stricter/non-uniform WFS implementations (notably WFS 1.1.0) and broader GML geometry variants (Curve/MultiCurve), while improving bbox computation and GeoJSON geometry null-handling.

Changes:

  • Adds namespace-tolerant geometry discovery/parsing in the GML 3 factory, including Curve/MultiCurve variants and nested geometry fallback.
  • Reworks bbox calculation to be recursive/generic across GeoJSON geometry types and introduces WFS-version fallback chains for count/getFeature calls.
  • Normalizes WFS KVP request parameter casing and corrects version-specific KVP parameters (TYPENAME(S), MAXFEATURES/COUNT), plus outputs GeoJSON geometry: null when geometry is missing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
http/php/mod_linkedDataProxy.php Adds WFS version fallback logic, filter compatibility conversions, and new generic bbox computation utilities used during proxy responses.
http/classes/class_wfs.php Normalizes/adjusts WFS KVP construction across versions (parameter names/casing; removes duplicate max-features param).
http/classes/class_gml_feature.php Makes GeoJSON serialization standards-compliant by emitting geometry: null instead of an empty string.
http/classes/class_gml_3_factory.php Improves geometry detection/parsing to be prefix/namespace-tolerant and supports Curve/MultiCurve structures.
Comments suppressed due to low confidence (1)

http/php/mod_linkedDataProxy.php:2539

  • In the Mapbender-GML->GeoJSON path, features with geometry: null are still added to $geojsonList->features, but no corresponding $geojsonBbox[$geojsonIndex] entry is created because bbox calculation is skipped unless geometry is an object with a type. Downstream code (e.g., HTML zoom links) assumes $geojsonBbox[$i] exists for each feature, so this can trigger undefined-index/property errors.
									if (is_object($featureGeoJson->geometry) && isset($featureGeoJson->geometry->type)) {
										$featureBbox = calculateBboxFromGeojsonGeometry($featureGeoJson->geometry);
										if ($featureBbox !== false) {
											$geojsonBbox [$geojsonIndex]->minx = $featureBbox->minx;
											$geojsonBbox [$geojsonIndex]->miny = $featureBbox->miny;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread http/php/mod_linkedDataProxy.php Outdated
Comment thread http/php/mod_linkedDataProxy.php Outdated
Comment on lines 2376 to 2380
foreach ( $geojsonList->features as $feature ) {
$minxF = 90;
$minyF = 180;
$maxxF = - 90;
$maxyF = - 180;
switch ($feature->geometry->type) {
case "Polygon" :
foreach ( $feature->geometry->coordinates [0] as $lonLat ) {
$lon = $lonLat [0];
$lat = $lonLat [1];
if ($minxF > $lat) {
$minxF = $lat;
}
if ($minyF > $lon) {
$minyF = $lon;
}
if ($maxxF < $lat) {
$maxxF = $lat;
}
if ($maxyF < $lon) {
$maxyF = $lon;
}
}
break;
case "Point" :
$lon = $feature->geometry->coordinates [0];
$lat = $feature->geometry->coordinates [1];
if ($minxF > $lat) {
$minxF = $lat;
}
if ($minyF > $lon) {
$minyF = $lon;
}
if ($maxxF < $lat) {
$maxxF = $lat;
}
if ($maxyF < $lon) {
$maxyF = $lon;
}
break;
case "LineString" :
foreach ( $feature->geometry->coordinates as $lonLat ) {
$lon = $lonLat [0];
$lat = $lonLat [1];
if ($minxF > $lat) {
$minxF = $lat;
}
if ($minyF > $lon) {
$minyF = $lon;
}
if ($maxxF < $lat) {
$maxxF = $lat;
}
if ($maxyF < $lon) {
$maxyF = $lon;
}
}
break;
$featureBbox = calculateBboxFromGeojsonGeometry($feature->geometry);
if ($featureBbox === false) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in commit 5a73017. The bbox array now stays index-aligned with features even when geometry is null/invalid, and HTML zoom links are only rendered when a valid bbox exists.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

Code Review by Gemini

Bug 1: Swapped X/Y (Longitude/Latitude) Coordinates in GeoJSON BBOX Calculation

In http/php/mod_linkedDataProxy.php, calculateBboxFromGeojsonGeometry() maps the calculated latitude/longitude bounds to $bbox properties incorrectly:

	$bbox = new stdClass();
	$bbox->minx = $minLat;
	$bbox->miny = $minLon;
	$bbox->maxx = $maxLat;
	$bbox->maxy = $maxLon;

minx (minimum X / Longitude) is assigned $minLat, while miny (minimum Y / Latitude) is assigned $minLon. This swaps X and Y coordinates in all calculated bounding boxes and breaks Leaflet map extent calculations (e.g. zoomToExtent).

Additionally, calculateBboxFromGeojsonFcObject() initializes $minxFC to 90 and $minyFC to 180. Since longitude ranges from -180 to 180 and latitude from -90 to 90, these bounds are inverted (e.g., longitudes between 90° and 180° fail lower-bound comparisons).

Suggested Fix:

In http/php/mod_linkedDataProxy.php:

function calculateBboxFromGeojsonGeometry($geometry) {
	if (!is_object($geometry) || !isset($geometry->type)) {
		return false;
	}
	$minLat = 90;
	$minLon = 180;
	$maxLat = -90;
	$maxLon = -180;

	if ($geometry->type === "GeometryCollection" && isset($geometry->geometries) && is_array($geometry->geometries)) {
		foreach ($geometry->geometries as $geometryPart) {
			$bboxPart = calculateBboxFromGeojsonGeometry($geometryPart);
			if ($bboxPart !== false) {
				if ($minLon > $bboxPart->minx) { $minLon = $bboxPart->minx; }
				if ($minLat > $bboxPart->miny) { $minLat = $bboxPart->miny; }
				if ($maxLon < $bboxPart->maxx) { $maxLon = $bboxPart->maxx; }
				if ($maxLat < $bboxPart->maxy) { $maxLat = $bboxPart->maxy; }
			}
		}
	}
	else if (isset($geometry->coordinates)) {
		$coords = json_decode(json_encode($geometry->coordinates), true);
		updateBboxAccumulatorFromCoordinates($coords, $minLat, $minLon, $maxLat, $maxLon);
	}

	if ($minLat > $maxLat || $minLon > $maxLon) {
		return false;
	}

	$bbox = new stdClass();
	$bbox->minx = $minLon;
	$bbox->miny = $minLat;
	$bbox->maxx = $maxLon;
	$bbox->maxy = $maxLat;
	return $bbox;
}

function calculateBboxFromGeojsonFcObject($geojsonList) {    
	$minxFC = 180;
	$minyFC = 90;
	$maxxFC = -180;
	$maxyFC = -90;
    foreach ( $geojsonList->features as $feature ) {
		if (isset($feature->geometry)) {
			$bboxFeature = calculateBboxFromGeojsonGeometry($feature->geometry);
			if ($bboxFeature !== false) {
				extendFeatureCollectionBbox($minxFC, $minyFC, $maxxFC, $maxyFC, $bboxFeature);
			}
		}
    }
	if ($minxFC > $maxxFC || $minyFC > $maxyFC) {
		return false;
	}
	return array($minxFC, $minyFC, $maxxFC, $maxyFC);
}

Bug 2: Unsafe explode(' ', ...) when parsing GML posList elements

In http/classes/class_gml_3_factory.php line 198 (and other GML coordinate parsing routines), coordinates are split using explode(' ', trim(...)):

$coordArray = explode(' ', trim($coordsDom->nodeValue));

GML responses frequently format posList coordinates across multiple lines (\n, \r) or separate values with multiple spaces/tabs. explode(' ', ...) leaves empty string elements in $coordArray, resulting in shifted/invalid coordinate pairs or PHP undefined index notices during coordinate iteration ($i += $dim).

Suggested Fix:

In http/classes/class_gml_3_factory.php:

$coordArray = preg_split('/\s+/', trim($coordsDom->nodeValue));

Bug 3: Global substring replacement for ValueReference during filter conversion

In http/php/mod_linkedDataProxy.php line 2205:

$filter = str_replace("ValueReference", "PropertyName", $filter);

Using str_replace on the raw XML filter string replaces any occurrence of the literal string "ValueReference", including within property values or text literal filter contents (e.g., <ogc:Literal>ValueReference</ogc:Literal>).

Suggested Fix:

In http/php/mod_linkedDataProxy.php:

$filter = str_replace('<fes:ValueReference', '<ogc:PropertyName', $filter);
$filter = str_replace('</fes:ValueReference>', '</ogc:PropertyName>', $filter);
$filter = str_replace('<ValueReference', '<PropertyName', $filter);
$filter = str_replace('</ValueReference>', '</PropertyName>', $filter);

…m to layer in mapviewer while loading geojson from linked open data

I changed it here directly so that it will be pushed all at once :)
@github-actions

Copy link
Copy Markdown

Code Review by Gemini

Here is the code review for the submitted pull request:


1. Critical Bug: Variable Name Typo in calculateBboxFromGeojsonFcObject

File: http/php/mod_linkedDataProxy.php (lines 332–334)

In calculateBboxFromGeojsonFcObject, line 332 assigns the calculated bounding box to $bboxFeature, but line 334 passes $featureBbox (which is undefined) to extendFeatureCollectionBbox:

// Lines 332-334
$bboxFeature = calculateBboxFromGeojsonGeometry($feature->geometry);
if ($bboxFeature !== false) {
    extendFeatureCollectionBbox($minxFC, $minyFC, $maxxFC, $maxyFC, $featureBbox);
}

Because $featureBbox is null, extendFeatureCollectionBbox() immediately exits doing nothing. As a result, $minxFC and $maxxFC are never updated, and calculateBboxFromGeojsonFcObject() will always return false.

Suggested Fix:

		if (isset($feature->geometry)) {
			$bboxFeature = calculateBboxFromGeojsonGeometry($feature->geometry);
			if ($bboxFeature !== false) {
				extendFeatureCollectionBbox($minxFC, $minyFC, $maxxFC, $maxyFC, $bboxFeature);
			}
		}

2. Fallback Filter Namespace Not Converted for WFS 1.1.0 Requests

File: http/php/mod_linkedDataProxy.php (line 2196)

The filter transformation from fes: (WFS 2.0) to ogc: (WFS 1.1.0) is performed once outside the request fallback loop based on $wfsDetectedVersion:

if ($filter != null && isset($wfsDetectedVersion) && strpos($wfsDetectedVersion, "2.0") !== 0) {
    $filter = str_replace('xmlns:fes="http://www.opengis.net/fes/2.0"', 'xmlns:ogc="http://www.opengis.net/ogc"', $filter);
    ...

If $wfsDetectedVersion is "2.0.0" and the 2.0.0 request fails, the fallback loop retries with $wfsRequestVersion = "1.1.0". However, $filter still contains fes:Filter tags. A WFS 1.1.0 server will reject the request due to the unsupported WFS 2.0 filter schema.

Suggested Fix:

Move or adjust the filter transformation logic so that it applies per-request version inside or before the retry loop:

function adaptFilterForWfsVersion($filter, $version) {
    if ($filter === null || strpos($version, "2.0") === 0) {
        return $filter;
    }
    $adapted = str_replace('xmlns:fes="http://www.opengis.net/fes/2.0"', 'xmlns:ogc="http://www.opengis.net/ogc"', $filter);
    $adapted = str_replace("<fes", "<ogc", $adapted);
    $adapted = str_replace("</fes", "</ogc", $adapted);
    return str_replace("ValueReference", "PropertyName", $adapted);
}

3. Unsafe count() Call on XPath Result in xpathGeometryNodes

File: http/classes/class_gml_3_factory.php (lines 140–146)

SimpleXMLElement::xpath() returns false on failure or when invalid XPath expressions are evaluated. Calling count($nodes) when $nodes is false will trigger a PHP warning (or a TypeError in PHP 8+):

private static function xpathGeometryNodes($simpleXMLNode, $prefixedPath, $localNamePath) {
    $nodes = $simpleXMLNode->xpath($prefixedPath);
    if (count($nodes) === 0) {
        $nodes = $simpleXMLNode->xpath($localNamePath);
    }
    return $nodes;
}

Suggested Fix:

	private static function xpathGeometryNodes($simpleXMLNode, $prefixedPath, $localNamePath) {
		if (!($simpleXMLNode instanceof SimpleXMLElement)) {
			return array();
		}
		$nodes = $simpleXMLNode->xpath($prefixedPath);
		if ($nodes === false || count($nodes) === 0) {
			$nodes = $simpleXMLNode->xpath($localNamePath);
		}
		return ($nodes === false) ? array() : $nodes;
	}

4. Incorrect Key Unsetting and Missing Variable Initialization in GML Feature Processing

File: http/php/mod_linkedDataProxy.php (lines 2530–2558 and lines 2755–2775)

  1. Unsetting wrong element during feature loop (lines 2530–2558):

    foreach ( $gml3Object->featureCollection->featureArray as $mbFeature ) {
        ...
        unset ( $gml3Object->featureCollection->featureArray [$geojsonIndex] );
        $geojsonIndex ++;
    }

    If any feature is skipped via continue, $geojsonIndex will lag behind the actual array keys of featureArray, resulting in unsetting the wrong index. Use the $key from foreach ($array as $key => $value) instead.

  2. Undefined variable $geojsonIndex in Item View (lines 2755–2775):
    In the single item view branch for GML feature parsing, $geojsonIndex and $geojsonBbox are not initialized before entering the foreach loop, triggering "Undefined variable" notices on the first iteration.

Suggested Fix:

In lines 2530–2558:

foreach ( $gml3Object->featureCollection->featureArray as $key => $mbFeature ) {
    $featureGeoJson = json_decode($mbFeature->toGeoJSON());
    if (!is_object($featureGeoJson) || !property_exists($featureGeoJson, 'geometry')) {
        unset ( $gml3Object->featureCollection->featureArray [$key] );
        continue;
    }
    ...
    $geojsonList->features [] = $featureGeoJson;
    unset ( $gml3Object->featureCollection->featureArray [$key] );
    $geojsonIndex ++;
}

In lines 2755–2760:

    $geojsonBbox = array();
    $geojsonIndex = 0;
    $minxFC = 90;
    $minyFC = 180;
    $maxxFC = - 90;
    $maxyFC = - 180;

5. Version Argument Ignored in getFeatureGet

File: http/classes/class_wfs.php (lines 173–190)

The method signature defines $version="2.0.0", but inside getFeatureGet it uses $this->getVersion() instead of the passed $version argument:

protected function getFeatureGet ($featureTypeName, $filter, $maxFeatures=null, $version="2.0.0") {
    switch ($this->getVersion()) { // Should use $version
        ...

Suggested Fix:

	protected function getFeatureGet ($featureTypeName, $filter, $maxFeatures=null, $version="2.0.0") {
		switch ($version) {
			case "2.0.2":
				$typeNameParameterName = "typeNames";
				$typeNameRequestParameterName = "TYPENAMES";
				$maxFeaturesParameterName = "COUNT";
				break;
			case "2.0.0":
				$typeNameParameterName = "typeNames";
				$typeNameRequestParameterName = "TYPENAMES";
				$maxFeaturesParameterName = "COUNT";
				break;
			default:
				$typeNameParameterName = "typeName";
				$typeNameRequestParameterName = "TYPENAME";
				$maxFeaturesParameterName = "MAXFEATURES";
				break;
		}
		$url = $this->getFeature .
				$this->getConjunctionCharacter($this->getFeature) . 
				"SERVICE=WFS&REQUEST=GetFeature&VERSION=" . 
				$version . "&".$typeNameRequestParameterName."=" . $featureTypeName;

@github-actions

Copy link
Copy Markdown

Code Review by Gemini

Here is a code review of the pull request changes:


1. Fatal Error on Null/Missing Geometry Serialization

File: http/classes/class_gml_feature.php
Lines: 46–50

		if ($this->geometry) {
			$str .= "{\"type\":\"name\", \"properties\":{\"name\":\"" . $this->geometry->srs . "\"}}, ";
		}
		else {
			$str .= "{\"type\":\"name\", \"properties\":{\"name\":\"" . $this->geometry->srs . "\"}}, ";
		}

Issue

When a feature has no geometry ($this->geometry is null or false), execution enters the else branch. However, the else block attempts to read $this->geometry->srs, which will throw a fatal error in PHP 8+ (Uncaught Error: Attempt to read property "srs" on null / bool). This breaks feature serialization whenever features without geometries are present.

Suggested Fix

Do not attempt to access $this->geometry when it is null or false. For example:

		if ($this->geometry && !empty($this->geometry->srs)) {
			$str .= "{\"type\":\"name\", \"properties\":{\"name\":\"" . $this->geometry->srs . "\"}}, ";
		}
		else {
			$str .= "{\"type\":\"name\", \"properties\":{\"name\":\"EPSG:4326\"}}, ";
		}

2. Wrong Key Used for Unsetting Array Elements in Memory Cleanup

File: http/php/mod_linkedDataProxy.php
Lines: 2527–2562

foreach ( $gml3Object->featureCollection->featureArray as $mbFeature ) {
	...
	if (!is_object($featureGeoJson) || !property_exists($featureGeoJson, 'geometry')) {
		unset ( $gml3Object->featureCollection->featureArray [$geojsonIndex] );
		continue;
	}
	...
	unset ( $gml3Object->featureCollection->featureArray [$geojsonIndex] );
	$geojsonIndex ++;
}

Issue

Inside the foreach loop, $geojsonIndex is used as the array key to unset elements from $gml3Object->featureCollection->featureArray.
However, $geojsonIndex is the counter tracking the number of valid GeoJSON output features, not the actual key of $gml3Object->featureCollection->featureArray.

  • If an invalid feature is skipped via continue, $geojsonIndex is not incremented, causing subsequent unset() calls to target wrong array keys or fail to free memory.

Suggested Fix

Use the loop key $key to unset elements:

foreach ( $gml3Object->featureCollection->featureArray as $key => $mbFeature ) {
	$featureGeoJson = json_decode($mbFeature->toGeoJSON());
	if (!is_object($featureGeoJson) || !property_exists($featureGeoJson, 'geometry')) {
		unset ( $gml3Object->featureCollection->featureArray [$key] );
		continue;
	}
	...
	$geojsonList->features [] = $featureGeoJson;
	// free memory
	unset ( $gml3Object->featureCollection->featureArray [$key] );
	$geojsonIndex ++;
}

3. Performance Overhead with unnecessary json_encode / json_decode

File: http/php/mod_linkedDataProxy.php
Line: 316

	else if (isset($geometry->coordinates)) {
		$coords = json_decode(json_encode($geometry->coordinates), true);
		updateBboxAccumulatorFromCoordinates($coords, $minLat, $minLon, $maxLat, $maxLon);
	}

Issue

$geometry->coordinates (obtained via json_decode($features)) is already a native PHP array structure. Calling json_encode and json_decode on coordinate arrays for every feature geometry causes unnecessary string allocations and CPU overhead during request processing on large feature collections.

Suggested Fix

Pass $geometry->coordinates directly to updateBboxAccumulatorFromCoordinates:

	else if (isset($geometry->coordinates)) {
		updateBboxAccumulatorFromCoordinates($geometry->coordinates, $minLat, $minLon, $maxLat, $maxLon);
	}

4. Potential TypeError in PHP 8 when XPath expression returns false

File: http/classes/class_gml_3_factory.php
Lines: 140–146

	private static function xpathGeometryNodes($simpleXMLNode, $prefixedPath, $localNamePath) {
		$nodes = $simpleXMLNode->xpath($prefixedPath);
		if (count($nodes) === 0) {
			$nodes = $simpleXMLNode->xpath($localNamePath);
		}
		return $nodes;
	}

Issue

SimpleXMLElement::xpath() returns an array of nodes on success, or false on evaluation failure. In PHP 8.0+, calling count(false) throws a TypeError: count(): Argument #1 ($value) must be of type Countable|array, bool given.

Suggested Fix

Ensure $nodes is an array before passing it to count():

	private static function xpathGeometryNodes($simpleXMLNode, $prefixedPath, $localNamePath) {
		$nodes = $simpleXMLNode->xpath($prefixedPath);
		if ($nodes === false || count($nodes) === 0) {
			$nodes = $simpleXMLNode->xpath($localNamePath);
		}
		return is_array($nodes) ? $nodes : array();
	}

@karlbrink
karlbrink merged commit c17474a into master Aug 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants