fix: Bản đồ không reset khi người dùng thay đổi zoom trên mobile
- Thêm MapInteractionWatcher lưu zoom/center khi người dùng tương tác - MapSizeHandler dùng ResizeObserver thay vì setTimeout: chỉ gọi invalidateSize khi container thực sự thay đổi (>2px) - Sau invalidateSize, tự động restore zoom mà người dùng đã chọn - canFitRef đảm bảo fitBounds chỉ chạy 1 lần duy nhất khi route vừa load - Áp dụng cho cả chế độ la bàn bật (xoay theo hướng) và tắt (hướng Bắc)
This commit is contained in:
@@ -16,14 +16,14 @@ interface TourNavigationPageProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FitBounds = ({ coords, canFitRef, compassActive }: { coords: [number, number][], canFitRef: React.MutableRefObject<boolean>, compassActive: boolean }) => {
|
const FitBounds = ({ coords, canFitRef }: { coords: [number, number][], canFitRef: React.MutableRefObject<boolean> }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (coords.length > 0 && canFitRef.current && !compassActive) {
|
if (coords.length > 0 && canFitRef.current) {
|
||||||
canFitRef.current = false;
|
canFitRef.current = false;
|
||||||
map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 });
|
map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 });
|
||||||
}
|
}
|
||||||
}, [map, coords, canFitRef, compassActive]);
|
}, [map, coords, canFitRef]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,30 +70,66 @@ const CompassInteractionDetector = ({
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MapRefSetter = ({ mapRef, userInteractedRef }: { mapRef: React.MutableRefObject<L.Map | null>, userInteractedRef: React.MutableRefObject<boolean> }) => {
|
const MapRefSetter = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | null> }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
const onZoomEnd = () => { userInteractedRef.current = true; };
|
}, [map, mapRef]);
|
||||||
const onMoveEnd = () => { userInteractedRef.current = true; };
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapInteractionWatcher = () => {
|
||||||
|
const map = useMap();
|
||||||
|
useEffect(() => {
|
||||||
|
const onZoomEnd = () => {
|
||||||
|
map._userZoomLevel = map.getZoom();
|
||||||
|
};
|
||||||
|
const onMoveEnd = () => {
|
||||||
|
map._userCenter = map.getCenter();
|
||||||
|
};
|
||||||
map.on('zoomend', onZoomEnd);
|
map.on('zoomend', onZoomEnd);
|
||||||
map.on('moveend', onMoveEnd);
|
map.on('moveend', onMoveEnd);
|
||||||
return () => {
|
return () => {
|
||||||
map.off('zoomend', onZoomEnd);
|
map.off('zoomend', onZoomEnd);
|
||||||
map.off('moveend', onMoveEnd);
|
map.off('moveend', onMoveEnd);
|
||||||
};
|
};
|
||||||
}, [map, mapRef, userInteractedRef]);
|
}, [map]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | null> }) => {
|
const MapSizeHandler = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | null> }) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const container = map.getContainer();
|
||||||
mapRef.current?.invalidateSize();
|
if (!container) return;
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
prevSizeRef.current = { width: container.clientWidth, height: container.clientHeight };
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
const newWidth = container.clientWidth;
|
||||||
|
const newHeight = container.clientHeight;
|
||||||
|
const prev = prevSizeRef.current;
|
||||||
|
|
||||||
|
if ((prev && (Math.abs(newWidth - prev.width) > 2 || Math.abs(newHeight - prev.height) > 2)) || newWidth === 0 || newHeight === 0) {
|
||||||
|
prevSizeRef.current = { width: newWidth, height: newHeight };
|
||||||
|
const prevZoom = map.getZoom();
|
||||||
|
const prevCenter = map.getCenter();
|
||||||
|
const userZoom = (map as any)._userZoomLevel as number | undefined;
|
||||||
|
|
||||||
|
map.invalidateSize({ animate: false });
|
||||||
|
|
||||||
|
if (userZoom !== undefined && Math.abs(map.getZoom() - userZoom) > 0.01) {
|
||||||
|
map.setZoom(userZoom, { animate: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(container);
|
||||||
|
return () => observer.disconnect();
|
||||||
}, [map, mapRef]);
|
}, [map, mapRef]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,12 +138,11 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
|||||||
const [currentHeading, setCurrentHeading] = useState(0);
|
const [currentHeading, setCurrentHeading] = useState(0);
|
||||||
const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
|
const [routeGeometry, setRouteGeometry] = useState<[number, number][] | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const userInteractedRef = useRef(false);
|
|
||||||
const canFitRef = useRef(true);
|
|
||||||
|
|
||||||
const mapRef = useRef<L.Map | null>(null);
|
const mapRef = useRef<L.Map | null>(null);
|
||||||
const watchIdRef = useRef<number | null>(null);
|
const watchIdRef = useRef<number | null>(null);
|
||||||
const fetchLockRef = useRef(false);
|
const fetchLockRef = useRef(false);
|
||||||
|
const canFitRef = useRef(true);
|
||||||
|
|
||||||
const originLat = routeData?.origin?.lat;
|
const originLat = routeData?.origin?.lat;
|
||||||
const originLng = routeData?.origin?.lng;
|
const originLng = routeData?.origin?.lng;
|
||||||
@@ -252,26 +287,13 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
|
|||||||
minZoom={2}
|
minZoom={2}
|
||||||
>
|
>
|
||||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" />
|
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" />
|
||||||
<MapSizeHandler mapRef={mapRef} />
|
<MapRefSetter mapRef={mapRef} />
|
||||||
<MapRefSetter mapRef={mapRef} userInteractedRef={userInteractedRef} />
|
<MapInteractionWatcher />
|
||||||
<FitBounds coords={routeGeometry || []} canFitRef={canFitRef} compassActive={isCompassActive} />
|
<FitBounds coords={routeGeometry || []} canFitRef={canFitRef} />
|
||||||
{routeData.origin && (
|
|
||||||
<Marker position={[routeData.origin.lat, routeData.origin.lng]} icon={userIcon}>
|
|
||||||
<Popup>
|
|
||||||
<div className="text-xs font-bold text-blue-600">Bạn đang ở đây</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
)}
|
|
||||||
{routeData.destination && (
|
|
||||||
<Marker position={[routeData.destination.lat, routeData.destination.lng]} icon={destIcon}>
|
|
||||||
<Popup>
|
|
||||||
<div className="text-xs font-bold text-red-600">{routeData.destination.name}</div>
|
|
||||||
</Popup>
|
|
||||||
</Marker>
|
|
||||||
)}
|
|
||||||
{routeGeometry && (
|
{routeGeometry && (
|
||||||
<Polyline positions={routeGeometry} color="#2563eb" weight={6} opacity={0.9} smoothFactor={1} />
|
<Polyline positions={routeGeometry} color="#2563eb" weight={6} opacity={0.9} smoothFactor={1} />
|
||||||
)}
|
)}
|
||||||
|
<MapSizeHandler mapRef={mapRef} />
|
||||||
<MapRotationHandler rotation={currentHeading} />
|
<MapRotationHandler rotation={currentHeading} />
|
||||||
<CompassInteractionDetector onUserInteraction={handleUserInteraction} />
|
<CompassInteractionDetector onUserInteraction={handleUserInteraction} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user