pcp-tgu-ops-ui: маршруты съёмки на карте — фикс антимеридиана
drawPlanWorks проецировал точки напрямую и проверял видимость по одной копии мира: полигон, пересекающий 180°, рисовался через всю карту, а слой мигал «то есть, то нет» при зуме/панораме. Перенёс паттерн из drawRequests/drawSwath: unwrapX (склейка контура через шов) + отрисовка в трёх копиях мира с culling по каждой.
This commit is contained in:
@@ -2,6 +2,26 @@ import { statusStyle } from "../../tgu-planning/tguStatus";
|
|||||||
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 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* «Разворачивает» X-координаты так, чтобы соседние точки не расходились больше чем на
|
||||||
|
* полмира — иначе полигон, пересекающий антимеридиан (180°), рисуется через всю карту.
|
||||||
|
*/
|
||||||
|
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 drawPlanWorks(
|
export function drawPlanWorks(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
polygons: MapPolygon[],
|
polygons: MapPolygon[],
|
||||||
@@ -11,26 +31,31 @@ export function drawPlanWorks(
|
|||||||
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((point) => projection.project(point));
|
||||||
if (!screenPoints.some((point) => point.x >= -32 && point.x <= viewport.width + 32 && point.y >= -32 && point.y <= viewport.height + 32)) {
|
const pts = unwrapX(raw, projection.worldSize);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = statusStyle(polygon.status || "PLANNED").color;
|
const color = statusStyle(polygon.status || "PLANNED").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.2);
|
ctx.fillStyle = hexToRgba(color, 0.2);
|
||||||
ctx.strokeStyle = hexToRgba(color, 0.72);
|
ctx.strokeStyle = hexToRgba(color, 0.72);
|
||||||
ctx.lineWidth = 1.2;
|
ctx.lineWidth = 1.2;
|
||||||
ctx.fill();
|
|
||||||
ctx.stroke();
|
// Рисуем полигон в трёх копиях мира (текущей и соседних), чтобы он был виден
|
||||||
|
// независимо от того, какая копия попала в видимую область при зуме/панораме.
|
||||||
|
for (const dx of [0, projection.worldSize, -projection.worldSize]) {
|
||||||
|
const visible = pts.some((point) => {
|
||||||
|
const x = point.x + dx;
|
||||||
|
return x >= -32 && x <= viewport.width + 32 && point.y >= -32 && point.y <= viewport.height + 32;
|
||||||
|
});
|
||||||
|
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.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user