feat: sample a WKT MULTIPOINT on /position into a MultiPoint coverage#78
Open
chuckwondo wants to merge 11 commits into
Open
feat: sample a WKT MULTIPOINT on /position into a MultiPoint coverage#78chuckwondo wants to merge 11 commits into
chuckwondo wants to merge 11 commits into
Conversation
The geometry a MultiPoint-domain coverage is sampled over: a frozen set of (x, y) positions. Bare pairs, not Position values, so an unservable z cannot be constructed rather than being rejected downstream. Rejects duplicate positions at construction. A repeat would be ambiguous as a coverage axis value, and the CoverageJSON schema rejects it via uniqueItems. No bounds property: nothing samples a bounding box over a set of points.
The CoverageInput variant pairing a MultiPoint geometry with a 2-D (bands, positions) masked array: one sampled value per band per position, the position axis aligned to geometry.positions. A masked entry marks a band with no value at that position, serializing as null. The shape check tests ndim before indexing data.shape[1], so a non-2-D array raises ValueError rather than IndexError. Not yet in the CoverageInput union: it joins there with the modeler arm that dispatches it, so the exhaustiveness check stays satisfied at every commit.
Add MultiPointInput to the CoverageInput union and the modeler's match: a MultiPoint domain whose single composite axis lists one (x, y) tuple per position, and a range running 1-D over that composite axis, one value per band per position. Masked entries serialize as null in place. The composite lists positions directly (values=[[x, y], ...]), inverting the Polygon domain's wrap of its one geometry. coordinates stays ["x", "y"] even when a latitude-first CRS gives referencing ["y", "x"]: the two describe different things and unifying them would misread a position.
pointdata_to_multipoint_input takes one read per position, None where a position fell outside the dataset, and stacks them into a (bands, positions) masked array: each read is a column, each None an all-masked column, so an out-of-bounds position serializes as null in place rather than dropping out. bands and crs are required, unlike the single-point converter: when every position is out of bounds there is no read to derive them from, so the caller resolves both up front and passes them in. The band dtype is stamped from the stacked array, so the declared range type equals the values serialized and does not depend on which positions landed inside. A synthesized masked column takes the band dtype rather than masked_all's float64 default, which would otherwise promote an integer read.
parse_multipoint_wkt reads both WKT spellings, MULTIPOINT((x y), (x y)) and MULTIPOINT(x y, x y), and a mix of the two. It strips the per-point parentheses so both reduce to the one x y, x y grammar _parse_xy_pairs already reads, then hands the points to MultiPoint for the set invariants. A strip, not a findall of parenthesized groups: a findall would silently drop the bare points of a mixed list. parse_position_coords is the single entry point the position endpoint dispatches on: a POINT yields a Position, a MULTIPOINT a MultiPoint, and anything else an InvalidCoords naming both accepted forms. Named for the coords parameter it reads, since it spans two grammars rather than one. Neither is wired into the route yet; that is the next commit.
Dispatch /position on parse_position_coords: a POINT keeps the existing Point path unchanged, a MULTIPOINT reads one sample per position and builds a MultiPoint coverage. A position outside the dataset becomes a null value rather than an error, so the request succeeds even when every position is outside. max_samples (default 1000) caps the number of positions, rejected before any read. It bounds the point-read count, a different resource from max_cells.
A MULTIPOINT position in the dataset's last row or column, read with nodata set, trips a rio-tiler off-by-one that flags the edge-aligned window boundless; a WarpedVRT then refuses the read and the ValueError propagates rather than nulling that one position. This is the one hole in the out-of-bounds-becomes-null contract. Pin the current behavior and note it at the catch site, both citing #73 (upstream cogeotiff/rio-tiler#966) so the test flags when the fix lands.
Request a MULTIPOINT from the container smoke check and assert a valid MultiPoint coverage: a composite tuple axis over the sampled positions and a 1-D range per band. Note MULTIPOINT on the README /position bullet, and add MULTIPOINT (and an /area stat) curl example to the README and the dev_app docstring.
Print each request, its status, and the coverage it returned along the way, so a passing run shows what was served rather than only a summary line: e.g. "GET /position?coords=MULTIPOINT(...) -> 200 MultiPoint (2 positions, 2 bands)". Labels pad to a common width so the status columns align, and a rejected request shows its error detail.
The route chose the Point vs MultiPoint path with an isinstance if/else, leaving exhaustiveness to be caught only indirectly (by the typed _read_point call rejecting a non-Position). Replace it with a match over Position | MultiPoint whose default asserts never, mirroring the modeler's to_coverage dispatch, so a future position geometry is a compile error at the dispatch itself. Behavior is unchanged; the InvalidCoords guard stays ahead of the shared setup.
Both are subsets of stronger existing tests: - test_wkt: the 'duplicate' and 'nan' reject cases re-parse inputs that test_parse_multipoint_wkt_reports_duplicate_and_non_finite already checks with message assertions. - test_factory: test_position_multipoint_nodata_serializes_as_null duplicates the full-body multipoint test, which asserts the same request's b2 == [None, 3.0] verbatim.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/positionnow accepts a WKTMULTIPOINTalongsidePOINT.MULTIPOINT((x y), ...)returns a CoverageJSON MultiPoint coverage with one value per band at each position;POINTis unchanged. Closes #65.A spatial-only multi-position sample is a MultiPoint, not a Trajectory: the CoverageJSON schema forces a
tcoordinate onto every Trajectory tuple, so a time-free multi-position sample cannot be one. This makes/positionthe non-temporal home for multi-position sampling (ADR-0005).Behavior
null, so the request still succeeds even when every position is outside (200, all-null). This is deliberate set semantics:MULTIPOINT((999 999))returns 200/null, whilePOINT(999 999)stays 400.max_samples(default 1000), checked before any read.Z/M/ZMtag, or az), consistent with the 2-D raster backing.Notable pieces
geometry.MultiPoint: a frozen value object owning its own invariants (non-empty, finite, distinct positions).input.MultiPointInput+pointdata_to_multipoint_input: assemble per-position reads into a(bands, positions)array, aligning an out-of-bounds position to an all-masked column and stamping the band dtype from the stacked array (so the declared range type matches what is serialized).modeler: acompositetuple axis, plus one N-D range builder now shared by the Grid and MultiPoint domains.wkt:parse_multipoint_wkt(accepting both((x y), ...)and(x y, ...)spellings) behind aparse_position_coordsdispatcher.match/assert_neverdispatch on the/positionroute, plus small test and smoke-tooling cleanups.Known limitation
MULTIPOINT+expressioninherits a pre-existing bug: at a nodata pixel, rio-tiler'sPointData.apply_expressiondrops the mask and returns a fabricated value instead ofnull(tracked in #72, upstream cogeotiff/rio-tiler#965). This already affectsPOINT+expression; this PR neither fixes nor worsens it.