Skip to content

Commit 39cf0d5

Browse files
committed
fix: handle antimeridian-crossing tiles in Web Mercator reprojection
Tiles near ±180° longitude cause RasterReprojector mesh refinement to diverge because forwardTo3857 maps nearby source coordinates on opposite sides of the antimeridian to EPSG:3857 x-values ~40M meters apart. Detect these tiles by checking if corner x-values in EPSG:3857 span more than half the world circumference, then wrap the projection functions to make the coordinate space continuous. Closes #366
1 parent 607ca07 commit 39cf0d5

1 file changed

Lines changed: 52 additions & 2 deletions

File tree

packages/deck.gl-geotiff/src/cog-layer.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ const TILE_SIZE = 512;
5353
*/
5454
const WEB_MERCATOR_METER_CIRCUMFERENCE = 40075016.686;
5555

56+
/**
57+
* Half the Web Mercator circumference in meters. Tiles whose corner x-values
58+
* span more than this in EPSG:3857 are assumed to cross the antimeridian.
59+
*/
60+
const HALF_CIRCUMFERENCE = WEB_MERCATOR_METER_CIRCUMFERENCE / 2;
61+
5662
/**
5763
* Scale factor for converting EPSG:3857 meters into deck.gl world units
5864
* (512×512).
@@ -463,11 +469,55 @@ export class COGLayer<
463469
};
464470
deckProjectionProps = {};
465471
} else {
472+
// Detect antimeridian-crossing tiles: project the four tile corners to
473+
// EPSG:3857 and check whether their x-values span more than half the
474+
// globe. When a tile straddles ±180° longitude, nearby source
475+
// coordinates on opposite sides of the antimeridian map to EPSG:3857
476+
// x-values ~40 million meters apart (+20M vs -20M). This discontinuity
477+
// causes RasterReprojector mesh refinement to diverge.
478+
//
479+
// The fix: shift negative x-values by +circumference so the tile's
480+
// EPSG:3857 coordinates are continuous (all positive). The inverse
481+
// function unwraps by subtracting the circumference for values beyond
482+
// the half-circumference.
483+
let tileForwardTo3857 = forwardTo3857;
484+
let tileInverseFrom3857 = inverseFrom3857;
485+
486+
const corners = [
487+
[0, 0],
488+
[width, 0],
489+
[width, height],
490+
[0, height],
491+
] as const;
492+
const cornerXs = corners.map(([cx, cy]) => {
493+
const [sx, sy] = forwardTransform(cx, cy);
494+
return forwardTo3857(sx, sy)[0];
495+
});
496+
const xMin = Math.min(...cornerXs);
497+
const xMax = Math.max(...cornerXs);
498+
499+
if (xMax - xMin > HALF_CIRCUMFERENCE) {
500+
tileForwardTo3857 = (x: number, y: number): [number, number] => {
501+
const [px, py] = forwardTo3857(x, y);
502+
return [
503+
px < 0 ? px + WEB_MERCATOR_METER_CIRCUMFERENCE : px,
504+
py,
505+
];
506+
};
507+
tileInverseFrom3857 = (x: number, y: number): [number, number] => {
508+
const unwrapped =
509+
x > HALF_CIRCUMFERENCE
510+
? x - WEB_MERCATOR_METER_CIRCUMFERENCE
511+
: x;
512+
return inverseFrom3857(unwrapped, y);
513+
};
514+
}
515+
466516
reprojectionFns = {
467517
forwardTransform,
468518
inverseTransform,
469-
forwardReproject: forwardTo3857,
470-
inverseReproject: inverseFrom3857,
519+
forwardReproject: tileForwardTo3857,
520+
inverseReproject: tileInverseFrom3857,
471521
};
472522
// Scale 3857 meters → deck.gl world units (512×512).
473523
//

0 commit comments

Comments
 (0)