Repository navigation
feat(core): layer coordinateSystem supports arbitrary CRS - #10874
Pessimistress wants to merge 2 commits into
Conversation
| unpreproject: position => { | ||
| const wgs84 = targetConverter ? targetConverter.to(position) : position; | ||
| const source = geographic ? wgs84 : sourceConverter.from(wgs84); | ||
| return [source[0], source[1], source[2] ?? 0]; |
There was a problem hiding this comment.
Picking conversion can throw
Medium Severity
getPreprojection().unpreproject forwards math.gl inverse conversion without catching domain failures. HexagonLayer picking now calls that helper on packed centroids, so a bin outside the target CRS domain can throw during pick instead of returning a non-finite position as unprojectPosition does.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit df1769d. Configure here.
| dataChanged: 'tile geometry format changed', | ||
| propsOrDataChanged: true | ||
| }; | ||
| } |
There was a problem hiding this comment.
Subdivision clears VectorTile binary flag
Medium Severity
MVTLayer now sets binary to false whenever getSubdivisionStep() is positive. VectorTileLayer always loads binary tiles and forces binary: true at init, including non-MVT CARTO payloads that are already WGS84. After the first update with subdivision enabled, picking skips binaryToGeojson and highlighted feature objects are wrong.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit df1769d. Configure here.
[High impact] Coordinate system handling refactored across rendering pipeline. This PR needs fixes before merging: clipping can hide valid points, and the earlier rendering and example-build failures remain. Findings
SummaryAdds arbitrary source CRS strings and separates source coordinates, packed attributes, and viewport projection.
The previous-review ancestry checks did not pass, so this review used the full PR diff. Tests and visual checks were not run.
|
| // GlobeView doesn't work well with binary data | ||
| const binary = this.context.viewport.resolution !== undefined ? false : this.props.binary; | ||
| // Prepared binary geometry bypasses normalization/subdivision. | ||
| const binary = this.getSubdivisionStep() > 0 ? false : this.props.binary; |
There was a problem hiding this comment.
With subdivision disabled, MVTLayer now keeps binary tiles and their supplied triangle indices in GlobeView. Those triangles were built in southward-Y tile coordinates, but converting positions to longitude/latitude reverses their winding. Supplied indices skip the new outward-facing correction, so GlobeView hides their back faces and polygon fills disappear. Keep the non-binary globe path until supplied triangles are also corrected.
| usePositionTransforms(): Pick<AttributeOptions, 'transformSource' | 'transform'> { | ||
| return { | ||
| transformSource: 'projection', | ||
| transform: | ||
| this.context.viewport.preproject && this.props.coordinateSystem !== 'cartesian' | ||
| ? transformPosition | ||
| : null | ||
| transform: this.internalState?.preproject ? transformPosition : null |
There was a problem hiding this comment.
A mask polygon declared in EPSG:3857 now stores longitude/latitude positions in MapView. Its getBounds() returns those packed positions, but joinLayerBounds() passes them through layer.projectPosition() again as meters in EPSG:3857. The mask texture then covers the wrong place.
Update bounds consumers to use the packed CRS and skip transforms already applied during packing. The terrain bounds helper uses the same path.
| this._sourceCrs = sourceCrs; | ||
| if (this.internalState) { | ||
| this.internalState.sourceCrs = sourceCrs; | ||
| this._updatePreprojection(this.context.viewport); |
There was a problem hiding this comment.
Unchanged geometry keeps rebuilding
_setSourceCrs() refreshes conversion against the shared viewport on every layer match, even when the layer was last drawn in another view. _update() immediately switches back to internalState.viewport. In multi-view apps with different target CRSs, this marks unchanged positions and geometry for rebuilding on every update. Use the retained layer viewport here, or defer the refresh to _update().
| this._updatePreprojection(this.context.viewport); | |
| this._updatePreprojection(this.internalState.viewport || this.context.viewport); |
| transformMat4(position, position, this.props.modelMatrix); | ||
| } | ||
| return this.context.viewport.preproject!(position); | ||
| for (let axis = 0; axis < 3; axis++) position[axis] += this.props.coordinateOrigin[axis]; |
There was a problem hiding this comment.
JavaScript callers commonly pass two-element coordinateOrigin values, and the existing shader path accepts them. This new loop adds undefined to altitude during CPU conversion, producing NaN positions that can make geometry disappear. Default the missing origin altitude to zero before adding it.
| for (let axis = 0; axis < 3; axis++) position[axis] += this.props.coordinateOrigin[axis]; | |
| for (let axis = 0; axis < 3; axis++) position[axis] += this.props.coordinateOrigin[axis] ?? 0; |
The typedef of CRS definition should at least include objects to support PROJJSON, a more modern description of CRS than the relatively fickle and outdated proj strings |
df1769d to
bbbcc9c
Compare
| preproject: this.state.packedCrs | ||
| ? position => viewport.projectPosition(position, {sourceCrs: this.state.packedCrs!}) | ||
| : undefined, |
There was a problem hiding this comment.
Globe triangulation ignores modelMatrix and coordinateOrigin when positions stay in WGS84. This callback projects the raw polygon, but the shader draws the transformed polygon. For example, a ring around latitude 80 with coordinateOrigin: [0, -160, 0] gets north-facing triangles, then draws around latitude -80, where those triangles face inward and are culled. Include the layer transforms in the temporary globe coordinates, and rebuild the triangles when those transforms change.
bbbcc9c to
b0c50dc
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b0c50dc. Configure here.
| return; | ||
| } | ||
|
|
||
| if (this._coordinateSystemLoading) return; |
There was a problem hiding this comment.
Stale viewport after async CRS load
Medium Severity
activateViewport returns while a coordinateSystem descriptor is still loading and never stores the incoming viewport. When the CRS resolves, _update rebuilds attributes and sublayers from the previous internalState.viewport, so a camera or view change during that wait is ignored for one update cycle.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b0c50dc. Configure here.
| const corner0 = transform.call(this, [clipBounds[0], clipBounds[1], 0]); | ||
| const corner1 = transform.call(this, [clipBounds[2], clipBounds[3], 0]); | ||
| clipProps.bounds = [ | ||
| Math.min(corner0[0], corner1[0]), | ||
| Math.min(corner0[1], corner1[1]), | ||
| Math.max(corner0[0], corner1[0]), | ||
| Math.max(corner0[1], corner1[1]) |
There was a problem hiding this comment.
With CPU conversion active, ClipExtension builds its bounds from only two converted corners. For example, a ScatterplotLayer in MapView with coordinateSystem: 'EPSG:3857', a 45-degree modelMatrix rotation, and square clipBounds rotates those corners onto nearly the same X coordinate. The resulting box excludes most points inside the original square.
Test anchors against the original source-space rectangle rather than a box built from two converted corners.


For #10739
Background
Major conceptual clean up of projection pipeline. Now explicitly separates:
For example, MVTLayer in v9.4 creates sublayers with
coordinateSystem: 'cartesian'in MapView andcoordinateSystem: 'lnglat'in GlobeView. After this PR, it creates sublayers withcoordinateSystem: 'OSMTile512'in all viewports.Antarctica,

coordinateSystem: 'EPSG:3031'(Stereographic) +GlobeViewFrance regions,

coordinateSystem: 'EPSG:2154'(Lambert-93) +MapViewSwitzerland cantons,

coordinateSystem: 'EPSG:2056'(LV95) +CustomProjectionView toCrs: 'EPSG:8857'(Equal Earth)Change List
Layer.coordinateSystemOSMTile512Note
High Risk
This is a v10-breaking change to core projection, public constants, viewport CRS APIs, and tiled-layer coordinate handling across aggregation and geo layers.
Overview
Layers can declare arbitrary CRS identifiers via
coordinateSystem(EPSG codes, PROJ strings, aliases like'OSMTile512').LayerManagerresolves descriptors asynchronously and blocks init/update until ready; layers track source CRS, packed attribute CRS, and optional CPUpreproject/unpreprojectpairs fromviewport.getPreprojection().The projection pipeline is split explicitly: layer data CRS → packed attributes → view target CRS → shader common space.
COORDINATE_SYSTEMand_PROJECTION_MODEare removed from public exports; layers use string CRS names. The project shader uniformcoordinateSystemis renamedcoordinateFrame(geodetic/projected/geocentric packed frames).Viewport
resolutionnow means subdivision intervals around the earth (step = circumference / resolution), default0everywhere includingGlobeView(migrate old 10° withresolution: 36).preproject/postUnprojectviewport hooks are removed in favor ofgetPreprojection(sourceCrs)andgetSubdivisionStep(sourceCrs).MVTLayeralways loads tile-local geometry withcoordinateSystem: 'OSMTile512'and converts per viewport; binary mode disables when subdivision is on. Aggregation, heatmap, clip/mask extensions, and related layers switch fromviewport.preprojecttoproject.isCustomProjectionand layer preprojection state. Docs and the v10 upgrade guide cover CRS config, transform order (modelMatrixthencoordinateOrigin), and breaking API changes.Reviewed by Cursor Bugbot for commit b0c50dc. Bugbot is set up for automated code reviews on this repo. Configure here.