Skip to content

feat(parser): support MultiGeometry placemarks - #16

Merged
danielebarbaro merged 1 commit into
mainfrom
feat/multigeometry-support
Sep 8, 2026
Merged

feat(parser): support MultiGeometry placemarks#16
danielebarbaro merged 1 commit into
mainfrom
feat/multigeometry-support

Conversation

@danielebarbaro

Copy link
Copy Markdown
Contributor

Problem

GeometryType has declared MULTI_GEOMETRY = 'MultiGeometry' since the enum was introduced, and KmlValidator loops over GeometryType::cases(). So when a Placemark holds a <MultiGeometry>, the validator matches it and hands the element to validateGeometryCoordinates(), which does:

if (empty($geometry->coordinates)) {
    throw new KmlException('Empty coordinates in geometry');
}

A MultiGeometry has no coordinates child. It holds nested geometries. So every document containing a MultiGeometry is rejected at load time:

MultiGeometry: KmlException -> Empty coordinates in geometry

And even past validation, getPlacemarks() had no branch for it: it checks ->Point, ->LineString, ->Polygon and nothing else, so the placemark would have come back with a name and no geometry at all.

This is not an exotic corner of the spec. MultiGeometry is what Google My Maps and ogr2ogr emit for any feature made of more than one shape, which is a large share of the KML actually in circulation.

Fix

Validator walks the nested geometries and validates each one, recursing when a MultiGeometry contains another (KML permits that), and rejects one that is empty:

Found MultiGeometry without any geometry

Coordinate validation still applies to everything inside, so an out of range latitude nested three levels deep is still caught.

Parser returns the placemark as:

[
    'name' => 'Mixed',
    'description' => '...',
    'type' => 'MultiGeometry',
    'geometries' => [
        ['type' => 'Point', 'coordinates' => [...]],
        ['type' => 'LineString', 'coordinates' => [...]],
        ['type' => 'Polygon', 'coordinates' => ['outerBoundary' => [...], 'innerBoundaries' => [...]]],
    ],
]

geometries instead of coordinates, since there is no single coordinate set. Each entry has the same shape a standalone placemark of that type would have.

toGeoJson() maps it onto a GeoJSON GeometryCollection, which nests the same way a MultiGeometry does.

Refactor

Geometry parsing and GeoJSON emission were both open-coded if / elseif chains repeating the same coordinate flattening three times. Both now dispatch off GeometryType, so adding a geometry means one match arm in each rather than hunting through two methods. This is what makes the recursion possible without duplicating the whole chain.

Output parity

The refactor touches the code path of every existing geometry type, so parity was checked rather than assumed:

  1. Full toGeoJson() + getPlacemarks() + getStyles() output for tests/files/kml-example/base.kml, dumped from main and from this branch: byte identical (6628 bytes both). That sample only contains Points, so on its own it proves little.
  2. tests/GeoJsonOutputTest.php asserts the exact feature arrays for a LineString and for a Polygon with an inner ring, including styleUrl and extendedData in the properties and the outer-ring-first ordering. Those three tests were run against main's src/ as well and pass unchanged there.

So Point, LineString and Polygon output is the same before and after.

Tests

tests/MultiGeometryTest.php, all failing on main (the first four fail at load):

  • a document with a MultiGeometry loads at all
  • the three nested geometries are parsed with the right shapes and no coordinates key on the placemark
  • GeoJSON comes out as a GeometryCollection with the exact expected coordinates
  • a MultiGeometry nested inside another one round-trips
  • an empty MultiGeometry is rejected
  • an out-of-range latitude nested inside a MultiGeometry is still caught

tests/GeoJsonOutputTest.php adds the LineString and Polygon coverage described above, which the suite did not have at all before: no test touched either geometry type.

Behaviour change

If a Placemark somehow contained two sibling geometries (<Point> and <Polygon>), the old getPlacemarks() ran every if and the last one won, while the validator validated the first. The two disagreed. Both now take the first match, in GeometryType declaration order. Invalid KML either way, but worth noting it resolves differently.

Not in scope

LineStyle / PolyStyle, <Folder> nesting and <SimpleData> are all still unsupported. Separate PRs.

GeometryType already declared MULTI_GEOMETRY, but the validator fed the
element to validateGeometryCoordinates(), which looks for a coordinates
child a MultiGeometry does not have. Every document containing one was
rejected at load time with "Empty coordinates in geometry", and the
parser had no handling for it either. MultiGeometry is what Google My
Maps and ogr2ogr emit for a feature made of several shapes, so this
covers a large slice of real world files.

The validator now walks the nested geometries and validates each one,
recursing when a MultiGeometry contains another, and rejects one that is
empty. The parser returns the placemark as type MultiGeometry with a
geometries list instead of coordinates, and toGeoJson() maps it onto a
GeoJSON GeometryCollection, which nests the same way.

Geometry parsing and GeoJSON emission were both open coded if/elseif
chains; they are now dispatched off GeometryType so a new geometry is
added in one place. Output for Point, LineString and Polygon is
unchanged, byte for byte.
@danielebarbaro
danielebarbaro merged commit 2c37af3 into main Sep 8, 2026
19 checks passed
@danielebarbaro
danielebarbaro deleted the feat/multigeometry-support branch September 8, 2026 13:00
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.

1 participant