4dd/m2kml

Metro2 KML to GeoJSON converter

Maintainers

Package info

gitlab.com/4dd/m2kml

Issues

Type:project

pkg:composer/4dd/m2kml

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

1.1.0 2026-04-11 23:50 UTC

This package is auto-updated.

Last update: 2026-04-11 19:51:55 UTC


README

A PHP CLI tool that converts metro2.org subway map data from Google Earth Pro KML format to GeoJSON.

The Problem It Solves

Metro2 map data is authored in Google Earth Pro and exported as KML files. To publish these on a web map, the KML must go through a series of transformations: normalize the KML for re-editing, apply underground depth values to coordinates, resolve style references, extract display levels, and finally convert to GeoJSON.

What m2kml does

m2kml replaces the entire convert portion of the pipeline with one in-memory pass:

m2kml m2spbm-source.kml m2spbm.geojson

No intermediate files. No Node.js dependency. The KML is parsed once into a PHP object graph, all transformations run in memory, and the result is written directly as GeoJSON.

The normalize command (which prepares KML for continued editing in Google Earth Pro) is kept as a separate step since it produces a KML file the author needs to keep:

m2kml normalize m2spbm-source.kml [m2spbm.kml]

Installation

Requires PHP 8.4+ and Composer.

Global install (recommended):

composer global require 4dd/m2kml

Make sure ~/.config/composer/vendor/bin (or ~/.composer/vendor/bin) is in your PATH.

Local install:

composer require 4dd/m2kml
./vendor/bin/m2kml input.kml output.geojson

Usage

Convert KML to GeoJSON:

m2kml input.kml output.geojson

Normalize KML for re-editing in Google Earth Pro:

m2kml normalize input.kml           # overwrites input.kml
m2kml normalize input.kml out.kml   # writes to a new file

How the Conversion Works

Pipeline overview

KML file
   │
   ▼  KmlParser
KmlDocument (PHP object graph)
   │  styles, styleMaps, folder tree with placemarks
   │
   ▼  StyleIndex::build()
StyleIndex (styleUrl → GeoJSON style properties)
   │
   ▼  GeoJsonBuilder::build()
GeoJSON FeatureCollection
   │  single recursive tree walk, independent transformers per placemark
   │
   ▼  json_encode()
output.geojson

1. KML Parser

Loads the KML file into a SimpleXML tree and extracts:

  • StylesLineStyle (color + width) and PolyStyle (color). IconStyle is ignored — GeoJSON has no icon concept.
  • StyleMaps — only the normal pair; the highlight pair is ignored.
  • Folder tree — recursively parsed into KmlFolder objects with nested KmlFolder and KmlPlacemark children.
  • Placemarks — name, description, styleUrl, geometry (LineString or Polygon), and ExtendedData key-value pairs.
  • Coordinates — parsed from KML's comma-separated lng,lat,alt tuples into PHP float arrays, rounded to 8 decimal places at parse time.

2. Style Index

Built once from the parsed document before the tree walk begins.

KML stores colors in ABGR hex format — the reverse of CSS RGB, with an alpha prefix:

KML:  ff 16 16 16
      ^^ ^^ ^^ ^^
      │  │  │  └── red   → CSS first pair
      │  │  └───── green → CSS second pair
      │  └──────── blue  → CSS third pair
      └─────────── alpha: 0x00=transparent, 0xff=opaque → opacity = value/255

For each Style, the index pre-computes the GeoJSON properties: stroke, stroke-opacity, stroke-width, fill, fill-opacity.

StyleMaps are resolved at index-build time: each StyleMap entry points to its normal Style, so StyleIndex::resolve(styleUrl) returns the final properties in one lookup regardless of whether the URL references a Style or a StyleMap.

3. GeoJSON Builder

A single recursive walk over the folder tree. For each placemark:

Description parsing — The <description> element in metro2 KML files contains YAML-formatted metadata, for example:

status: 'planed'
color: '#ef4136'
depth: -63

Parsed with symfony/yaml. Scalars remain strings; YAML lists become PHP arrays.

Metadata propagation — A folder's description metadata propagates to all child folders and placemarks. Any property declared in a parent folder is inherited by its children; a child's own declaration of the same key takes precedence. This applies to all metadata keys, including depth and depth_map. depth and depth_map are mutually exclusive — whichever is declared at the closer scope wins and clears the other.

Depth application — Underground coordinates. Two modes:

  • Simple depth (depth: -63): the Z component of every coordinate is set to that value.
  • Depth map (depth_map: '0:-52, 5:-57, 80:-57, 95:-63, 100:-63'): position percentages (0–100) mapped to depth values. The tool calculates cumulative Haversine distances along the line geometry, determines each point's position as a percentage of total length, and linearly interpolates the depth between the declared pivot points. This allows a tunnel to gradually descend or ascend along its route.

The depth is applied directly to the already-parsed float[][] coordinate arrays before they are written to GeoJSON. Altitude values are rounded to 2 decimal places.

Name transform — Placemark names follow a d{N}description convention where N (0–4) is a display mode. For example, d1sw0-pocket-track--karetnaya has display mode 1. The digit is extracted and written as the display property. The name itself is never included in GeoJSON output.

Property assembly — For each placemark, properties are merged in this order:

  1. Style properties from StyleIndex (stroke, stroke-opacity, stroke-width, fill, fill-opacity)
  2. display mode (from name)
  3. Inherited metadata context (all ancestor folder props merged, placemark's own props win) — depth and depth_map are excluded from output (consumed by depth application)
  4. Pre-existing ExtendedData from the KML (for files that were already processed)
  5. parents — the folder ancestry stack as [{"name": "service-lines"}, {"name": "06"}]

styleUrl and top-level root folder of parents stack are never added to properties.

4. Output

json_encode($collection, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)

Unicode characters and slashes are written as-is (not escaped), which keeps Cyrillic station names readable in the file.

What the Normalize Command Does

Google Earth Pro shows description text as a preview tooltip on each item in the tree panel. On large files this makes the tree very slow to navigate. The normalize command adds <Snippet maxLines="0"/> to every Placemark, Folder, and Document element, which hides the preview. This is the only operation that needs to touch the KML as XML.

The conversion pipeline never needs this — it reads the raw KML file directly and ignores the Snippet element.