From da753b4ca98844f155892088626cc43f1ae8f576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D1=8C=D0=B5=D0=B2?= Date: Mon, 1 Jun 2026 08:41:49 +0300 Subject: [PATCH] fix(tgu-ops-ui): fix antimeridian wrapping for request and swath polygons on 2D map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tgu-map-2d/canvas/drawRequests.ts | 48 ++++++++++++------ .../features/tgu-map-2d/canvas/drawSwath.ts | 50 +++++++++++++++---- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawRequests.ts b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawRequests.ts index 1839716..59671b6 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawRequests.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawRequests.ts @@ -8,33 +8,53 @@ const SURVEY_COLORS: Record = { }; 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( ctx: CanvasRenderingContext2D, polygons: MapPolygon[], projection: MapProjection, - _viewport: { width: number; height: number } + viewport: { width: number; height: number } ) { ctx.save(); for (const polygon of polygons) { 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; - 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.strokeStyle = hexToRgba(color, 0.85); ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); - ctx.fill(); - ctx.stroke(); + 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.stroke(); + } ctx.setLineDash([]); } ctx.restore(); diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawSwath.ts b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawSwath.ts index 2a5125a..c2dee16 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawSwath.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawSwath.ts @@ -1,6 +1,38 @@ import type { MapProjection } from "../geometry/mapProjection"; 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( ctx: CanvasRenderingContext2D, polygons: MapPolygon[], @@ -13,18 +45,14 @@ export function drawSwath( ctx.lineWidth = 1; for (const polygon of polygons) { if (polygon.points.length < 3) continue; - const screenPoints = polygon.points.map((point) => projection.project(point)); - if (!screenPoints.some((p) => p.x >= -32 && p.x <= viewport.width + 32 && p.y >= -32 && p.y <= viewport.height + 32)) { - continue; + const raw = polygon.points.map((p) => projection.project(p)); + const pts = unwrapX(raw, projection.worldSize); + for (const dx of [0, projection.worldSize, -projection.worldSize]) { + if (!inView(pts, dx, viewport.width, viewport.height)) continue; + tracePath(ctx, pts, dx); + ctx.fill(); + ctx.stroke(); } - ctx.beginPath(); - 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.stroke(); } ctx.restore(); }