fix(tgu-ops-ui): fix antimeridian wrapping for request and swath polygons on 2D map

Polygons crossing the antimeridian were rendered with a large visual jump.
Now each polygon is drawn three times (dx = 0, ±worldSize) after unwrapping
X-coordinates, with viewport culling to skip invisible copies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Дмитрий Соловьев
2026-06-01 08:41:49 +03:00
parent d7dda1e2f5
commit da753b4ca9
2 changed files with 73 additions and 25 deletions
@@ -8,33 +8,53 @@ const SURVEY_COLORS: Record<string, string> = {
}; };
const DEFAULT_COLOR = "#FFA040"; const DEFAULT_COLOR = "#FFA040";
type Pt = { x: number; y: number };
function unwrapX(points: Pt[], worldSize: number): Pt[] {
if (points.length === 0) return [];
const out: Pt[] = [points[0]];
let prevX = points[0].x;
for (let i = 1; i < points.length; i++) {
let x = points[i].x;
while (x - prevX > worldSize / 2) x -= worldSize;
while (prevX - x > worldSize / 2) x += worldSize;
out.push({ x, y: points[i].y });
prevX = x;
}
return out;
}
export function drawRequests( export function drawRequests(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
polygons: MapPolygon[], polygons: MapPolygon[],
projection: MapProjection, projection: MapProjection,
_viewport: { width: number; height: number } viewport: { width: number; height: number }
) { ) {
ctx.save(); ctx.save();
for (const polygon of polygons) { for (const polygon of polygons) {
if (polygon.points.length < 3) continue; if (polygon.points.length < 3) continue;
const screenPoints = polygon.points.map((point) => projection.project(point)); const raw = polygon.points.map((p) => projection.project(p));
const pts = unwrapX(raw, projection.worldSize);
const color = SURVEY_COLORS[polygon.surveyType ?? ""] ?? DEFAULT_COLOR; const color = SURVEY_COLORS[polygon.surveyType ?? ""] ?? DEFAULT_COLOR;
ctx.beginPath();
screenPoints.forEach((point, index) => {
if (index === 0) {
ctx.moveTo(point.x, point.y);
} else {
ctx.lineTo(point.x, point.y);
}
});
ctx.closePath();
ctx.fillStyle = hexToRgba(color, 0.18); ctx.fillStyle = hexToRgba(color, 0.18);
ctx.strokeStyle = hexToRgba(color, 0.85); ctx.strokeStyle = hexToRgba(color, 0.85);
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]); ctx.setLineDash([4, 3]);
for (const dx of [0, projection.worldSize, -projection.worldSize]) {
const visible = pts.some((p) => {
const x = p.x + dx;
return x >= -64 && x <= viewport.width + 64 && p.y >= -64 && p.y <= viewport.height + 64;
});
if (!visible) continue;
ctx.beginPath();
for (let i = 0; i < pts.length; i++) {
if (i === 0) ctx.moveTo(pts[i].x + dx, pts[i].y);
else ctx.lineTo(pts[i].x + dx, pts[i].y);
}
ctx.closePath();
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
}
ctx.setLineDash([]); ctx.setLineDash([]);
} }
ctx.restore(); ctx.restore();
@@ -1,6 +1,38 @@
import type { MapProjection } from "../geometry/mapProjection"; import type { MapProjection } from "../geometry/mapProjection";
import type { MapPolygon } from "../model/mapTypes"; import type { MapPolygon } from "../model/mapTypes";
type Pt = { x: number; y: number };
function unwrapX(points: Pt[], worldSize: number): Pt[] {
if (points.length === 0) return [];
const out: Pt[] = [points[0]];
let prevX = points[0].x;
for (let i = 1; i < points.length; i++) {
let x = points[i].x;
while (x - prevX > worldSize / 2) x -= worldSize;
while (prevX - x > worldSize / 2) x += worldSize;
out.push({ x, y: points[i].y });
prevX = x;
}
return out;
}
function inView(points: Pt[], dx: number, vw: number, vh: number): boolean {
return points.some((p) => {
const x = p.x + dx;
return x >= -64 && x <= vw + 64 && p.y >= -64 && p.y <= vh + 64;
});
}
function tracePath(ctx: CanvasRenderingContext2D, points: Pt[], dx: number) {
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
if (i === 0) ctx.moveTo(points[i].x + dx, points[i].y);
else ctx.lineTo(points[i].x + dx, points[i].y);
}
ctx.closePath();
}
export function drawSwath( export function drawSwath(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
polygons: MapPolygon[], polygons: MapPolygon[],
@@ -13,18 +45,14 @@ export function drawSwath(
ctx.lineWidth = 1; ctx.lineWidth = 1;
for (const polygon of polygons) { for (const polygon of polygons) {
if (polygon.points.length < 3) continue; if (polygon.points.length < 3) continue;
const screenPoints = polygon.points.map((point) => projection.project(point)); const raw = polygon.points.map((p) => projection.project(p));
if (!screenPoints.some((p) => p.x >= -32 && p.x <= viewport.width + 32 && p.y >= -32 && p.y <= viewport.height + 32)) { const pts = unwrapX(raw, projection.worldSize);
continue; for (const dx of [0, projection.worldSize, -projection.worldSize]) {
} if (!inView(pts, dx, viewport.width, viewport.height)) continue;
ctx.beginPath(); tracePath(ctx, pts, dx);
screenPoints.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
});
ctx.closePath();
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
} }
}
ctx.restore(); ctx.restore();
} }