feat(map): focus selected marker at max zoom and tighten collapsed device panel
All checks were successful
Deploy to Production / deploy (push) Successful in 1m51s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m51s
This commit is contained in:
@@ -67,6 +67,10 @@ type TelemetrySnapshot = {
|
|||||||
|
|
||||||
type ConnectionState = "connecting" | "live" | "retrying"
|
type ConnectionState = "connecting" | "live" | "retrying"
|
||||||
type BaseMapType = "osm" | "amap" | "amap-satellite"
|
type BaseMapType = "osm" | "amap" | "amap-satellite"
|
||||||
|
type MapControlBridge = {
|
||||||
|
__mapFitAll?: () => void
|
||||||
|
__mapFocusDevice?: (coordinate: [number, number]) => void
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- constants ---------- */
|
/* ---------- constants ---------- */
|
||||||
|
|
||||||
@@ -338,17 +342,38 @@ function FitBoundsControl({
|
|||||||
coordinates: Array<[number, number]>
|
coordinates: Array<[number, number]>
|
||||||
}) {
|
}) {
|
||||||
const map = useMap()
|
const map = useMap()
|
||||||
const fitAll = () => {
|
const fitAll = useCallback(() => {
|
||||||
if (!coordinates.length) return
|
if (!coordinates.length) return
|
||||||
if (coordinates.length === 1) {
|
if (coordinates.length === 1) {
|
||||||
map.setView(coordinates[0], 14, { animate: true })
|
map.setView(coordinates[0], 14, { animate: true })
|
||||||
} else {
|
} else {
|
||||||
map.fitBounds(coordinates, { padding: [40, 40] })
|
map.fitBounds(coordinates, { padding: [40, 40] })
|
||||||
}
|
}
|
||||||
}
|
}, [coordinates, map])
|
||||||
// expose via a ref so the outside button can call it
|
|
||||||
const ref = window as unknown as Record<string, () => void>
|
const focusDevice = useCallback(
|
||||||
ref.__mapFitAll = fitAll
|
(coordinate: [number, number]) => {
|
||||||
|
const maxZoom = map.getMaxZoom()
|
||||||
|
const targetZoom = Number.isFinite(maxZoom) ? maxZoom : 18
|
||||||
|
map.setView(coordinate, targetZoom, { animate: true })
|
||||||
|
},
|
||||||
|
[map],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = window as unknown as MapControlBridge
|
||||||
|
ref.__mapFitAll = fitAll
|
||||||
|
ref.__mapFocusDevice = focusDevice
|
||||||
|
return () => {
|
||||||
|
if (ref.__mapFitAll === fitAll) {
|
||||||
|
delete ref.__mapFitAll
|
||||||
|
}
|
||||||
|
if (ref.__mapFocusDevice === focusDevice) {
|
||||||
|
delete ref.__mapFocusDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [fitAll, focusDevice])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,9 +515,14 @@ function Dashboard() {
|
|||||||
[devicesWithCoords, isGcj02],
|
[devicesWithCoords, isGcj02],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDeviceClick = useCallback((deviceId: string) => {
|
const handleDeviceClick = useCallback(
|
||||||
setActiveDeviceId(deviceId)
|
(deviceId: string, coordinate: [number, number]) => {
|
||||||
}, [])
|
setActiveDeviceId(deviceId)
|
||||||
|
const mapBridge = window as unknown as MapControlBridge
|
||||||
|
mapBridge.__mapFocusDevice?.(coordinate)
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
const isLive = status === "live"
|
const isLive = status === "live"
|
||||||
|
|
||||||
@@ -574,11 +604,12 @@ function Dashboard() {
|
|||||||
{devicesWithCoords.map((device) => {
|
{devicesWithCoords.map((device) => {
|
||||||
const isOffline = checkIsOffline(device.last_event_time)
|
const isOffline = checkIsOffline(device.last_event_time)
|
||||||
const isSelected = activeDeviceId === device.device_id
|
const isSelected = activeDeviceId === device.device_id
|
||||||
const [projLat, projLon] = projectCoord(
|
const projectedCoordinate = projectCoord(
|
||||||
device.lat as number,
|
device.lat as number,
|
||||||
device.lon as number,
|
device.lon as number,
|
||||||
isGcj02,
|
isGcj02,
|
||||||
)
|
)
|
||||||
|
const [projLat, projLon] = projectedCoordinate
|
||||||
const color = deviceMarkerColor(device.fix_status, isOffline)
|
const color = deviceMarkerColor(device.fix_status, isOffline)
|
||||||
const icon = buildDeviceIcon(color, isSelected, isOffline)
|
const icon = buildDeviceIcon(color, isSelected, isOffline)
|
||||||
|
|
||||||
@@ -587,7 +618,8 @@ function Dashboard() {
|
|||||||
position={[projLat, projLon]}
|
position={[projLat, projLon]}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
click: () => handleDeviceClick(device.device_id),
|
click: () =>
|
||||||
|
handleDeviceClick(device.device_id, projectedCoordinate),
|
||||||
}}
|
}}
|
||||||
key={device.device_id}
|
key={device.device_id}
|
||||||
>
|
>
|
||||||
@@ -639,12 +671,14 @@ function Dashboard() {
|
|||||||
<div
|
<div
|
||||||
className={`absolute bottom-6 right-6 z-9999 w-80 max-w-[calc(100%-3rem)] transition-all duration-300 ${isPanelExpanded ? "opacity-80" : "opacity-100"}`}
|
className={`absolute bottom-6 right-6 z-9999 w-80 max-w-[calc(100%-3rem)] transition-all duration-300 ${isPanelExpanded ? "opacity-80" : "opacity-100"}`}
|
||||||
>
|
>
|
||||||
<Card className="shadow-lg border bg-background/50 backdrop-blur-md overflow-hidden">
|
<Card className="shadow-lg border bg-background/50 backdrop-blur-md overflow-hidden py-0 gap-0">
|
||||||
<CardHeader
|
<CardHeader
|
||||||
className="p-3 flex flex-row items-center justify-between space-y-0 cursor-pointer select-none"
|
className={`flex flex-row items-center justify-between space-y-0 cursor-pointer select-none transition-all duration-300 ${
|
||||||
|
isPanelExpanded ? "p-3 min-h-11" : "px-3 py-0 min-h-0"
|
||||||
|
}`}
|
||||||
onClick={() => setIsPanelExpanded(!isPanelExpanded)}
|
onClick={() => setIsPanelExpanded(!isPanelExpanded)}
|
||||||
>
|
>
|
||||||
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
<CardTitle className="font-semibold flex items-center gap-2 text-sm">
|
||||||
Selected Device
|
Selected Device
|
||||||
{activeDevice?.signal_health &&
|
{activeDevice?.signal_health &&
|
||||||
!isPanelExpanded &&
|
!isPanelExpanded &&
|
||||||
|
|||||||
Reference in New Issue
Block a user