fix: stabilize globe projection loading and globe ship icon fallback
This commit is contained in:
부모
05b0c6b881
커밋
5b7d1c4331
@ -606,6 +606,44 @@ function makeGlobeCircleRadiusExpr() {
|
|||||||
|
|
||||||
const GLOBE_SHIP_CIRCLE_RADIUS_EXPR = makeGlobeCircleRadiusExpr() as never;
|
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 {
|
function getMapTilerKey(): string | null {
|
||||||
const k = import.meta.env.VITE_MAPTILER_KEY;
|
const k = import.meta.env.VITE_MAPTILER_KEY;
|
||||||
if (typeof k !== "string") return null;
|
if (typeof k !== "string") return null;
|
||||||
@ -1051,6 +1089,7 @@ export function Map3D({
|
|||||||
const projectionRef = useRef<MapProjectionId>(projection);
|
const projectionRef = useRef<MapProjectionId>(projection);
|
||||||
const globeShipIconLoadingRef = useRef(false);
|
const globeShipIconLoadingRef = useRef(false);
|
||||||
const projectionBusyRef = useRef(false);
|
const projectionBusyRef = useRef(false);
|
||||||
|
const projectionBusyTokenRef = useRef(0);
|
||||||
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
||||||
const projectionPrevRef = useRef<MapProjectionId>(projection);
|
const projectionPrevRef = useRef<MapProjectionId>(projection);
|
||||||
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
|
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
|
||||||
@ -1344,28 +1383,37 @@ export function Map3D({
|
|||||||
projectionBusyTimerRef.current = null;
|
projectionBusyTimerRef.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const endProjectionLoading = useCallback(() => {
|
||||||
|
if (!projectionBusyRef.current) return;
|
||||||
|
projectionBusyRef.current = false;
|
||||||
|
clearProjectionBusyTimer();
|
||||||
|
if (onProjectionLoadingChange) {
|
||||||
|
onProjectionLoadingChange(false);
|
||||||
|
}
|
||||||
|
}, [clearProjectionBusyTimer, onProjectionLoadingChange]);
|
||||||
|
|
||||||
const setProjectionLoading = useCallback(
|
const setProjectionLoading = useCallback(
|
||||||
(loading: boolean) => {
|
(loading: boolean) => {
|
||||||
if (projectionBusyRef.current === loading) return;
|
if (projectionBusyRef.current === loading) return;
|
||||||
projectionBusyRef.current = loading;
|
if (!loading) {
|
||||||
|
endProjectionLoading();
|
||||||
if (loading) {
|
return;
|
||||||
clearProjectionBusyTimer();
|
|
||||||
projectionBusyTimerRef.current = setTimeout(() => {
|
|
||||||
if (projectionBusyRef.current) {
|
|
||||||
setProjectionLoading(false);
|
|
||||||
console.warn("Projection loading fallback timeout reached.");
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
clearProjectionBusyTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearProjectionBusyTimer();
|
||||||
|
projectionBusyRef.current = true;
|
||||||
|
const token = ++projectionBusyTokenRef.current;
|
||||||
if (onProjectionLoadingChange) {
|
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 = () => {
|
const pulseMapSync = () => {
|
||||||
@ -1379,11 +1427,9 @@ export function Map3D({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
clearProjectionBusyTimer();
|
clearProjectionBusyTimer();
|
||||||
if (projectionBusyRef.current) {
|
endProjectionLoading();
|
||||||
setProjectionLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [clearProjectionBusyTimer, setProjectionLoading]);
|
}, [clearProjectionBusyTimer, endProjectionLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showSeamarkRef.current = settings.showSeamark;
|
showSeamarkRef.current = settings.showSeamark;
|
||||||
@ -2167,45 +2213,29 @@ export function Map3D({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ensureImage = () => {
|
const ensureImage = () => {
|
||||||
|
ensureFallbackShipImage(map, imgId);
|
||||||
|
if (globeShipIconLoadingRef.current) return;
|
||||||
|
if (map.hasImage(imgId)) return;
|
||||||
|
|
||||||
const addFallbackImage = () => {
|
const addFallbackImage = () => {
|
||||||
const size = 96;
|
ensureFallbackShipImage(map, imgId);
|
||||||
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);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (map.hasImage(imgId)) return;
|
let fallbackTimer: ReturnType<typeof window.setTimeout> | null = null;
|
||||||
if (globeShipIconLoadingRef.current) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
globeShipIconLoadingRef.current = true;
|
globeShipIconLoadingRef.current = true;
|
||||||
|
fallbackTimer = window.setTimeout(() => {
|
||||||
|
addFallbackImage();
|
||||||
|
}, 80);
|
||||||
void map
|
void map
|
||||||
.loadImage("/assets/ship.svg")
|
.loadImage("/assets/ship.svg")
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
globeShipIconLoadingRef.current = false;
|
globeShipIconLoadingRef.current = false;
|
||||||
if (map.hasImage(imgId)) return;
|
if (fallbackTimer != null) {
|
||||||
|
clearTimeout(fallbackTimer);
|
||||||
|
fallbackTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
|
const loadedImage = (response as { data?: HTMLImageElement | ImageBitmap } | undefined)?.data;
|
||||||
if (!loadedImage) {
|
if (!loadedImage) {
|
||||||
@ -2214,6 +2244,13 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (map.hasImage(imgId)) {
|
||||||
|
try {
|
||||||
|
map.removeImage(imgId);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
map.addImage(imgId, loadedImage, { pixelRatio: 2, sdf: true });
|
map.addImage(imgId, loadedImage, { pixelRatio: 2, sdf: true });
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -2222,10 +2259,18 @@ export function Map3D({
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
globeShipIconLoadingRef.current = false;
|
globeShipIconLoadingRef.current = false;
|
||||||
|
if (fallbackTimer != null) {
|
||||||
|
clearTimeout(fallbackTimer);
|
||||||
|
fallbackTimer = null;
|
||||||
|
}
|
||||||
addFallbackImage();
|
addFallbackImage();
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
globeShipIconLoadingRef.current = false;
|
globeShipIconLoadingRef.current = false;
|
||||||
|
if (fallbackTimer != null) {
|
||||||
|
clearTimeout(fallbackTimer);
|
||||||
|
fallbackTimer = null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
addFallbackImage();
|
addFallbackImage();
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user