Geometry & dimensions¶
Geometry values are instances of Geometry: one of seven immutable,
hashable OGC Simple Features types, each
optionally carrying a CRS and per-vertex Z (elevation) and M (measure)
ordinates.
| Type | Made of | Constructor |
|---|---|---|
Point |
one coordinate | gm.Point |
LineString |
ordered vertices | gm.LineString |
Polygon |
a shell + holes | gm.Polygon / gm.box |
MultiPoint |
many points | gm.MultiPoint |
MultiLineString |
many lines | gm.MultiLineString |
MultiPolygon |
many polygons | gm.MultiPolygon |
GeometryCollection |
any mix | gm.GeometryCollection |
Always (x, y) — lon/lat only under a geographic CRS
Every constructor takes coordinates in (x, y) order. Under a
geographic CRS those axes are (longitude, latitude) in that CRS's
native angular units (such as degrees, radians, or grads). Projected CRSs
use their linear axes; CRS-free geometries use raw coordinates.
Pass crs=4326 for WGS 84 longitude/latitude in degrees.
Points¶
Construct a point with gm.Point(x, y); under a geographic CRS, (x, y) is
(longitude, latitude) in that CRS's native angular units.
import gometry as gm
paris = gm.Point(2.3522, 48.8566, crs=4326)
print(paris.to_wkt())
print("x:", paris.x, "y:", paris.y)
print("crs:", paris.crs, "coordinate_axes:", paris.coordinate_axes)
POINT (2.3522 48.8566)
x: 2.3522 y: 48.8566
crs: EPSG:4326 coordinate_axes: XY
.x / .y are for single points only
A Point exposes .x, .y (and .z / .m when present) for scalar
access. For any other geometry, pull every vertex out with
geom.coords (see
Inspecting geometries).
LineStrings¶
A LineString is an ordered sequence of vertices. Pass any sequence of
(x, y) pairs or an (N, 2) NumPy-style array — gometry copies the coordinates
once into its Rust geometry representation, so large inputs avoid per-point
Python object construction.
import numpy as np
import gometry as gm
coords = np.array([(0, 0), (1, 2), (3, 1), (4, 3)], dtype=float)
line = gm.LineString(coords)
print(line.to_wkt())
print("length:", line.length)
LINESTRING (0 0, 1 2, 3 1, 4 3)
length: 6.708203932499369
Polygons¶
A Polygon is a shell (exterior ring) plus zero or more holes
(interior rings). Rings are closed automatically — you do not need to repeat
the first vertex at the end, though it is harmless if you do.
import gometry as gm
square_with_hole = gm.Polygon(
[(0, 0), (4, 0), (4, 4), (0, 4)],
holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]],
)
print(square_with_hole.to_wkt())
print("holes:", len(square_with_hole.interiors))
print("area:", square_with_hole.area)
POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))
holes: 1
area: 15.0
Constructors check structure, not topology
gm.Polygon rejects structurally malformed input eagerly — non-finite
coordinates, or a ring with fewer than three vertices, raise a
InvalidGeometryError.
It does not run full OGC validity, so a self-intersecting "bowtie" is
built as an invalid geometry rather than rejected. Detect it with
geom.validate() and fix it with repair — see
Validation.
Boxes¶
For the common axis-aligned rectangle, gm.box is a shortcut
taking (minx, miny, maxx, maxy):
import gometry as gm
extent = gm.box(0, 0, 10, 5)
print(extent.to_wkt())
print("bounds:", extent.bounds)
POLYGON ((0 0, 10 0, 10 5, 0 5, 0 0))
bounds: (0.0, 0.0, 10.0, 5.0)
Multi-part geometries¶
Multi-geometries group several parts of the same type.
gm.MultiPoint accepts a sequence of coordinate pairs:
import numpy as np
import gometry as gm
cloud = gm.MultiPoint(np.random.default_rng(0).random((5, 2)).tolist())
print(cloud.geometry_type, "with", len(cloud), "points")
MultiPoint with 5 points
MultiLineString and MultiPolygon take sequences of part-coordinate
sequences and part geometries respectively:
import gometry as gm
mls = gm.MultiLineString([
[(0, 0), (1, 1)],
[(2, 0), (3, 1)],
])
mp = gm.MultiPolygon([
gm.box(0, 0, 1, 1),
gm.box(2, 2, 3, 3),
])
print(mls.to_wkt())
print(mp.to_wkt())
MULTILINESTRING ((0 0, 1 1), (2 0, 3 1))
MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))
Explicit empty members are retained in their original order and count. For
example, gm.MultiLineString([gm.LineString(), [(0, 0), (1, 1)]]) renders as
MULTILINESTRING (EMPTY, (0 0, 1 1)); use .parts to inspect those typed
empty members. Multipart members must share one coordinate-axis layout.
GeometryCollections¶
When parts have mixed types, use a GeometryCollection:
import gometry as gm
gc = gm.GeometryCollection([
gm.Point(0, 0),
gm.LineString([(1, 1), (2, 2)]),
gm.box(3, 3, 4, 4),
])
print(gc.to_wkt())
GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (1 1, 2 2), POLYGON ((3 3, 4 3, 4 4, 3 4, 3 3)))
Collection area counts overlaps once; length sums members
.area measures the union of a collection's
areal support, so overlapping members count once. .length
sums every member's length or perimeter, including overlapping members.
Points and lines contribute zero area.
Prefer homogeneous multi-types when you want a single-kind column; reserve
collections for genuinely heterogeneous
results (e.g. an overlay that returns both lines and points, or the
tessellation operations).
Inspecting geometries¶
These properties and functions apply to every geometry type:
| Call | Returns |
|---|---|
geom.geometry_type |
the type name, e.g. "Polygon" |
geom.coordinate_axes |
ordinate layout: "XY", "XYZ", "XYM", "XYZM" |
geom.is_empty |
whether the geometry holds no coordinates |
bounds |
(minx, miny, maxx, maxy) tuple |
geom.coords |
every vertex as a flat, indexable Coordinates view |
geom.parts |
component geometries of a multi-part |
poly.exterior / poly.interiors |
the shell and hole rings of a Polygon |
import gometry as gm
poly = gm.Polygon(
[(0, 0), (4, 0), (4, 4), (0, 4)],
holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]],
)
print("type:", poly.geometry_type)
print("coordinate_axes:", poly.coordinate_axes)
print("bounds:", poly.bounds)
print("vertices:", len(poly.coords))
print("parts:", len(poly.parts))
print("holes:", len(poly.interiors))
type: Polygon
coordinate_axes: XY
bounds: (0.0, 0.0, 4.0, 4.0)
vertices: 10
parts: 1
holes: 1
geom.coords is a flat, indexable Coordinates view over every vertex
(each ring, each part, flattened depth-first). Index it for tuples, iterate it,
or pull whole axes as columns and hand
them straight to NumPy, pandas, or polars without a Python loop:
import numpy as np
import gometry as gm
coords = gm.LineString([(0, 0), (1, 2), (3, 1)], z=[10.0, 20.0, 30.0]).coords
print("len:", len(coords))
print("first tuple:", coords[0])
print("axes:", coords.coordinate_axes)
print("x column:", repr(np.asarray(coords.x))) # zero-copy ndarray
print("dict keys:", list(coords.to_dict().keys()))
len: 3
first tuple: (0.0, 0.0, 10.0)
axes: XYZ
x column: array([0., 1., 3.])
dict keys: ['x', 'y', 'z']
.x/.y (and .z/.m when present) are zero-copy columns; .to_dict() returns
read-only NumPy columns such as {'x': ndarray, 'y': ndarray} for direct use
with pd.DataFrame or pl.DataFrame (forced Z/M use NaN where absent);
np.asarray(coords) materializes an (N, dims) array; and .row_index tags each vertex
with its source row when the geometry is a GeometryArray.
Geometry types¶
Every geometry is an instance of a concrete subclass of
Geometry — Point,
LineString, Polygon,
MultiPoint, MultiLineString,
MultiPolygon, or
GeometryCollection — so isinstance works and
constructors/operations return the precise type:
import gometry as gm
print(type(gm.Point(1, 2)).__name__)
print(type(gm.box(0, 0, 1, 1)).__name__)
print(isinstance(gm.box(0, 0, 1, 1), gm.Polygon))
Point
Polygon
True
The multi-part classes are also registered as collections.abc.Sequence, so
isinstance checks and ABC-based dispatch recognize the sequence interface.
Their static types are Sequence[Point], Sequence[LineString],
Sequence[Polygon], or Sequence[Geometry]. Use class patterns such as
MultiPoint(parts) for matching; anonymous sequence patterns do not match
geometry objects. NumPy still treats each multipart geometry as one object:
np.asarray(multi) has shape (), and np.array([multi, multi], dtype=object)
has shape (2,).
import gometry as gm
poly = gm.Polygon([(0, 0), (4, 0), (4, 4), (0, 4)], holes=[[(1, 1), (2, 1), (2, 2), (1, 2)]])
print("exterior:", poly.exterior.geometry_type, "| holes:", len(poly.interiors))
mp = gm.MultiPolygon([gm.box(0, 0, 1, 1), gm.box(2, 2, 3, 3)])
print("parts:", len(mp), "| first:", mp[0].geometry_type, "| areas:", [p.area for p in mp])
exterior: LineString | holes: 1
parts: 2 | first: Polygon | areas: [1.0, 1.0]
Accessing a member on the wrong type raises AttributeError: line.x fails
because scalar .x is a Point property. Use line.coords.x for its X column.
Equality and hashing¶
Geometries are value objects: == and hash() compare the CRS, coordinate
epoch, and exact geometry (type, coordinates including Z/M, and vertex order), so
geometries work in sets and as dict keys:
import gometry as gm
a = gm.LineString([(0, 0), (1, 1)])
b = gm.LineString([(0, 0), (1, 1)])
reversed_ = gm.LineString([(1, 1), (0, 0)])
print("a == b:", a == b, "| a == reversed:", a == reversed_)
print("distinct:", len({a, b, reversed_}))
a == b: True | a == reversed: False
distinct: 2
This == is structural (vertex-order sensitive). For the topological
"same shape on the map" test that ignores vertex order, use
gm.equals; for a representation-independent vertex check,
apply normalize to both inputs before calling
gm.equals_exact.
Dimensions: XY, XYZ, XYM, XYZM¶
gometry supports four coordinate layouts — XY, XYZ, XYM, and XYZM — from the constructors through IO.
Three meanings of "dimension"¶
"Dimension" has three distinct meanings in GIS, and gometry keeps them separate:
| Concept | Property | Example value |
|---|---|---|
| Topological dimension | geom.topological_dimension |
0 (Point), 1 (LineString), 2 (Polygon) |
| Coordinate axes / ordinate layout | geom.coordinate_axes |
"XY", "XYZ", "XYM", "XYZM" |
| Has-ordinate flags | geom.has_z, geom.has_m |
True / False |
import gometry as gm
trace = gm.LineString(
[(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
z=[10.0, 20.0, 30.0],
m=[0.0, 1.0, 2.0],
crs=4326,
)
print("geometry_type:", trace.geometry_type)
print("topological_dimension:", trace.topological_dimension)
print("coordinate_axes:", trace.coordinate_axes)
print("has_z / has_m:", trace.has_z, "/", trace.has_m)
geometry_type: LineString
topological_dimension: 1
coordinate_axes: XYZM
has_z / has_m: True / True
Z is a spatial axis; M is a measure¶
- Z is an additional spatial ordinate, usually elevation or height. gometry does not assume Z is in meters unless a vertical CRS says so — Z is a third spatial number until a CRS gives it units.
- M is a measure ordinate. It commonly carries linear-referencing values, route
measures, distance-along-line, per-vertex timestamps, or application-specific event
alignment. M is not a spatial axis and must never be silently treated as Z. M is also
distinct from the coordinate epoch (
geom.epoch): the epoch is a single decimal year that dates the whole geometry's CRS realization, whereas M is a free per-vertex value with no datum.
A GPX track or vehicle trace has both elevation (Z) and a capture timestamp or route measure (M) aligned to each vertex. gometry lets you store both on the geometry instead of the common workaround of a 2D line plus side arrays:
import gometry as gm
track = gm.LineString(
[(21.00, 52.00), (21.01, 52.01), (21.02, 52.00)],
z=[105.0, 118.0, 110.0],
m=[1_700_000_000, 1_700_000_030, 1_700_000_060],
crs=4326,
)
coords = list(track.coords.select('XYZM'))
print("coordinate_axes:", track.coordinate_axes)
for x, y, z, m in coords:
print(f" lon={x} lat={y} elev={z} t={int(m)}")
coordinate_axes: XYZM
lon=21.0 lat=52.0 elev=105.0 t=1700000000
lon=21.01 lat=52.01 elev=118.0 t=1700000030
lon=21.02 lat=52.0 elev=110.0 t=1700000060
coords.select(...) chooses a fixed coordinate-view
layout for nested output, iteration, column access, ndarray/dict exports, and
equality without changing the geometry. Nested coordinate export preserves
missing geometry rows as None; flattened columns omit their coordinates.
Explicit constructors identify ambiguous tuples¶
A bare 3-tuple (x, y, v) is ambiguous — it could be XYZ or XYM. Use the
z= and m= keywords to identify the layout:
import gometry as gm
p_xy = gm.Point(21.0, 52.0, crs=4326)
p_xyz = gm.Point(21.0, 52.0, z=105.0, crs=4326)
p_xym = gm.Point(21.0, 52.0, m=1_700_000_000.0, crs=4326)
p_xyzm = gm.Point(21.0, 52.0, z=105.0, m=1_700_000_000.0, crs=4326)
for p in (p_xy, p_xyz, p_xym, p_xyzm):
print(p.coordinate_axes, "→", p.to_wkt())
XY → POINT (21 52)
XYZ → POINT Z (21 52 105)
XYM → POINT M (21 52 1700000000)
XYZM → POINT ZM (21 52 105 1700000000)
For arrays and lines, pass z= and m= keywords to gm.points and gm.LineString:
points_xy = gm.points(lons, lats, crs=4326)
points_zm = gm.points(lons, lats, z=elevations, m=timestamps, crs=4326)
line = gm.LineString(xy, z=elevations, m=timestamps, crs=4326)
Consistent layout per scalar geometry
A non-collection geometry must use one coordinate-axis layout for every vertex — a
LineString cannot mix XY and XYZM vertices. A GeometryCollection may hold children
with different layouts, but the API makes that explicit rather than silent.
WKT carries the Z, M, and ZM dimensional tags, which round-trip through gometry:
import gometry as gm
print(gm.from_wkt("POINT Z (1 2 3)").coordinate_axes)
print(gm.from_wkt("POINT M (1 2 7.5)").coordinate_axes)
print(gm.from_wkt("POINT ZM (1 2 3 7.5)").coordinate_axes)
XYZ
XYM
XYZM
Measuring with Z and M¶
3D spatial measurement uses the Z axis. length_3d and point-to-point
distance_3d(other) include the vertical component, and min_z / max_z
(properties) summarize the Z extent that complements the XY bounds. Because a 3D length only makes
sense when every axis shares one linear unit, these require a projected CRS (or no
CRS); a geographic CRS mixes degrees and metre heights and raises under every unit= —
reproject first.
They report the CRS's own linear unit, exactly like their 2D siblings: a US-survey-foot
CRS gives feet from length_3d, as it does from length. The free functions
gm.length_3d and gm.distance_3d take the
same unit= override as gm.length and
gm.distance when you want SI metres, projected native linear
units, or CRS-free raw coordinate units.
On a geometry without Z, accessors and metrics have different results. The
accessors (min_z / max_z) describe what is
there, returning None for a scalar or nan for an array element. The metrics
(length_3d / distance_3d) require Z to compute: a scalar raises, while an array
degrades per element to nan for the rows that lack Z.
import gometry as gm
climb = gm.LineString([(0.0, 0.0), (3.0, 4.0), (3.0, 4.0)], z=[0.0, 0.0, 10.0], crs=32634)
print('length_3d:', climb.length_3d)
print('z extent:', climb.min_z, climb.max_z)
a = gm.Point(0.0, 0.0, z=0.0, crs=32634)
b = gm.Point(3.0, 4.0, z=12.0, crs=32634)
print('distance_3d:', gm.distance_3d(a, b))
length_3d: 15.0
z extent: 0.0 10.0
distance_3d: 13.0
Linear referencing¶
Linear referencing uses the geometry's CRS metric by default. Values are absolute
distances unless normalized=True, when 0.0 and 1.0 mean the start and end.
Pass basis='m' when the line's M ordinate is the route measure instead.
| Task | Distance basis (default) | M basis |
|---|---|---|
| point at a location | line_interpolate(at) |
line_interpolate(at, basis='m') |
| location nearest a point | line_locate(point) |
line_locate(point, basis='m') |
| portion between two locations | line_substring(start, end) |
line_substring(start, end, basis='m') |
import gometry as gm
line = gm.LineString([(0.0, 0.0), (10.0, 0.0)], crs=32634)
midpoint = line.line_interpolate(0.5, normalized=True)
samples = line.line_interpolate(count=3)
print('midpoint:', midpoint.to_wkt())
print('samples:', [point.to_wkt() for point in samples])
midpoint: POINT (5 0)
samples: ['POINT (0 0)', 'POINT (5 0)', 'POINT (10 0)']
Distances and route positions¶
With CRS-aware units (the default on a geographic CRS), linework edges are straight in the CRS's longitude/latitude chart and LRS measures the ellipsoidal arclength of the retained chart-straight source edge, rather than geodesic distance between each pair of vertices.
line_locate minimizes against those
same chart edges. When backend distances compare equal, it returns the interior
projection before a point or endpoint, then uses route order. This ranks
available measurements, not an exact geodesic continuum minimum. Repeated
support and self-crossings can have several stations at one location, so
locate/interpolate is not a unique inverse there.
An independently interpolated point has canonical longitude, while a
line_substring cut stays in the retained source edge's lifted chart.
unit='planar' instead uses Cartesian distance in the stored coordinate space.
Planar distance-basis locate uses the exactly nearest segment; exact ties prefer
an interior projection over an endpoint, then the first segment in route order,
matching planar proximity.
Zero-length X/Y segments consume no distance, but their stored vertices remain on the route.
Interpolation at an interior prefix shared by a zero-length run
selects its first stored vertex; normalized 0.0 and 1.0 select the first
and last stored vertices, including when the whole route has zero length.
line_substring includes active-distinct stored vertices in its inclusive
range, even when consecutive vertices share X/Y and differ only in Z/M.
At a multipart prefix boundary, interpolation selects the preceding part's
endpoint, so interpolating a locate station at that boundary need not recover
the next part's nearest point.
Sampling multiple locations¶
On the distance basis, line_interpolate accepts a sequence of locations or
count= for evenly spaced samples. With basis='m', a scalar line accepts one
stored measure for a Point or a measure sequence for a GeometryArray[Point];
an array receiver accepts one measure per line row.
An array receiver returns
row-aligned points for one location per row or Groups when
each row has its own sample count or explicit station sequence.
Construct
numeric groups from a flat NumPy payload and CSR offsets, for example
gm.Groups(np.array([0, 5, 10, 2, 8]), [0, 3, 5]), and pass them as at;
each route uses its corresponding stations, under either distance or M basis.
Plain numeric at sequences remain flat, including []; use Groups to request
grouped output even for zero routes.
An empty station row produces an empty group, a missing station row or route
produces a missing group, and an empty route produces one empty point per
station.
normalized and unit belong only to the
distance basis, so a measured route never silently mixes two coordinate
systems. On the distance basis, unit selects the route metric even when
normalized=True:
fractions are measured along that selected route, so changing unit can change
the resulting position.
count= also uses fractions of the selected metric and
accepts unit=, but rejects normalized=True because the count already defines
fractional sample positions.
M-based positions¶
M linear referencing locates positions by the M ordinate rather than by
fractional or absolute length. line_interpolate(m, basis='m') returns the
point at a given measure, line_locate(point, basis='m') returns the measure
nearest a point, and line_substring(start_m, end_m, basis='m') cuts the
section between two measures. They require every vertex to carry a monotonic,
non-decreasing M. The extent accessors mirror the Z family:
min_m / max_m summarize the measure span (None for a scalar, nan for an
array element, when no vertex carries M).
Within each edge, M varies linearly with arclength. For increasing endpoint
measures, a requested m selects the arclength fraction
(m - m0) / (m1 - m0); equal endpoint measures remain constant.
Z is carried at that same fraction, and line_locate(..., basis='m')
converts the nearest point's arclength fraction back to M.
On a geographic CRS this is ellipsoidal arclength of the retained chart-straight source edge; the fraction is generally different from its longitude/latitude parameter. For example, a WGS 84 meridian from latitude −10° to 10° with endpoint measures 100 and 300 places M=150 at latitude approximately −5.00037949475414°, not −5°.
Distance-basis interpolation and substring use
the same Z/M carry rule; interpolate_m assigns vertex measures by cumulative
edge arclength without moving X/Y/Z.
Empty routes and invalid measures¶
Empty linework has no positions, so every
form answers it instead of raising: line_locate returns nan,
line_interpolate returns POINT EMPTY, line_substring returns
LINESTRING EMPTY, and interpolate_m returns the empty geometry with an M
axis. A POINT EMPTY query locates to nan.
Missing or non-monotonic M is
different: both scalar and array calls raise
InvalidGeometryError, because they asked for
measures the geometry does not have. Array failures identify the original input
row; an invalid route is never converted into a successful empty result.
Wrong-kind and parameter errors also raise on both.
import gometry as gm
route = gm.LineString([(0.0, 0.0), (10.0, 0.0)], m=[0.0, 100.0], crs=32634)
print("at m=50:", route.line_interpolate(50.0, basis='m'))
print("locate:", route.line_locate(gm.Point(5.0, 0.0, crs=32634), basis='m'))
print("m 20..80:", route.line_substring(20.0, 80.0, basis='m'))
at m=50: POINT M (5 0 50)
locate: 50.0
m 20..80: LINESTRING M (2 0 20, 8 0 80)
Working with coordinate sequences¶
segments() extracts each consecutive pair of
stored vertices as a LineString, including closing ring edges and repeated
coordinate pairs. It visits lines and polygon rings independently, so parts
and holes never acquire connecting edges. Points and empty geometries yield
no segments. A scalar returns a GeometryArray[LineString]; an array returns
one segment group per source row, retaining missing rows.
line_merge(directed=True) joins line endpoints
without reversing any source line. Both directed and ordinary merging stop
at branching junctions. Directed merging also stops wherever a join would
require reversing a source sequence.
When joined endpoints share X/Y but differ in Z or M, directed merging retains
both stored occurrences as consecutive vertices.
Geographic endpoints also join across seam and pole aliases. Their published
longitude spellings may change, with vertices inserted where needed to preserve
an authored full-period edge. Inserted Z/M follow the original source arc.
Use map_coordinates for one flattened,
same-shape coordinate transform. Use
map_coordinate_sequences when
lines and rings need independent edits or a different vertex count. Its callback
receives a read-only (N, D) float64 array in each sequence's native ordinate
schema. Return a matrix with any number of rows and the same D.
import gometry as gm
import numpy as np
line = gm.LineString([(0, 0), (2, 1), (4, 0)])
print("segments:", line.segments().to_wkt())
print("endpoints:", line.map_coordinate_sequences(lambda c: c[[0, -1]]).to_wkt())
routes = gm.GeometryArray([line, line])
stations = gm.Groups(np.array([0, 1, 0.25, 0.75]), [0, 2, 4])
samples = routes.line_interpolate(stations, normalized=True)
print("sample counts:", samples.counts.tolist())
segments: ['LINESTRING (0 0, 2 1)', 'LINESTRING (2 1, 4 0)']
endpoints: LINESTRING (0 0, 4 0)
sample counts: [2, 2]
Parts and rings keep their grouping, coordinate schemas, CRS, and epoch. Points receive one-coordinate matrices and can return zero or one coordinate. A multipoint receives its stored point sequence; resizing it replaces its point membership, including empty slots. Empty source components skip the callback. Empty output empties a point or line and removes a hole; a polygon becomes empty when all its rings become empty. An empty shell with nonempty holes is an error.
Callback output uses ordinary constructor admission: a nonempty line needs at least two vertices, and a nonempty ring needs three open corners or four closed vertices. Open rings are closed on all active ordinates; conflicting Z/M closure is rejected. Non-finite coordinates and changed ordinate widths raise errors. No validity repair is performed.
Z/M under operations¶
Planar Simple Features predicates are XY topology predicates. Whether a contains b is
decided in X/Y; Z and M are ignored for the truth value but preserved on the geometry.
Constructive operations resolve Z and M independently wherever they are
derivable and drop only the axis that cannot be sourced:
- Operations whose output vertices are copied from input vertices preserve Z/M:
simplify,concave_hull,reverse, the triangulations, and source vertices retained byconvex_hullkeep their ordinates. The convex hull's XY shape is independent of input point order (±0.0 compares as the same ordinate). When several input vertices share an XY position, a copied hull vertex comes from the first vertex in storage order, including its Z/M and signed-zero spelling. - Operations whose new vertices are resolvable against the input — overlay,
split,snap, and linear referencing — interpolate Z/M along source segments. They retain active-distinct source vertices even when consecutive X/Y positions match; distance-based linear referencing gives zero-length X/Y segments no length. If an axis has a genuinely unsourceable output vertex (such as a box corner entering the output), or conflicting source values, that axis is dropped while the other axis may remain. -
On geographic input, overlays and source-preserving linework — including
boundary,split,snap, the default chart path ofsegmentize,convex_hull,nearest_points,shortest_line,minimum_clearance_line,subdivide,line_merge,coverage_union_all, andcoverage_simplifycarry eligible Z/M at an inserted vertex when its producer traces it to a source edge. Z/M follow that edge's original endpoints by ellipsoidal arclength of the retained chart-straight source edge on its CRS ellipsoid, not by its chart parameter or a later cut fragment. This includes vertices inserted at the antimeridian for GeoJSON serialization.Source vertices retain their stored ordinate bits; when sources disagree, Z and M resolve independently and only a conflicting or unavailable axis is dropped. A planar
unit=choice for a nearest witness does not change how its geographic source-edge ordinates are carried.A computed point with no sourceable edge has no source Z/M. Recursive subdivision retains original input-edge traces through later cuts, including seam vertices (±180° in EPSG:4326).
On a certified zero-length source edge, an interior sample retains an axis only when both endpoint values have identical bits; a disagreement follows the operation's per-axis conflict policy. Exact source endpoints keep their own values. A nonzero edge whose measured length underflows to zero raises a metric error.
-
A degenerate
minimum_rotated_rectanglethat follows geographic source-edge support carries resolvable Z/M by the same original-edge arclength rule. A computed areal rectangle is a new XY construction and is returned in XY. - Empty overlay results retain the axes declared by the operation:
differencefollows its first operand, while the other set operations use the union of their operands' axes. - Operations that compute brand-new points —
centroid,point_on_surface,envelope,buffer,voronoi_*,polylabel— cannot source Z/M for invented vertices, so they return 2D. - Circle solutions from
maximum_inscribed_circleandminimum_bounding_circlehave XY computed centers and positive-radius polygon realizations. They preserve the source CRS and coordinate epoch. A zero-radius bounding center selected from an input site retains the first source point's Z/M and stored spelling.
import gometry as gm
track = gm.LineString(
[(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)],
z=[10.0, 20.0, 30.0], m=[0.0, 1.0, 2.0], crs=4326,
)
centroid = track.centroid()
print("centroid:", centroid.geometry_type, centroid.coordinate_axes)
flat = track.force_2d()
print("force_2d:", flat.geometry_type, flat.coordinate_axes)
centroid: Point XY
force_2d: LineString XY
- input
- result
For a permanent 2D conversion, use force_2d to
drop Z and M. force_3d fills only vertices that
lack Z. Use with_coordinates to replace
one or more ordinates without changing topology. Each supplied value can be a
scalar broadcast over vertices or a column with one value per vertex in the
flattened coordinate sequence; omitted ordinates are preserved, and
z=None / m=None removes that optional axis. X and Y cannot be removed.
Matrix mode replaces the coordinate matrix using its existing axis layout and
cannot be mixed with ordinate columns:
import gometry as gm
track = gm.LineString([(0, 0), (1, 1), (2, 0)], z=[10, 20, 30], crs=4326)
flat = track.force_2d()
print("force_2d axes:", flat.coordinate_axes)
print("flat centroid:", (flat.centroid()).to_wkt())
print("force_3d axes:", flat.force_3d().coordinate_axes)
print("with_coordinates(z=...):", flat.with_coordinates(z=10.0))
print("with_coordinates(m=...):", flat.with_coordinates(m=0.0).coordinate_axes)
force_2d axes: XY
flat centroid: POINT (1 0.5)
force_3d axes: XYZ
with_coordinates(z=...): LINESTRING Z (0 0 10, 1 1 10, 2 0 10)
with_coordinates(m=...): XYM
IO and format limits¶
WKT, WKB/EWKB, and GeoArrow preserve both ordinates. to_geojson() raises on M, and __geo_interface__ raises on M or a
coordinate epoch because GeoJSON has no slots for them. WKB/EWKB preserve M but
not epoch; GeoArrow preserves epoch metadata. WKT/WKB preserve dimensional-empty
axes, while GeoJSON does not. Format details are in Text & binary
formats; GeoArrow's columnar metadata is described
in Arrow & storage.
Enforcing a dimension contract¶
Use axes= with gm.require to require a coordinate layout.
For example, require XY when your storage must not carry Z or M:
area = gm.require(obj, crs=4326, axes="XY")
trace = gm.require(obj, crs=4326, axes="XYZM")
See Validation for topology checks and ingestion examples.
See also¶
- Predicates — spatial relationships (
contains,intersects, DE-9IM). - Constructive operations — buffers, overlays, hulls, and how they carry Z/M.
- CRS — the metric frame for lon/lat area, length, and distance.
- Arrays — vectorized
GeometryArraycolumns and the missing-row rule. - Validation — the OGC validity side of the storage contract.