Skip to content

Fix raster/vector misregistration and projected-CRS input handling - #308

Merged
frewsxcv merged 4 commits into
mainfrom
fix/raster-vector-registration
Oct 8, 2026
Merged

frewsxcv merged 4 commits into
mainfrom
fix/raster-vector-registration

Conversation

@frewsxcv

@frewsxcv frewsxcv commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A world raster and world vector layers don't line up. The Great Lakes in the raster sit roughly 300 km south of the Lakes layer in EPSG:3857, the default CRS.

Causes and fixes

Raster UVs vs. clamped extent (the main bug). Before projecting, the raster grid is clamped to the target CRS's area of use (±85.06° for 3857). The UVs still spanned the whole texture ([col/cols, 1 - row/rows]), so a ±90° raster was squashed onto ±85.06°. Each vertex's UV now comes from the source coordinate it was sampled at, and ProjectedRasterGrid carries uvs and a triangles() iterator that the renderer uses directly.

Grid too coarse. Each grid cell maps the texture with a straight-line (affine) fit, which can't follow Mercator's curve. With a fixed 32×32 grid that caused about 7 km of error at 45°N and 53 km near the pole. The grid now starts at 32 cells per side and refines each axis separately until the error at edge midpoints is under 0.25 texel. The cap is 2048 cells per side and 2^18 cells total. The error is converted to texels through each cell's own affine map, because polar cells are very anisotropic. World 4326 → 3857 ends up at 32×512.

Projected-source rasters in local CRSs. For example, a world 3857 raster shown in UTM drew folded triangles over valid areas. Only the part of such a raster that covers the target's area of use, expanded by 25% on each side, is sampled now.

geo_geodesy::Transformer treats every input as degrees. It always called Coor2D::gis, which multiplies projected metres by π/180. That broke:

  • vector reprojection from a projected source CRS
  • the cursor lat/lng readout in projected CRSs
  • the measure tool
  • the camera when the CRS changes

All of these now go through a new rgis_crs::CrsTransformer, which converts units based on whether each CRS is geographic. The geo-geodesy dependency is removed.

GeoTIFF extent. The loader now applies the tiepoint's (I, J) offset and the half-pixel shift for PixelIsPoint. Extents match gdalinfo for every fixture in geotiff-test-data except rotated ones; rotated transforms were never supported.

Tests

  • rgis-transform/src/alignment_tests.rs: an exhaustive registration test. Lon/lat sample points go through the vector pipeline, and synthetic rasters go through the raster pipeline. Each projected sample is then located in the rendered mesh using the same triangles and barycentric UV interpolation as the GPU, and its UV is compared with the true one. The test covers 5 raster setups (world 4326, Great Lakes 4326, world 3857, UTM 18N) × 6 targets (4326, 3857, 3395, 32618, 3978, 3035). The worst case is 0.15 px. With the old UV and grid logic swapped back in, it fails at 168 px (about 310 km) for Great Lakes samples in 3857.
  • CrsTransformer round-trip tests for projected sources.
  • compute_extent unit tests, plus a test pinning pixel_as_point.tif to the extent gdalinfo reports.

cargo test --workspace passes, and cargo check --target wasm32-unknown-unknown -p rgis is clean with -D warnings.

Playwright snapshot changes

Four raster-layer snapshots were updated from CI's --update-snapshots run:

  • eox_cloudless and eox_cloudless + World: Countries: the raster now lines up with the vector coastlines and reaches the Arctic.
  • antimeridian.tif (lon -204..-162): clamped at 180°, the same way vectors are, instead of the whole texture being squeezed into -180..-162. The slice west of -180° isn't wrapped to the east side; neither version did that.
  • hot-oam drone image: same imagery, slightly shifted. The old baseline also predated main's graticule labels and scale bar, and it showed raw X/Y where the readout now gives a correct lat/lng.

Grid refinement and small high-res rasters

The first CI run timed out loading the hot-oam, NLCD, Umbra and Maxar rasters. The grid error was being measured on f32 mesh positions. For 5 cm drone imagery in 3857, f32 rounding (about 0.5 m) alone exceeds the 0.25-texel target, so refinement ran to the 2^18-cell cap. The error is now measured in f64, and refinement stops on any axis where doubling it no longer halves its error. small_high_res_rasters_keep_a_coarse_grid covers this case.

Not addressed

EPSG:3857 is still projected on the ellipsoid rather than the sphere, because geodesy's parse_proj drops +b. It shifts rasters and vectors equally, so it doesn't cause misregistration between them.

The extent assumed the tiepoint was raster point (0, 0) and that it marked
the top-left pixel's outer corner. Account for the tiepoint's (I, J) and,
for GTRasterTypeGeoKey = PixelIsPoint, shift by half a pixel so the extent
covers the outer pixel edges, as GDAL does. Extents now match gdalinfo for
every fixture in geotiff-test-data except rotated ones (unsupported).
Rasters didn't line up with vector layers, e.g. the Great Lakes over a
world raster in EPSG:3857:

- The raster mesh was sampled over the extent clamped to the target CRS's
  area of use (±85.06° for 3857), but its UVs always spanned the full
  texture, squashing a ±90° raster toward the equator (~310 km at 45°N).
  UVs are now derived from each vertex's source coordinate.
- A fixed 32×32 grid mapped the texture affinely within cells far larger
  than the projection's curvature allows (~7 km off at 45°N, 53 km near
  the pole in 3857). The grid is now refined per axis until the error at
  edge midpoints is under 0.25 texel, capped at 2^18 cells.
- Projected-source rasters shown in a local CRS (e.g. 3857 → UTM) drew
  folded triangles; only the part covering the target's (expanded) area
  of use is sampled now.

geo_geodesy::Transformer always treated input as degrees, scaling metres
by π/180. That broke vector reprojection from projected CRSs, the cursor
lat/lng readout, the measure tool, and the camera on CRS change. Replace
it with rgis_crs::CrsTransformer, which handles units per CRS, and drop
the geo-geodesy dependency.

Add an exhaustive registration test (rgis-transform alignment_tests) that
pushes sample points through the vector pipeline and locates them in the
rendered raster mesh, across 36 raster-CRS × target-CRS combinations.
@frewsxcv
frewsxcv enabled auto-merge October 8, 2026 20:40
Grid refinement measured the mesh's error using its f32 positions. For a
small, high-resolution raster (e.g. 5 cm drone imagery in 3857, where f32
spacing is ~0.5 m), the rounding alone exceeded the 0.25-texel target, so
refinement ran to the 2^18-cell cap. That made the web app hang loading
such rasters (Playwright timeouts on the hot-oam, NLCD, Umbra and Maxar
fixtures).

Sample and measure in f64, converting to f32 only for the final mesh. Also
stop refining an axis once doubling it no longer at least halves its
error, so no other non-converging case can run to the cap either.
Taken from CI's --update-snapshots run (Linux). Rasters now line up with
vector layers, e.g. eox_cloudless coastlines with World: Countries, and
antimeridian.tif is clamped at 180° rather than squeezed into -180..-162.
The old baselines also predated main's graticule labels and scale bar.
@frewsxcv
frewsxcv added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 9b79cc0 Oct 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant