Skip to content

Commit 25fb37d

Browse files
committed
fix(dashboard): smooth globe map loading
1 parent b55279b commit 25fb37d

1 file changed

Lines changed: 114 additions & 64 deletions

File tree

src/components/dashboard/geo-points-map.tsx

Lines changed: 114 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,16 @@ const GLOBE_VIEW_STATE: MapViewState = {
6868
longitude: 0,
6969
latitude: 30,
7070
zoom: 2.25,
71-
minZoom: 1.5,
72-
maxZoom: 3,
71+
minZoom: 2,
72+
maxZoom: 5,
7373
pitch: 0,
7474
bearing: 0,
7575
};
76+
const GLOBE_MOBILE_VIEW_STATE: MapViewState = {
77+
...GLOBE_VIEW_STATE,
78+
zoom: 1.75,
79+
minZoom: 1,
80+
};
7681
const GLOBE_ROTATION_DEGREES_PER_SECOND = 10;
7782
const GLOBE_USER_INTERACTION_PAUSE_MS = 5000;
7883
const GLOBE_RECOVERY_DURATION_MS = 1200;
@@ -92,6 +97,8 @@ function buildRasterStyle(
9297
const layerId = `insightflare-raster-layer-${theme}`;
9398
const endpoint = `/api/public/resources/map-tiles/{z}/{x}/{y}.png?theme=${theme}`;
9499
const isGlobe = projectionMode === "globe";
100+
const backgroundColor =
101+
theme === "dark" ? "rgb(10, 10, 10)" : "rgb(255, 255, 255)";
95102

96103
return {
97104
version: 8,
@@ -102,12 +109,9 @@ function buildRasterStyle(
102109
...(isGlobe
103110
? {
104111
sky: {
105-
"sky-color":
106-
theme === "dark" ? "rgb(8, 12, 18)" : "rgb(244, 247, 250)",
107-
"horizon-color":
108-
theme === "dark" ? "rgb(17, 24, 39)" : "rgb(233, 238, 244)",
109-
"fog-color":
110-
theme === "dark" ? "rgb(8, 12, 18)" : "rgb(244, 247, 250)",
112+
"sky-color": backgroundColor,
113+
"horizon-color": backgroundColor,
114+
"fog-color": backgroundColor,
111115
"atmosphere-blend": 0.18,
112116
},
113117
}
@@ -127,9 +131,8 @@ function buildRasterStyle(
127131
id: "insightflare-globe-ocean",
128132
type: "background" as const,
129133
paint: {
130-
"background-color":
131-
theme === "dark" ? "rgb(15, 23, 42)" : "rgb(226, 233, 241)",
132-
"background-opacity": theme === "dark" ? 0.88 : 0.82,
134+
"background-color": backgroundColor,
135+
"background-opacity": 1,
133136
},
134137
},
135138
]
@@ -454,6 +457,7 @@ export function GeoPointsMap({
454457
null,
455458
);
456459
const [hoveredCountryName, setHoveredCountryName] = useState("");
460+
const [mapSettled, setMapSettled] = useState(false);
457461
const [currentZoom, setCurrentZoom] = useState(
458462
normalizeClusterZoom(DEFAULT_VIEW_STATE.zoom),
459463
);
@@ -529,8 +533,12 @@ export function GeoPointsMap({
529533

530534
const initialViewState = useMemo(
531535
() =>
532-
isGlobe ? GLOBE_VIEW_STATE : computeInitialViewState(normalizedPoints),
533-
[isGlobe, normalizedPoints],
536+
isGlobe
537+
? isMobile
538+
? GLOBE_MOBILE_VIEW_STATE
539+
: GLOBE_VIEW_STATE
540+
: computeInitialViewState(normalizedPoints),
541+
[isGlobe, isMobile, normalizedPoints],
534542
);
535543
const mobileMinZoom =
536544
initialViewState.minZoom ?? DEFAULT_VIEW_STATE.minZoom ?? 0;
@@ -611,6 +619,10 @@ export function GeoPointsMap({
611619
() => buildRasterStyle(effectiveMapTheme, projectionMode),
612620
[effectiveMapTheme, projectionMode],
613621
);
622+
623+
useEffect(() => {
624+
setMapSettled(false);
625+
}, [mapInitialViewState, mapStyle]);
614626
const normalizedSelectedCountryCode = useMemo(
615627
() => normalizeCountryCode(selectedCountryCode),
616628
[selectedCountryCode],
@@ -785,30 +797,44 @@ export function GeoPointsMap({
785797
rotationRecoveryFromRef.current = null;
786798
}, []);
787799

800+
const markMapSettled = useCallback((map: MaplibreMap) => {
801+
if (!map.isStyleLoaded() || !map.areTilesLoaded()) return;
802+
setMapSettled(true);
803+
}, []);
804+
788805
const applyProjectionMode = useCallback(
789806
(map: MaplibreMap) => {
790807
if (!map.isStyleLoaded()) return false;
791808
map.setProjection({ type: isGlobe ? "globe" : "mercator" });
792809
if (isGlobe) {
793810
map.jumpTo({
794-
center: [GLOBE_VIEW_STATE.longitude, GLOBE_VIEW_STATE.latitude],
795-
zoom: GLOBE_VIEW_STATE.zoom,
811+
center: [initialViewState.longitude, initialViewState.latitude],
812+
zoom: initialViewState.zoom,
796813
bearing: 0,
797814
pitch: 0,
798815
});
799-
setCurrentZoom(normalizeClusterZoom(GLOBE_VIEW_STATE.zoom));
816+
setCurrentZoom(
817+
normalizeClusterZoom(initialViewState.zoom ?? GLOBE_VIEW_STATE.zoom),
818+
);
800819
}
801820
return true;
802821
},
803-
[isGlobe],
822+
[initialViewState, isGlobe],
804823
);
805824

806825
useEffect(() => {
807826
const map = mapRef.current?.getMap();
808827
if (!map || !isMobile) return;
809-
map.jumpTo({ zoom: mobileMinZoom });
810-
setCurrentZoom(normalizeClusterZoom(mobileMinZoom));
811-
}, [isMobile, mobileMinZoom]);
828+
map.jumpTo({
829+
center: [initialViewState.longitude, initialViewState.latitude],
830+
zoom: initialViewState.zoom,
831+
bearing: 0,
832+
pitch: 0,
833+
});
834+
setCurrentZoom(
835+
normalizeClusterZoom(initialViewState.zoom ?? DEFAULT_VIEW_STATE.zoom),
836+
);
837+
}, [initialViewState, isMobile]);
812838

813839
useEffect(() => {
814840
const map = mapRef.current?.getMap();
@@ -824,7 +850,7 @@ export function GeoPointsMap({
824850
}, [applyProjectionMode, mapLoaded]);
825851

826852
useEffect(() => {
827-
if (!autoRotateEnabled) return;
853+
if (!autoRotateEnabled || !mapSettled) return;
828854

829855
const rotate = (timestamp: number) => {
830856
const map = mapRef.current?.getMap();
@@ -872,13 +898,16 @@ export function GeoPointsMap({
872898
const recoveryFrom = rotationRecoveryFromRef.current;
873899
const nextLatitude = recoveryFrom
874900
? recoveryFrom.latitude +
875-
(GLOBE_VIEW_STATE.latitude - recoveryFrom.latitude) *
901+
((initialViewState.latitude ?? GLOBE_VIEW_STATE.latitude) -
902+
recoveryFrom.latitude) *
876903
recoveryProgress
877-
: GLOBE_VIEW_STATE.latitude;
904+
: (initialViewState.latitude ?? GLOBE_VIEW_STATE.latitude);
878905
const nextZoom = recoveryFrom
879906
? recoveryFrom.zoom +
880-
(GLOBE_VIEW_STATE.zoom - recoveryFrom.zoom) * recoveryProgress
881-
: GLOBE_VIEW_STATE.zoom;
907+
((initialViewState.zoom ?? GLOBE_VIEW_STATE.zoom) -
908+
recoveryFrom.zoom) *
909+
recoveryProgress
910+
: (initialViewState.zoom ?? GLOBE_VIEW_STATE.zoom);
882911
const nextLongitude = normalizeLongitude(
883912
center.lng +
884913
deltaSeconds * GLOBE_ROTATION_DEGREES_PER_SECOND * speedProgress,
@@ -916,7 +945,7 @@ export function GeoPointsMap({
916945
rotationFrameRef.current = null;
917946
}
918947
};
919-
}, [autoRotateEnabled]);
948+
}, [autoRotateEnabled, initialViewState, mapSettled]);
920949

921950
if (loading) {
922951
return (
@@ -943,47 +972,68 @@ export function GeoPointsMap({
943972
}`}
944973
style={MAP_VIEWPORT_RENDER_ISOLATION_STYLE}
945974
>
946-
<Map
947-
ref={mapRef}
948-
initialViewState={mapInitialViewState}
949-
mapStyle={mapStyle}
950-
attributionControl={false}
951-
scrollZoom
952-
maxPitch={0}
953-
dragRotate={false}
954-
pitchWithRotate={false}
955-
renderWorldCopies={!isGlobe}
956-
onDragStart={(event) => {
957-
if (event.originalEvent) pauseAutoRotate();
958-
}}
959-
onDrag={(event) => {
960-
if (event.originalEvent) pauseAutoRotate();
961-
}}
962-
onZoomStart={(event) => {
963-
if (event.originalEvent) pauseAutoRotate();
964-
}}
965-
onMove={(event) => {
966-
if (event.originalEvent) pauseAutoRotate();
967-
}}
968-
onMoveEnd={(event) => {
969-
if (event.originalEvent) pauseAutoRotate();
970-
}}
971-
onZoom={(event) => {
972-
if (event.originalEvent) pauseAutoRotate();
973-
const nextZoom = normalizeClusterZoom(event.viewState.zoom);
974-
setCurrentZoom((prev) =>
975-
Math.abs(prev - nextZoom) > 0.0001 ? nextZoom : prev,
976-
);
975+
{!mapSettled ? (
976+
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center bg-background">
977+
<Spinner className="size-6" />
978+
</div>
979+
) : null}
980+
<motion.div
981+
className="absolute inset-0"
982+
initial={false}
983+
animate={{
984+
opacity: mapSettled ? 1 : 0,
985+
scale: mapSettled ? 1 : 0.5,
986+
y: mapSettled ? 0 : "50%",
977987
}}
978-
onMouseDown={pauseAutoRotate}
979-
onTouchStart={pauseAutoRotate}
980-
onLoad={(event) => {
981-
setMapLoaded(true);
982-
applyProjectionMode(event.target);
988+
transition={{
989+
duration: 3,
990+
ease: [0.16, 1.45, 0.34, 1],
983991
}}
984992
>
985-
<DeckOverlay interleaved={false} layers={layers} />
986-
</Map>
993+
<Map
994+
ref={mapRef}
995+
initialViewState={mapInitialViewState}
996+
mapStyle={mapStyle}
997+
attributionControl={false}
998+
scrollZoom
999+
maxPitch={0}
1000+
dragRotate={false}
1001+
pitchWithRotate={false}
1002+
renderWorldCopies={!isGlobe}
1003+
onDragStart={(event) => {
1004+
if (event.originalEvent) pauseAutoRotate();
1005+
}}
1006+
onDrag={(event) => {
1007+
if (event.originalEvent) pauseAutoRotate();
1008+
}}
1009+
onZoomStart={(event) => {
1010+
if (event.originalEvent) pauseAutoRotate();
1011+
}}
1012+
onMove={(event) => {
1013+
if (event.originalEvent) pauseAutoRotate();
1014+
}}
1015+
onMoveEnd={(event) => {
1016+
if (event.originalEvent) pauseAutoRotate();
1017+
}}
1018+
onZoom={(event) => {
1019+
if (event.originalEvent) pauseAutoRotate();
1020+
const nextZoom = normalizeClusterZoom(event.viewState.zoom);
1021+
setCurrentZoom((prev) =>
1022+
Math.abs(prev - nextZoom) > 0.0001 ? nextZoom : prev,
1023+
);
1024+
}}
1025+
onMouseDown={pauseAutoRotate}
1026+
onTouchStart={pauseAutoRotate}
1027+
onLoad={(event) => {
1028+
setMapLoaded(true);
1029+
applyProjectionMode(event.target);
1030+
markMapSettled(event.target);
1031+
}}
1032+
onIdle={(event) => markMapSettled(event.target)}
1033+
>
1034+
<DeckOverlay interleaved={false} layers={layers} />
1035+
</Map>
1036+
</motion.div>
9871037
<AnimatePresence>
9881038
{showCountryToolbar ? (
9891039
<motion.div

0 commit comments

Comments
 (0)