fix: stabilize globe projection loading and globe ship icon fallback

This commit is contained in:
htlee 2026-02-15 16:12:10 +09:00
부모 05b0c6b881
커밋 5b7d1c4331

파일 보기

@ -606,6 +606,44 @@ function makeGlobeCircleRadiusExpr() {
const GLOBE_SHIP_CIRCLE_RADIUS_EXPR = makeGlobeCircleRadiusExpr() as never;
function buildFallbackGlobeShipIcon() {
const size = 96;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) return null;
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);
return ctx.getImageData(0, 0, size, size);
}
function ensureFallbackShipImage(map: maplibregl.Map, imageId: string) {
if (!map || map.hasImage(imageId)) return;
const image = buildFallbackGlobeShipIcon();
if (!image) return;
try {
map.addImage(imageId, image, { pixelRatio: 2, sdf: true });
} catch {
// ignore
}
}
function getMapTilerKey(): string | null {
const k = import.meta.env.VITE_MAPTILER_KEY;
if (typeof k !== "string") return null;
@ -1051,6 +1089,7 @@ export function Map3D({
const projectionRef = useRef<MapProjectionId>(projection);
const globeShipIconLoadingRef = useRef(false);
const projectionBusyRef = useRef(false);
const projectionBusyTokenRef = useRef(0);
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
const projectionPrevRef = useRef<MapProjectionId>(projection);
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
@ -1344,28 +1383,37 @@ export function Map3D({
projectionBusyTimerRef.current = null;
}, []);
const endProjectionLoading = useCallback(() => {
if (!projectionBusyRef.current) return;
projectionBusyRef.current = false;
clearProjectionBusyTimer();
if (onProjectionLoadingChange) {
onProjectionLoadingChange(false);
}
}, [clearProjectionBusyTimer, onProjectionLoadingChange]);
const setProjectionLoading = useCallback(
(loading: boolean) => {
if (projectionBusyRef.current === loading) return;
projectionBusyRef.current = loading;
if (loading) {
clearProjectionBusyTimer();
projectionBusyTimerRef.current = setTimeout(() => {
if (projectionBusyRef.current) {
setProjectionLoading(false);
console.warn("Projection loading fallback timeout reached.");
}
}, 3000);
} else {
clearProjectionBusyTimer();
if (!loading) {
endProjectionLoading();
return;
}
clearProjectionBusyTimer();
projectionBusyRef.current = true;
const token = ++projectionBusyTokenRef.current;
if (onProjectionLoadingChange) {
onProjectionLoadingChange(loading);
onProjectionLoadingChange(true);
}
projectionBusyTimerRef.current = setTimeout(() => {
if (!projectionBusyRef.current || projectionBusyTokenRef.current !== token) return;
console.debug("Projection loading fallback timeout reached.");
endProjectionLoading();
}, 4000);
},
[onProjectionLoadingChange, clearProjectionBusyTimer],
[clearProjectionBusyTimer, endProjectionLoading, onProjectionLoadingChange],
);
const pulseMapSync = () => {
@ -1379,11 +1427,9 @@ export function Map3D({
useEffect(() => {
return () => {
clearProjectionBusyTimer();
if (projectionBusyRef.current) {
setProjectionLoading(false);
}
endProjectionLoading();
};
}, [clearProjectionBusyTimer, setProjectionLoading]);
}, [clearProjectionBusyTimer, endProjectionLoading]);
useEffect(() => {
showSeamarkRef.current = settings.showSeamark;
@ -2167,45 +2213,29 @@ export function Map3D({
};
const ensureImage = () => {
ensureFallbackShipImage(map, imgId);
if (globeShipIconLoadingRef.current) return;
if (map.hasImage(imgId)) return;
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 });
ensureFallbackShipImage(map, imgId);
kickRepaint(map);
};
if (map.hasImage(imgId)) return;
if (globeShipIconLoadingRef.current) return;
let fallbackTimer: ReturnType<typeof window.setTimeout> | null = null;
try {
globeShipIconLoadingRef.current = true;
fallbackTimer = window.setTimeout(() => {
addFallbackImage();
}, 80);
void map
.loadImage("/assets/ship.svg")
.then((response) => {
globeShipIconLoadingRef.current = false;
if (map.hasImage(imgId)) return;
if (fallbackTimer != null) {
clearTimeout(fallbackTimer);
fallbackTimer = null;
}
const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
if (!loadedImage) {
@ -2214,6 +2244,13 @@ export function Map3D({
}
try {
if (map.hasImage(imgId)) {
try {
map.removeImage(imgId);
} catch {
// ignore
}
}
map.addImage(imgId, loadedImage, { pixelRatio: 2, sdf: true });
kickRepaint(map);
} catch (e) {
@ -2222,10 +2259,18 @@ export function Map3D({
})
.catch(() => {
globeShipIconLoadingRef.current = false;
if (fallbackTimer != null) {
clearTimeout(fallbackTimer);
fallbackTimer = null;
}
addFallbackImage();
});
} catch (e) {
globeShipIconLoadingRef.current = false;
if (fallbackTimer != null) {
clearTimeout(fallbackTimer);
fallbackTimer = null;
}
try {
addFallbackImage();
} catch (fallbackError) {