fix(map): align prediction vectors with ship course + improve contrast

This commit is contained in:
htlee 2026-02-15 19:41:15 +09:00
부모 11aff67a04
커밋 a8aa916076

파일 보기

@ -370,6 +370,15 @@ function normalizeAngleDeg(value: number, offset = 0): number {
return ((v % 360) + 360) % 360;
}
function toValidBearingDeg(value: unknown): number | null {
if (typeof value !== "number" || !Number.isFinite(value)) return null;
// AIS heading uses 511 as "not available". Some feeds may also use 360 as "not available".
if (value === 511) return null;
if (value < 0) return null;
if (value >= 360) return null;
return value;
}
function getDisplayHeading({
cog,
heading,
@ -379,8 +388,8 @@ function getDisplayHeading({
heading: number | null | undefined;
offset?: number;
}) {
const raw =
isFiniteNumber(heading) && heading >= 0 && heading <= 360 && heading !== 511 ? heading : isFiniteNumber(cog) ? cog : 0;
// Use COG (0=N, 90=E...) as the primary bearing so ship icons + prediction vectors stay aligned.
const raw = toValidBearingDeg(cog) ?? toValidBearingDeg(heading) ?? 0;
return normalizeAngleDeg(raw, offset);
}
@ -1329,16 +1338,18 @@ export function Map3D({
const fleetFocusLat = fleetFocus?.center?.[1];
const fleetFocusZoom = fleetFocus?.zoom;
const reorderGlobeFeatureLayers = useCallback(() => {
const map = mapRef.current;
if (!map || projectionRef.current !== "globe") return;
if (projectionBusyRef.current) return;
const reorderGlobeFeatureLayers = useCallback(() => {
const map = mapRef.current;
if (!map || projectionRef.current !== "globe") return;
if (projectionBusyRef.current) return;
const ordering = [
"zones-fill",
"zones-line",
"zones-label",
"predict-vectors-outline",
"predict-vectors",
"predict-vectors-hl-outline",
"predict-vectors-hl",
"ships-globe-halo",
"ships-globe-outline",
@ -2457,7 +2468,9 @@ export function Map3D({
if (!map) return;
const srcId = "predict-vectors-src";
const outlineId = "predict-vectors-outline";
const lineId = "predict-vectors";
const hlOutlineId = "predict-vectors-hl-outline";
const hlId = "predict-vectors-hl";
const ensure = () => {
@ -2480,20 +2493,21 @@ export function Map3D({
if (!isTarget && !isSelected && !isPinnedHighlight) continue;
const sog = isFiniteNumber(t.sog) ? t.sog : null;
const cog =
isFiniteNumber(t.cog) ? t.cog : isFiniteNumber(t.heading) ? t.heading : null;
if (sog == null || cog == null) continue;
const bearing = toValidBearingDeg(t.cog) ?? toValidBearingDeg(t.heading);
if (sog == null || bearing == null) continue;
if (sog < 0.2) continue;
const distM = sog * metersPerSecondPerKnot * horizonSeconds;
if (!Number.isFinite(distM) || distM <= 0) continue;
const to = destinationPointLngLat([t.lon, t.lat], cog, distM);
const to = destinationPointLngLat([t.lon, t.lat], bearing, distM);
const rgb = isTarget
const baseRgb = isTarget
? LEGACY_CODE_COLORS_RGB[legacy?.shipCode ?? ""] ?? OTHER_AIS_SPEED_RGB.moving
: OTHER_AIS_SPEED_RGB.moving;
const alpha = isTarget ? 0.48 : 0.28;
const rgb = lightenColor(baseRgb, isTarget ? 0.55 : 0.62);
const alpha = isTarget ? 0.72 : 0.52;
const alphaHl = isTarget ? 0.92 : 0.84;
const hl = isSelected || isPinnedHighlight ? 1 : 0;
features.push({
@ -2504,10 +2518,11 @@ export function Map3D({
mmsi: t.mmsi,
minutes: horizonMinutes,
sog,
cog,
cog: bearing,
target: isTarget ? 1 : 0,
hl,
color: rgbaCss(rgb, alpha),
colorHl: rgbaCss(rgb, alphaHl),
},
});
}
@ -2524,7 +2539,7 @@ export function Map3D({
return;
}
const ensureLayer = (id: string, paint: LayerSpecification["paint"], filter?: unknown[]) => {
const ensureLayer = (id: string, paint: LayerSpecification["paint"], filter: unknown[]) => {
if (!map.getLayer(id)) {
try {
map.addLayer(
@ -2532,7 +2547,7 @@ export function Map3D({
id,
type: "line",
source: srcId,
...(filter ? { filter: filter as never } : {}),
filter: filter as never,
layout: {
visibility,
"line-cap": "round",
@ -2548,30 +2563,63 @@ export function Map3D({
} else {
try {
map.setLayoutProperty(id, "visibility", visibility);
map.setFilter(id, filter as never);
if (paint && typeof paint === "object") {
for (const [key, value] of Object.entries(paint)) {
map.setPaintProperty(id, key as never, value as never);
}
}
} catch {
// ignore
}
}
};
const baseFilter = ["==", ["to-number", ["get", "hl"], 0], 0] as unknown as unknown[];
const hlFilter = ["==", ["to-number", ["get", "hl"], 0], 1] as unknown as unknown[];
// Outline (halo) for readability over bathymetry + seamark textures.
ensureLayer(
outlineId,
{
"line-color": "rgba(2,6,23,0.86)",
"line-width": 4.8,
"line-opacity": 1,
"line-blur": 0.2,
"line-dasharray": [1.2, 1.8] as never,
} as never,
baseFilter,
);
ensureLayer(
lineId,
{
"line-color": ["coalesce", ["get", "color"], "rgba(148,163,184,0.3)"] as never,
"line-width": 1.2,
"line-color": ["coalesce", ["get", "color"], "rgba(226,232,240,0.62)"] as never,
"line-width": 2.4,
"line-opacity": 1,
"line-dasharray": [1.2, 1.8] as never,
} as never,
baseFilter,
);
ensureLayer(
hlOutlineId,
{
"line-color": "rgba(2,6,23,0.92)",
"line-width": 6.4,
"line-opacity": 1,
"line-blur": 0.25,
"line-dasharray": [1.2, 1.8] as never,
} as never,
hlFilter,
);
ensureLayer(
hlId,
{
"line-color": ["coalesce", ["get", "color"], "rgba(226,232,240,0.7)"] as never,
"line-width": 2.2,
"line-color": ["coalesce", ["get", "colorHl"], ["get", "color"], "rgba(241,245,249,0.92)"] as never,
"line-width": 3.6,
"line-opacity": 1,
"line-dasharray": [1.2, 1.8] as never,
} as never,
["==", ["to-number", ["get", "hl"], 0], 1] as unknown as unknown[],
hlFilter,
);
reorderGlobeFeatureLayers();