Skip to content

mortie.geometry

Lazy WKB/WKT geometry codec. The geometry backend (shapely>=2 preferred, spherely accepted) is imported on first use, so numpy stays the only runtime dependency.

Lazy WKB/WKT geometry codec for mortie (issue #71).

The runtime stays numpy-only: this module imports a geometry backend (shapely>=2 preferred, spherely accepted) lazily and uses it only as a codec — bytes/text ↔ ring coordinate arrays. All spherical correctness (antimeridian / pole handling) stays mortie's own job; the backend is never asked for spatial predicates. Importing :mod:mortie succeeds with neither backend installed; the geometry functions raise a clear :class:ImportError when first touched without one (the same lazy-gate pattern :mod:mortie.arrow uses for pyarrow).

Coordinate convention: WKB/WKT store (x, y) = (lon, lat) degrees (EPSG:4326). mortie's coverage entry points take (lats, lons), so this module flips the axes at the boundary and works in degrees throughout.

from_wkb(data, order=18, moc=False, normalize=True, tolerance=None, max_cells=None)

Cover a geometry given as WKB (or EWKB) bytes.

Thin wrapper: decode with :func:geometry_from_wkb, then :func:from_geometry.

Parameters:

Name Type Description Default
data bytes

WKB or EWKB bytes.

required
order optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
moc optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
normalize optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
tolerance optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
max_cells optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18

Returns:

Type Description
numpy.ndarray or list of numpy.ndarray

As :func:from_geometry.

Raises:

Type Description
ValueError

As :func:from_geometry — including moc / tolerance / max_cells passed for linear geometry.

See Also

from_geometry : The shared parameter semantics and the full contract.

Source code in mortie/geometry.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
def from_wkb(data, order=18, moc=False, normalize=True,
             tolerance=None, max_cells=None):
    """Cover a geometry given as WKB (or EWKB) bytes.

    Thin wrapper: decode with :func:`geometry_from_wkb`, then
    :func:`from_geometry`.

    Parameters
    ----------
    data : bytes
        WKB or EWKB bytes.
    order, moc, normalize, tolerance, max_cells : optional
        Forwarded to :func:`from_geometry` unchanged.  See there for the full
        contract — in particular that ``morton_coverage_moc`` has no
        orientation auto-correct, so with ``moc=True`` the ring winding is
        taken **as authored**.

    Returns
    -------
    numpy.ndarray or list of numpy.ndarray
        As :func:`from_geometry`.

    Raises
    ------
    ValueError
        As :func:`from_geometry` — including ``moc`` / ``tolerance`` /
        ``max_cells`` passed for linear geometry.

    See Also
    --------
    from_geometry : The shared parameter semantics and the full contract.
    """
    return from_geometry(
        geometry_from_wkb(data), order=order, moc=moc, normalize=normalize,
        tolerance=tolerance, max_cells=max_cells,
    )

from_wkt(text, order=18, moc=False, normalize=True, tolerance=None, max_cells=None)

Cover a geometry given as WKT (or EWKT) text.

Thin wrapper: decode with :func:geometry_from_wkt, then :func:from_geometry.

Parameters:

Name Type Description Default
text str

WKT or EWKT text.

required
order optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
moc optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
normalize optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
tolerance optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18
max_cells optional

Forwarded to :func:from_geometry unchanged. See there for the full contract — in particular that morton_coverage_moc has no orientation auto-correct, so with moc=True the ring winding is taken as authored.

18

Returns:

Type Description
numpy.ndarray or list of numpy.ndarray

As :func:from_geometry.

Raises:

Type Description
ValueError

As :func:from_geometry — including moc / tolerance / max_cells passed for linear geometry.

See Also

from_geometry : The shared parameter semantics and the full contract.

Source code in mortie/geometry.py
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
def from_wkt(text, order=18, moc=False, normalize=True,
             tolerance=None, max_cells=None):
    """Cover a geometry given as WKT (or EWKT) text.

    Thin wrapper: decode with :func:`geometry_from_wkt`, then
    :func:`from_geometry`.

    Parameters
    ----------
    text : str
        WKT or EWKT text.
    order, moc, normalize, tolerance, max_cells : optional
        Forwarded to :func:`from_geometry` unchanged.  See there for the full
        contract — in particular that ``morton_coverage_moc`` has no
        orientation auto-correct, so with ``moc=True`` the ring winding is
        taken **as authored**.

    Returns
    -------
    numpy.ndarray or list of numpy.ndarray
        As :func:`from_geometry`.

    Raises
    ------
    ValueError
        As :func:`from_geometry` — including ``moc`` / ``tolerance`` /
        ``max_cells`` passed for linear geometry.

    See Also
    --------
    from_geometry : The shared parameter semantics and the full contract.
    """
    return from_geometry(
        geometry_from_wkt(text), order=order, moc=moc, normalize=normalize,
        tolerance=tolerance, max_cells=max_cells,
    )

from_geometry(geom, order=18, moc=False, normalize=True, tolerance=None, max_cells=None)

Cover a backend geometry with morton indices (issue #71).

The geometry is decomposed via :func:decompose and routed to mortie's existing coverage entry points — so WKB/WKT ingest produces exactly the same cover as calling those functions on the same (lats, lons) arrays.

  • Polygon / MultiPolygon → :func:mortie.morton_coverage (flat) or, with moc=True, :func:mortie.morton_coverage_moc (compact mixed-order). Holes and disjoint parts are handled by the one even-odd descent.
  • LineString / MultiLineString → :func:mortie.linestring_coverage.

Parameters:

Name Type Description Default
geom backend geometry

A shapely/spherely geometry object (e.g. from :func:geometry_from_wkb).

required
order int

HEALPix order (1–29). Default 18.

18
moc bool

Polygonal only: return a compact MOC instead of a flat cover.

False
normalize bool

Polygonal: auto-correct ring orientation at ingest, on both the flat and the moc=True path (see :func:mortie.morton_coverage). Default True: any simple ring whose interior decisively reads as the larger region is reversed so the smaller side is covered (S2's convention; issue #144 decision (A)), hemisphere-plus rings included. Pass False to take the winding as authored — the only way a WKB/WKT ring can express a bigger-than-complement interior (wind every ring, holes included, with its intended region on the left). normalize=False with linear geometry raises ValueError (a line has no ring orientation).

True
tolerance optional

Polygonal moc=True only: the adaptive stop criteria of :func:mortie.morton_coverage_moc (mutually exclusive).

None
max_cells optional

Polygonal moc=True only: the adaptive stop criteria of :func:mortie.morton_coverage_moc (mutually exclusive).

None

Returns:

Type Description
numpy.ndarray or list of numpy.ndarray

Polygonal → 1-D uint64 morton array. LineString → 1-D array; MultiLineString → list of arrays, one per line (the :func:mortie.linestring_coverage contract).

Raises:

Type Description
ValueError

If moc / tolerance / max_cells are passed for linear geometry (they apply only to polygonal geometry), or from :func:decompose for an unsupported or empty geometry.

Source code in mortie/geometry.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
def from_geometry(geom, order=18, moc=False, normalize=True,
                  tolerance=None, max_cells=None):
    """Cover a backend geometry with morton indices (issue #71).

    The geometry is decomposed via :func:`decompose` and routed to mortie's
    existing coverage entry points — so WKB/WKT ingest produces exactly the same
    cover as calling those functions on the same ``(lats, lons)`` arrays.

    * **Polygon / MultiPolygon** → :func:`mortie.morton_coverage` (flat) or, with
      ``moc=True``, :func:`mortie.morton_coverage_moc` (compact mixed-order).
      Holes and disjoint parts are handled by the one even-odd descent.
    * **LineString / MultiLineString** → :func:`mortie.linestring_coverage`.

    Parameters
    ----------
    geom : backend geometry
        A shapely/spherely geometry object (e.g. from :func:`geometry_from_wkb`).
    order : int, optional
        HEALPix order (1–29).  Default 18.
    moc : bool, optional
        Polygonal only: return a compact MOC instead of a flat cover.
    normalize : bool, optional
        Polygonal: auto-correct ring orientation at ingest, on both the
        flat and the ``moc=True`` path (see :func:`mortie.morton_coverage`).
        Default ``True``: any simple ring whose interior decisively reads as
        the larger region is reversed so the smaller side is covered (S2's
        convention; issue #144 decision (A)), hemisphere-plus rings included.
        Pass ``False`` to take the winding **as authored** — the only way a
        WKB/WKT ring can express a bigger-than-complement interior (wind
        every ring, holes included, with its intended region on the left).
        ``normalize=False`` with linear geometry raises ``ValueError`` (a
        line has no ring orientation).
    tolerance, max_cells : optional
        Polygonal ``moc=True`` only: the adaptive stop criteria of
        :func:`mortie.morton_coverage_moc` (mutually exclusive).

    Returns
    -------
    numpy.ndarray or list of numpy.ndarray
        Polygonal → 1-D ``uint64`` morton array.  LineString → 1-D array;
        MultiLineString → list of arrays, one per line (the
        :func:`mortie.linestring_coverage` contract).

    Raises
    ------
    ValueError
        If ``moc`` / ``tolerance`` / ``max_cells`` are passed for linear
        geometry (they apply only to polygonal geometry), or from
        :func:`decompose` for an unsupported or empty geometry.
    """
    from .coverage import morton_coverage, morton_coverage_moc
    from .linestring import linestring_coverage

    kind, parts = decompose(geom)

    if kind == "polygonal":
        lats = [p[0] for p in parts]
        lons = [p[1] for p in parts]
        if moc:
            return morton_coverage_moc(
                lats, lons, order=order, tolerance=tolerance,
                max_cells=max_cells, normalize=normalize,
            )
        return morton_coverage(lats, lons, order=order, normalize=normalize)

    # linear
    if moc or tolerance is not None or max_cells is not None:
        raise ValueError(
            "moc / tolerance / max_cells apply only to polygonal geometry"
        )
    if not normalize:
        raise ValueError("normalize applies only to polygonal geometry")
    if len(parts) == 1:
        return linestring_coverage(parts[0][0], parts[0][1], order=order)
    lats = [p[0] for p in parts]
    lons = [p[1] for p in parts]
    return linestring_coverage(lats, lons, order=order)

to_wkb(morton, dissolve=True, step=1, srid=None)

Emit a morton cover as WKB (or EWKB) bytes.

Parameters:

Name Type Description Default
morton array_like of uint64

A morton cover (flat or mixed-order MOC).

required
dissolve optional

Forwarded to :func:to_geometry unchanged; see there for the full contract (pole caps, antimeridian splitting, edge densification).

True
step optional

Forwarded to :func:to_geometry unchanged; see there for the full contract (pole caps, antimeridian splitting, edge densification).

True
srid int

With srid set (e.g. 4326), emit EWKB carrying that SRID; otherwise plain WKB.

None

Returns:

Type Description
bytes

The encoded WKB (or EWKB) bytes.

Raises:

Type Description
NotImplementedError

As :func:to_geometry — a non-shapely backend, or a dissolved hole that nests into no exterior.

See Also

to_geometry : The dissolve / step contract in full.

Source code in mortie/geometry.py
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
def to_wkb(morton, dissolve=True, step=1, srid=None):
    """Emit a morton cover as WKB (or EWKB) bytes.

    Parameters
    ----------
    morton : array_like of uint64
        A morton cover (flat or mixed-order MOC).
    dissolve, step : optional
        Forwarded to :func:`to_geometry` unchanged; see there for the full
        contract (pole caps, antimeridian splitting, edge densification).
    srid : int, optional
        With ``srid`` set (e.g. ``4326``), emit EWKB carrying that SRID;
        otherwise plain WKB.

    Returns
    -------
    bytes
        The encoded WKB (or EWKB) bytes.

    Raises
    ------
    NotImplementedError
        As :func:`to_geometry` — a non-shapely backend, or a dissolved hole
        that nests into no exterior.

    See Also
    --------
    to_geometry : The ``dissolve`` / ``step`` contract in full.
    """
    return geometry_to_wkb(to_geometry(morton, dissolve=dissolve, step=step), srid=srid)

to_wkt(morton, dissolve=True, step=1, srid=None)

Emit a morton cover as WKT (or EWKT) text.

Parameters:

Name Type Description Default
morton array_like of uint64

A morton cover (flat or mixed-order MOC).

required
dissolve optional

Forwarded to :func:to_geometry unchanged; see there for the full contract (pole caps, antimeridian splitting, edge densification).

True
step optional

Forwarded to :func:to_geometry unchanged; see there for the full contract (pole caps, antimeridian splitting, edge densification).

True
srid int

With srid set, emit EWKT (SRID=<n>;<WKT>); otherwise plain WKT.

None

Returns:

Type Description
str

The encoded WKT (or EWKT) text.

Raises:

Type Description
NotImplementedError

As :func:to_geometry — a non-shapely backend, or a dissolved hole that nests into no exterior.

See Also

to_geometry : The dissolve / step contract in full.

Source code in mortie/geometry.py
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
def to_wkt(morton, dissolve=True, step=1, srid=None):
    """Emit a morton cover as WKT (or EWKT) text.

    Parameters
    ----------
    morton : array_like of uint64
        A morton cover (flat or mixed-order MOC).
    dissolve, step : optional
        Forwarded to :func:`to_geometry` unchanged; see there for the full
        contract (pole caps, antimeridian splitting, edge densification).
    srid : int, optional
        With ``srid`` set, emit EWKT (``SRID=<n>;<WKT>``); otherwise plain WKT.

    Returns
    -------
    str
        The encoded WKT (or EWKT) text.

    Raises
    ------
    NotImplementedError
        As :func:`to_geometry` — a non-shapely backend, or a dissolved hole
        that nests into no exterior.

    See Also
    --------
    to_geometry : The ``dissolve`` / ``step`` contract in full.
    """
    return geometry_to_wkt(to_geometry(morton, dissolve=dissolve, step=step), srid=srid)

to_geometry(morton, dissolve=True, step=1)

Convert a morton cover to a backend geometry (issue #71).

Parameters:

Name Type Description Default
morton array_like of uint64

A morton cover (flat or mixed-order MOC; each word self-encodes order).

required
dissolve bool

True (default) emits the single dissolved outline of the whole cover (exterior rings, holes, and disjoint components), built natively by edge-cancellation — no backend spatial predicate. False emits a per-cell MultiPolygon — one quad per cell.

True
step int

Boundary points per cell edge (default 1 = 4 corners / straight chords). step>1 densifies each edge to follow the curved HEALPix boundary.

1

Returns:

Type Description
backend geometry

A shapely (or spherely) MultiPolygon in EPSG:4326 lon/lat degrees.

Raises:

Type Description
NotImplementedError

If the active backend is not shapely, or if a dissolved hole nests into no exterior (pass dissolve=False).

Notes

Emit requires the shapely backend (it constructs geometry objects). The dissolved emit (dissolve=True) handles pole-enclosing covers (e.g. polar caps), exteriors crossing the antimeridian any even number of times, and antimeridian-crossing holes: crossing rings are cut at ±180° and reconnected by the GeoJSON convention — a single split MultiPolygon with explicit ±90° pole vertices stitched down the antimeridian. A cover spanning near or over a hemisphere (2π sr), or one with a boundary ring enclosing more than a hemisphere (e.g. an equatorial band), raises ValueError — its exterior/hole winding is ambiguous (issue #108); split such a cover or use dissolve=False.

Source code in mortie/geometry.py
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
def to_geometry(morton, dissolve=True, step=1):
    """Convert a morton cover to a backend geometry (issue #71).

    Parameters
    ----------
    morton : array_like of uint64
        A morton cover (flat or mixed-order MOC; each word self-encodes order).
    dissolve : bool, optional
        ``True`` (default) emits the single dissolved outline of the whole cover
        (exterior rings, holes, and disjoint components), built natively by
        edge-cancellation — no backend spatial predicate.  ``False`` emits a
        per-cell ``MultiPolygon`` — one quad per cell.
    step : int, optional
        Boundary points per cell edge (default 1 = 4 corners / straight chords).
        ``step>1`` densifies each edge to follow the curved HEALPix boundary.

    Returns
    -------
    backend geometry
        A shapely (or spherely) ``MultiPolygon`` in EPSG:4326 lon/lat degrees.

    Raises
    ------
    NotImplementedError
        If the active backend is not shapely, or if a dissolved hole nests
        into no exterior (pass ``dissolve=False``).

    Notes
    -----
    Emit requires the shapely backend (it constructs geometry objects).  The
    dissolved emit (``dissolve=True``) handles pole-enclosing covers (e.g. polar
    caps), exteriors crossing the antimeridian any even number of times, and
    antimeridian-crossing holes: crossing rings are cut at ±180° and reconnected
    by the GeoJSON convention — a single split ``MultiPolygon`` with explicit
    ±90° pole vertices stitched down the antimeridian.  A cover spanning near
    or over a hemisphere (2π sr), or one with a boundary ring enclosing more
    than a hemisphere (e.g. an equatorial band), raises ``ValueError`` — its
    exterior/hole winding is ambiguous (issue #108); split such a cover or use
    ``dissolve=False``.
    """
    mod = _require_shapely("geometry emit")
    if dissolve:
        return mod.MultiPolygon(_dissolved_polygons(mod, morton, step))
    return mod.MultiPolygon(_per_cell_polygons(mod, morton, step))