mlt-py 0.2.6

Python bindings for MapLibre Tile (MLT) format via PyO3
Documentation
import json
import struct
from pathlib import Path

import pytest

import maplibre_tiles as mlt

SYNTHETIC = Path(__file__).parents[3] / "test" / "synthetic" / "0x02"
Z_FIXTURES = sorted(SYNTHETIC.glob("z_*.mlt"))
WKB_TYPES = {
    1: "Point",
    2: "LineString",
    3: "Polygon",
    4: "MultiPoint",
    5: "MultiLineString",
    6: "MultiPolygon",
}


def _read_wkb(data, offset=0):
    assert data[offset] == 1
    (code,) = struct.unpack_from("<I", data, offset + 1)
    dims = 3 if code > 1000 else 2
    offset += 5

    def count():
        nonlocal offset
        (n,) = struct.unpack_from("<I", data, offset)
        offset += 4
        return n

    def position():
        nonlocal offset
        pos = list(struct.unpack_from(f"<{dims}d", data, offset))
        offset += 8 * dims
        return pos

    def positions():
        return [position() for _ in range(count())]

    def parts():
        nonlocal offset
        out = []
        for _ in range(count()):
            part, offset = _read_wkb(data, offset)
            out.append(part["coordinates"])
        return out

    kind = WKB_TYPES[code % 1000]
    if kind == "Point":
        coords = position()
    elif kind == "LineString":
        coords = positions()
    elif kind == "Polygon":
        coords = [positions() for _ in range(count())]
    else:
        coords = parts()
    return {"type": kind, "coordinates": coords}, offset


def _wkb_geometry(wkb):
    geometry, end = _read_wkb(wkb)
    assert end == len(wkb)
    return geometry


def _fc(geometry):
    return {
        "type": "FeatureCollection",
        "features": [{"type": "Feature", "geometry": geometry, "properties": {}}],
    }


@pytest.mark.parametrize("fixture", Z_FIXTURES, ids=[f.stem for f in Z_FIXTURES])
def test_z_fixture_wkb_matches_its_geojson(fixture):
    expected = json.loads(fixture.with_suffix(".json").read_text())["features"]
    layers = mlt.decode_mlt(fixture.read_bytes())
    features = [feat for layer in layers for feat in layer.features]
    assert [_wkb_geometry(feat.wkb) for feat in features] == [
        feat["geometry"] for feat in expected
    ]
    assert [layer.z_step for layer in layers for _ in layer.features] == [
        feat["properties"]["_z_step"] for feat in expected
    ]


def test_a_flat_layer_has_no_z_step_and_2d_wkb():
    blob = mlt.encode_geojson(_fc({"type": "Point", "coordinates": [1, 2]}), "r")
    layer = mlt.decode_mlt(blob)[0]
    assert layer.z_step is None
    assert _wkb_geometry(layer.features[0].wkb)["coordinates"] == [1, 2]


def test_a_transformed_z_reads_as_metres():
    blob = mlt.encode_geojson(
        _fc({"type": "Point", "coordinates": [0, 0, 100_120]}), "r", z_step=-1
    )
    wkb = mlt.decode_mlt(blob, z=0, x=0, y=0, tms=False)[0].features[0].wkb
    assert _wkb_geometry(wkb)["coordinates"][2] == 12.0


@pytest.mark.parametrize(
    "geometry",
    [
        {"type": "Point", "coordinates": [1, 2, 3]},
        {"type": "LineString", "coordinates": [[0, 0, 7], [5, 5, -2]]},
        {
            "type": "Polygon",
            "coordinates": [[[0, 0, 1], [10, 0, 2], [10, 10, 3], [0, 0, 1]]],
        },
        {"type": "MultiPoint", "coordinates": [[1, 2, 3], [4, 5, 6]]},
    ],
    ids=["point", "line", "polygon", "multi_point"],
)
def test_z_roundtrips_through_encode_geojson(geometry):
    blob = mlt.encode_geojson(_fc(geometry), "r", z_step=0)
    layer = mlt.decode_mlt(blob)[0]
    assert layer.z_step == 0
    assert _wkb_geometry(layer.features[0].wkb) == geometry
    assert json.loads(mlt.decode_mlt_to_geojson(blob))["features"][0]["geometry"] == geometry


@pytest.mark.parametrize(
    ("geometry", "z_step", "message"),
    [
        ({"type": "Point", "coordinates": [1, 2]}, 0, r"'z_step' needs \[x, y, z\] positions"),
        ({"type": "Point", "coordinates": [1, 2, 3]}, None, r"\[x, y, z\] positions need a 'z_step'"),
        ({"type": "Point", "coordinates": [1, 2, 3]}, 5, r"a z step of 10\^5 m is outside 10\^-3..=10\^4 m"),
    ],
    ids=["z_step_without_z", "z_without_z_step", "z_step_out_of_range"],
)
def test_z_and_z_step_must_agree(geometry, z_step, message):
    with pytest.raises(ValueError, match=f"^{message}$"):
        mlt.encode_geojson(_fc(geometry), "r", z_step=z_step)