Skip to content

feat(parser): carry the Folder path on every placemark - #26

Merged
danielebarbaro merged 1 commit into
mainfrom
feat/folder-hierarchy
Sep 9, 2026
Merged

feat(parser): carry the Folder path on every placemark#26
danielebarbaro merged 1 commit into
mainfrom
feat/folder-hierarchy

Conversation

@danielebarbaro

Copy link
Copy Markdown
Contributor

Problem

$placemarksXml = $this->xml->xpath('//kml:Placemark');

// matches at any depth, which is what makes the parser find placemarks inside folders at all. It is also what makes the folders invisible: a document organised into <Folder> elements came back as one flat, undifferentiated list.

That structure is not decoration. It is how the author grouped the map, and in a Google My Maps export it is usually the layer names. Anyone who needed it had to parse the file a second time themselves, which defeats the point of the package.

The README listed Folder under "no", with "folders are flattened, the hierarchy is lost".

Fix

Every placemark carries a folder key:

[
    'name' => 'Lago Blu',
    'description' => 'A lake',
    'folder' => ['Piemonte', 'Laghi'],
    'type' => 'Point',
    'coordinates' => [...],
]

Outermost first, empty array for a placemark sitting directly under the Document.

Why a key rather than a tree

A getFolders() returning a nested structure with the placemarks inside was the obvious alternative. It was rejected because it means the same placemarks exist in two representations that have to be kept in step, and every caller then has to decide which one to read.

The flat list plus a path is smaller, has no duplication, and grouping stays a one-liner:

collect($parser->getPlacemarks())
    ->groupBy(fn (array $placemark) => implode('/', $placemark['folder']));

Unnamed folders

A <Folder> with no <name> contributes an empty string rather than being skipped. Skipping it would make ['Piemonte'] mean both "one level deep inside Piemonte" and "two levels deep, inside an unnamed folder inside Piemonte". The path length always matches the real nesting depth.

How it is resolved

Each placemark is asked for its own ancestors with ancestor::kml:Folder, rather than walking the tree a second time and re-associating the results. The kml prefix is registered on the placemark node itself, so this keeps working on the legacy earth.google.com namespaces, which is covered by a test rather than assumed.

GeoJSON

folder is carried into properties, but only for a placemark that is actually in one. Adding 'folder' => [] to every feature of every flat document would be noise in the payload, and it matches how styleUrl and extendedData already behave.

A flat document produces byte-identical GeoJSON to before. The existing exact-array test in tests/GeoJsonOutputTest.php still passes untouched, which is the evidence for that.

Tests

tests/FolderHierarchyTest.php, 9 tests against one document containing a loose placemark, a named folder, a folder nested inside it, an unnamed folder and a sibling folder:

  • a placemark outside every folder gets []
  • a placemark in one folder gets its name
  • nesting comes out outermost first
  • an unnamed folder keeps the depth
  • sibling folders do not bleed into each other
  • every placemark is still returned, still in document order
  • folders resolve on a legacy earth.google.com/kml/2.1 document
  • the path reaches the GeoJSON properties
  • the property is absent when there is no folder

Suite 79 to 88. PHPStan and Pint clean.

Behaviour change

getPlacemarks() entries gain a key. Additive, but a test comparing a whole placemark array with === or Pest's toBe() will notice. Worth a changelog line.

README updated: the Folder row moves to supported, the shapes gain folder, and the grouping snippet is documented.

Placemarks are collected with a flat //kml:Placemark query, so a
document organised into folders came back as one undifferentiated list
and the structure the author put there was lost. Anyone grouping a map
by folder had to parse the file a second time themselves.

Each placemark now carries a folder key holding the names of the Folder
elements containing it, outermost first, and an empty array when it sits
directly under the Document. The list stays flat, so nothing is
duplicated and grouping is a one-liner for the caller.

A Folder without a name contributes an empty string rather than being
skipped, so the length of the path always matches the real nesting
depth. Rather than walking the tree twice, each placemark is asked for
its own ancestors, which also keeps this working on the legacy
namespaces since the prefix is registered per node.

toGeoJson() carries the path into properties, but only for a placemark
that is actually in a folder, so output for flat documents is unchanged.
@danielebarbaro
danielebarbaro merged commit 1f057b6 into main Sep 9, 2026
27 checks passed
@danielebarbaro
danielebarbaro deleted the feat/folder-hierarchy branch September 9, 2026 06:53
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