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 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));
|
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]) {
|
function rgbToHex(rgb: [number, number, number]) {
|
||||||
const toHex = (v: number) => {
|
const toHex = (v: number) => {
|
||||||
const clamped = Math.max(0, Math.min(255, Math.round(v)));
|
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
|
OT: [139, 92, 246], // #8b5cf6
|
||||||
PS: [239, 68, 68], // #ef4444
|
PS: [239, 68, 68], // #ef4444
|
||||||
FC: [245, 158, 11], // #f59e0b
|
FC: [245, 158, 11], // #f59e0b
|
||||||
|
C21: [236, 72, 153], // #ec4899
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEPTH_DISABLED_PARAMS = {
|
const DEPTH_DISABLED_PARAMS = {
|
||||||
@ -558,6 +579,7 @@ export function Map3D({
|
|||||||
const showSeamarkRef = useRef(settings.showSeamark);
|
const showSeamarkRef = useRef(settings.showSeamark);
|
||||||
const baseMapRef = useRef<BaseMapId>(baseMap);
|
const baseMapRef = useRef<BaseMapId>(baseMap);
|
||||||
const projectionRef = useRef<MapProjectionId>(projection);
|
const projectionRef = useRef<MapProjectionId>(projection);
|
||||||
|
const globeShipIconLoadingRef = useRef(false);
|
||||||
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
||||||
|
|
||||||
const pulseMapSync = () => {
|
const pulseMapSync = () => {
|
||||||
@ -1176,32 +1198,71 @@ export function Map3D({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ensureImage = () => {
|
const ensureImage = () => {
|
||||||
|
const addFallbackImage = () => {
|
||||||
|
const size = 96;
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Simple top-down ship silhouette, pointing north.
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
ctx.fillStyle = "rgba(255,255,255,1)";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(size / 2, 6);
|
||||||
|
ctx.lineTo(size / 2 - 14, 24);
|
||||||
|
ctx.lineTo(size / 2 - 18, 58);
|
||||||
|
ctx.lineTo(size / 2 - 10, 88);
|
||||||
|
ctx.lineTo(size / 2 + 10, 88);
|
||||||
|
ctx.lineTo(size / 2 + 18, 58);
|
||||||
|
ctx.lineTo(size / 2 + 14, 24);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillRect(size / 2 - 8, 34, 16, 18);
|
||||||
|
|
||||||
|
const img = ctx.getImageData(0, 0, size, size);
|
||||||
|
map.addImage(imgId, img, { pixelRatio: 2, sdf: true });
|
||||||
|
kickRepaint(map);
|
||||||
|
};
|
||||||
|
|
||||||
if (map.hasImage(imgId)) return;
|
if (map.hasImage(imgId)) return;
|
||||||
const size = 96;
|
if (globeShipIconLoadingRef.current) return;
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = size;
|
|
||||||
canvas.height = size;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
// Simple top-down ship silhouette, pointing north.
|
try {
|
||||||
ctx.clearRect(0, 0, size, size);
|
globeShipIconLoadingRef.current = true;
|
||||||
ctx.fillStyle = "rgba(255,255,255,1)";
|
void map
|
||||||
ctx.beginPath();
|
.loadImage("/assets/ship.svg")
|
||||||
ctx.moveTo(size / 2, 6);
|
.then((response) => {
|
||||||
ctx.lineTo(size / 2 - 14, 24);
|
globeShipIconLoadingRef.current = false;
|
||||||
ctx.lineTo(size / 2 - 18, 58);
|
if (map.hasImage(imgId)) return;
|
||||||
ctx.lineTo(size / 2 - 10, 88);
|
|
||||||
ctx.lineTo(size / 2 + 10, 88);
|
|
||||||
ctx.lineTo(size / 2 + 18, 58);
|
|
||||||
ctx.lineTo(size / 2 + 14, 24);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.fillRect(size / 2 - 8, 34, 16, 18);
|
const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
|
||||||
|
if (!loadedImage) {
|
||||||
|
addFallbackImage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const img = ctx.getImageData(0, 0, size, size);
|
try {
|
||||||
map.addImage(imgId, img, { pixelRatio: 2, sdf: true });
|
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 = () => {
|
const ensure = () => {
|
||||||
@ -1228,8 +1289,11 @@ export function Map3D({
|
|||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: globeShipData.map((t) => {
|
features: globeShipData.map((t) => {
|
||||||
const legacy = legacyHits?.get(t.mmsi) ?? null;
|
const legacy = legacyHits?.get(t.mmsi) ?? null;
|
||||||
const cog = isFiniteNumber(t.cog) ? t.cog : 0;
|
const heading = getDisplayHeading({
|
||||||
const cogNorm = ((cog % 360) + 360) % 360;
|
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 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 sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||||
const selected = t.mmsi === selectedMmsi;
|
const selected = t.mmsi === selectedMmsi;
|
||||||
@ -1245,7 +1309,8 @@ export function Map3D({
|
|||||||
properties: {
|
properties: {
|
||||||
mmsi: t.mmsi,
|
mmsi: t.mmsi,
|
||||||
name: t.name || "",
|
name: t.name || "",
|
||||||
cog: cogNorm,
|
cog: heading,
|
||||||
|
heading,
|
||||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||||
shipColor: getGlobeShipColor({
|
shipColor: getGlobeShipColor({
|
||||||
selected,
|
selected,
|
||||||
@ -1379,7 +1444,7 @@ export function Map3D({
|
|||||||
"icon-allow-overlap": true,
|
"icon-allow-overlap": true,
|
||||||
"icon-ignore-placement": true,
|
"icon-ignore-placement": true,
|
||||||
"icon-anchor": "center",
|
"icon-anchor": "center",
|
||||||
"icon-rotate": ["to-number", ["get", "cog"], 0],
|
"icon-rotate": ["to-number", ["get", "heading"], 0],
|
||||||
// Keep the icon on the sea surface.
|
// Keep the icon on the sea surface.
|
||||||
"icon-rotation-alignment": "map",
|
"icon-rotation-alignment": "map",
|
||||||
"icon-pitch-alignment": "map",
|
"icon-pitch-alignment": "map",
|
||||||
@ -1927,7 +1992,11 @@ export function Map3D({
|
|||||||
getIcon: () => "ship",
|
getIcon: () => "ship",
|
||||||
getPosition: (d) =>
|
getPosition: (d) =>
|
||||||
[d.lon, d.lat] as [number, number],
|
[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",
|
sizeUnits: "pixels",
|
||||||
getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? FLAT_SHIP_ICON_SIZE_SELECTED : FLAT_SHIP_ICON_SIZE),
|
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),
|
getColor: (d) => getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null),
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user