fix(map3d): align globe ship icon rendering and heading
This commit is contained in:
부모
1225d5c54c
커밋
2514591703
@ -137,9 +137,29 @@ function extractProjectionType(map: maplibregl.Map): MapProjectionId | undefined
|
||||
}
|
||||
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
const GLOBE_ICON_HEADING_OFFSET_DEG = -90;
|
||||
|
||||
const clampNumber = (value: number, minValue: number, maxValue: number) => Math.max(minValue, Math.min(maxValue, value));
|
||||
|
||||
function normalizeAngleDeg(value: number, offset = 0): number {
|
||||
const v = value + offset;
|
||||
return ((v % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
function getDisplayHeading({
|
||||
cog,
|
||||
heading,
|
||||
offset = 0,
|
||||
}: {
|
||||
cog: number | null | undefined;
|
||||
heading: number | null | undefined;
|
||||
offset?: number;
|
||||
}) {
|
||||
const raw =
|
||||
isFiniteNumber(heading) && heading >= 0 && heading <= 360 && heading !== 511 ? heading : isFiniteNumber(cog) ? cog : 0;
|
||||
return normalizeAngleDeg(raw, offset);
|
||||
}
|
||||
|
||||
function rgbToHex(rgb: [number, number, number]) {
|
||||
const toHex = (v: number) => {
|
||||
const clamped = Math.max(0, Math.min(255, Math.round(v)));
|
||||
@ -182,6 +202,7 @@ const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
||||
OT: [139, 92, 246], // #8b5cf6
|
||||
PS: [239, 68, 68], // #ef4444
|
||||
FC: [245, 158, 11], // #f59e0b
|
||||
C21: [236, 72, 153], // #ec4899
|
||||
};
|
||||
|
||||
const DEPTH_DISABLED_PARAMS = {
|
||||
@ -558,6 +579,7 @@ export function Map3D({
|
||||
const showSeamarkRef = useRef(settings.showSeamark);
|
||||
const baseMapRef = useRef<BaseMapId>(baseMap);
|
||||
const projectionRef = useRef<MapProjectionId>(projection);
|
||||
const globeShipIconLoadingRef = useRef(false);
|
||||
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
||||
|
||||
const pulseMapSync = () => {
|
||||
@ -1176,7 +1198,7 @@ export function Map3D({
|
||||
};
|
||||
|
||||
const ensureImage = () => {
|
||||
if (map.hasImage(imgId)) return;
|
||||
const addFallbackImage = () => {
|
||||
const size = 96;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
@ -1202,6 +1224,45 @@ export function Map3D({
|
||||
|
||||
const img = ctx.getImageData(0, 0, size, size);
|
||||
map.addImage(imgId, img, { pixelRatio: 2, sdf: true });
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
if (map.hasImage(imgId)) return;
|
||||
if (globeShipIconLoadingRef.current) return;
|
||||
|
||||
try {
|
||||
globeShipIconLoadingRef.current = true;
|
||||
void map
|
||||
.loadImage("/assets/ship.svg")
|
||||
.then((response) => {
|
||||
globeShipIconLoadingRef.current = false;
|
||||
if (map.hasImage(imgId)) return;
|
||||
|
||||
const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
|
||||
if (!loadedImage) {
|
||||
addFallbackImage();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
map.addImage(imgId, loadedImage, { pixelRatio: 2, sdf: true });
|
||||
kickRepaint(map);
|
||||
} catch (e) {
|
||||
console.warn("Ship icon image add failed:", e);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
globeShipIconLoadingRef.current = false;
|
||||
addFallbackImage();
|
||||
});
|
||||
} catch (e) {
|
||||
globeShipIconLoadingRef.current = false;
|
||||
try {
|
||||
addFallbackImage();
|
||||
} catch (fallbackError) {
|
||||
console.warn("Ship icon image setup failed:", e, fallbackError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ensure = () => {
|
||||
@ -1228,8 +1289,11 @@ export function Map3D({
|
||||
type: "FeatureCollection",
|
||||
features: globeShipData.map((t) => {
|
||||
const legacy = legacyHits?.get(t.mmsi) ?? null;
|
||||
const cog = isFiniteNumber(t.cog) ? t.cog : 0;
|
||||
const cogNorm = ((cog % 360) + 360) % 360;
|
||||
const heading = getDisplayHeading({
|
||||
cog: t.cog,
|
||||
heading: t.heading,
|
||||
offset: GLOBE_ICON_HEADING_OFFSET_DEG,
|
||||
});
|
||||
const hull = clampNumber((isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3, 50, 420);
|
||||
const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||
const selected = t.mmsi === selectedMmsi;
|
||||
@ -1245,7 +1309,8 @@ export function Map3D({
|
||||
properties: {
|
||||
mmsi: t.mmsi,
|
||||
name: t.name || "",
|
||||
cog: cogNorm,
|
||||
cog: heading,
|
||||
heading,
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||
shipColor: getGlobeShipColor({
|
||||
selected,
|
||||
@ -1379,7 +1444,7 @@ export function Map3D({
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
"icon-anchor": "center",
|
||||
"icon-rotate": ["to-number", ["get", "cog"], 0],
|
||||
"icon-rotate": ["to-number", ["get", "heading"], 0],
|
||||
// Keep the icon on the sea surface.
|
||||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "map",
|
||||
@ -1927,7 +1992,11 @@ export function Map3D({
|
||||
getIcon: () => "ship",
|
||||
getPosition: (d) =>
|
||||
[d.lon, d.lat] as [number, number],
|
||||
getAngle: (d) => (isFiniteNumber(d.cog) ? d.cog : 0),
|
||||
getAngle: (d) =>
|
||||
getDisplayHeading({
|
||||
cog: d.cog,
|
||||
heading: d.heading,
|
||||
}),
|
||||
sizeUnits: "pixels",
|
||||
getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? FLAT_SHIP_ICON_SIZE_SELECTED : FLAT_SHIP_ICON_SIZE),
|
||||
getColor: (d) => getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null),
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user