feat(map3d): add projection mode transition loading overlay
This commit is contained in:
부모
2514591703
커밋
f745bb16d7
@ -596,6 +596,80 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.map-loader-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 950;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(2, 6, 23, 0.42);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.map-loader-overlay__panel {
|
||||
width: min(72vw, 320px);
|
||||
background: rgba(15, 23, 42, 0.94);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.map-loader-overlay__spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 3px solid rgba(148, 163, 184, 0.28);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: map-loader-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
.map-loader-overlay__text {
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.map-loader-overlay__bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.2);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-loader-overlay__fill {
|
||||
width: 28%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--accent);
|
||||
animation: map-loader-fill 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes map-loader-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes map-loader-fill {
|
||||
0% {
|
||||
transform: translateX(-40%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(220%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-40%);
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
|
||||
@ -115,6 +115,8 @@ export function DashboardPage() {
|
||||
showSeamark: false,
|
||||
});
|
||||
|
||||
const [isProjectionLoading, setIsProjectionLoading] = useState(false);
|
||||
|
||||
const [clock, setClock] = useState(() => new Date().toLocaleString("ko-KR", { hour12: false }));
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => setClock(new Date().toLocaleString("ko-KR", { hour12: false })), 1000);
|
||||
@ -472,6 +474,17 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="map-area">
|
||||
{isProjectionLoading ? (
|
||||
<div className="map-loader-overlay" role="status" aria-live="polite">
|
||||
<div className="map-loader-overlay__panel">
|
||||
<div className="map-loader-overlay__spinner" />
|
||||
<div className="map-loader-overlay__text">지도 모드 동기화 중...</div>
|
||||
<div className="map-loader-overlay__bar">
|
||||
<div className="map-loader-overlay__fill" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<Map3D
|
||||
targets={targetsForMap}
|
||||
zones={zones}
|
||||
@ -486,6 +499,7 @@ export function DashboardPage() {
|
||||
pairLinks={pairLinksForMap}
|
||||
fcLinks={fcLinksForMap}
|
||||
fleetCircles={fleetCirclesForMap}
|
||||
onProjectionLoadingChange={setIsProjectionLoading}
|
||||
/>
|
||||
<MapLegend />
|
||||
{selectedLegacyVessel ? (
|
||||
|
||||
@ -42,6 +42,7 @@ type Props = {
|
||||
pairLinks?: PairLink[];
|
||||
fcLinks?: FcLink[];
|
||||
fleetCircles?: FleetCircle[];
|
||||
onProjectionLoadingChange?: (loading: boolean) => void;
|
||||
};
|
||||
|
||||
const SHIP_ICON_MAPPING = {
|
||||
@ -570,6 +571,7 @@ export function Map3D({
|
||||
pairLinks,
|
||||
fcLinks,
|
||||
fleetCircles,
|
||||
onProjectionLoadingChange,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
@ -580,8 +582,41 @@ export function Map3D({
|
||||
const baseMapRef = useRef<BaseMapId>(baseMap);
|
||||
const projectionRef = useRef<MapProjectionId>(projection);
|
||||
const globeShipIconLoadingRef = useRef(false);
|
||||
const projectionBusyRef = useRef(false);
|
||||
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
||||
const projectionPrevRef = useRef<MapProjectionId>(projection);
|
||||
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
||||
|
||||
const clearProjectionBusyTimer = useCallback(() => {
|
||||
if (projectionBusyTimerRef.current == null) return;
|
||||
clearTimeout(projectionBusyTimerRef.current);
|
||||
projectionBusyTimerRef.current = null;
|
||||
}, []);
|
||||
|
||||
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.");
|
||||
}
|
||||
}, 18000);
|
||||
} else {
|
||||
clearProjectionBusyTimer();
|
||||
}
|
||||
|
||||
if (onProjectionLoadingChange) {
|
||||
onProjectionLoadingChange(loading);
|
||||
}
|
||||
},
|
||||
[onProjectionLoadingChange, clearProjectionBusyTimer],
|
||||
);
|
||||
|
||||
const pulseMapSync = () => {
|
||||
setMapSyncEpoch((prev) => prev + 1);
|
||||
requestAnimationFrame(() => {
|
||||
@ -590,6 +625,15 @@ export function Map3D({
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearProjectionBusyTimer();
|
||||
if (projectionBusyRef.current) {
|
||||
setProjectionLoading(false);
|
||||
}
|
||||
};
|
||||
}, [clearProjectionBusyTimer, setProjectionLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
showSeamarkRef.current = settings.showSeamark;
|
||||
}, [settings.showSeamark]);
|
||||
@ -816,6 +860,10 @@ export function Map3D({
|
||||
let cancelled = false;
|
||||
let retries = 0;
|
||||
const maxRetries = 18;
|
||||
const isTransition = projectionPrevRef.current !== projection;
|
||||
projectionPrevRef.current = projection;
|
||||
|
||||
if (isTransition) setProjectionLoading(true);
|
||||
|
||||
const disposeMercatorOverlay = () => {
|
||||
const current = overlayRef.current;
|
||||
@ -852,6 +900,9 @@ export function Map3D({
|
||||
|
||||
const syncProjectionAndDeck = () => {
|
||||
if (cancelled) return;
|
||||
if (!isTransition) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map.isStyleLoaded()) {
|
||||
if (!cancelled && retries < maxRetries) {
|
||||
@ -889,6 +940,7 @@ export function Map3D({
|
||||
window.requestAnimationFrame(() => syncProjectionAndDeck());
|
||||
return;
|
||||
}
|
||||
if (isTransition) setProjectionLoading(false);
|
||||
console.warn("Projection switch failed:", e);
|
||||
}
|
||||
|
||||
@ -932,22 +984,39 @@ export function Map3D({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (isTransition) {
|
||||
const mercatorReady = projection === "mercator" && !!overlayRef.current;
|
||||
const globeReady = projection === "globe" && !!map.getLayer("deck-globe");
|
||||
if (mercatorReady || globeReady) {
|
||||
setProjectionLoading(false);
|
||||
} else if (!cancelled && retries < maxRetries) {
|
||||
retries += 1;
|
||||
window.requestAnimationFrame(() => syncProjectionAndDeck());
|
||||
return;
|
||||
} else {
|
||||
setProjectionLoading(false);
|
||||
}
|
||||
}
|
||||
pulseMapSync();
|
||||
};
|
||||
|
||||
if (!isTransition) return;
|
||||
|
||||
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
||||
else {
|
||||
const stop = onMapStyleReady(map, syncProjectionAndDeck);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
stop();
|
||||
if (isTransition) setProjectionLoading(false);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (isTransition) setProjectionLoading(false);
|
||||
};
|
||||
}, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists]);
|
||||
}, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, setProjectionLoading]);
|
||||
|
||||
// Base map toggle
|
||||
useEffect(() => {
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user