feat: add map reset functionality and improve incident popup handling
This commit is contained in:
parent
6327d7b886
commit
c282d958a5
|
@ -21,6 +21,7 @@ import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"
|
||||||
import { ITooltips } from "./controls/top/tooltips"
|
import { ITooltips } from "./controls/top/tooltips"
|
||||||
import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
|
import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
|
||||||
import Tooltips from "./controls/top/tooltips"
|
import Tooltips from "./controls/top/tooltips"
|
||||||
|
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
|
||||||
|
|
||||||
// Updated CrimeIncident type to match the structure in crime_incidents
|
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||||
interface ICrimeIncident {
|
interface ICrimeIncident {
|
||||||
|
@ -225,6 +226,57 @@ export default function CrimeMap() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Add event listener for map reset
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMapReset = (e: CustomEvent) => {
|
||||||
|
const { duration } = e.detail || { duration: 1500 };
|
||||||
|
|
||||||
|
// Find the map instance
|
||||||
|
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
|
||||||
|
if (!mapInstance) {
|
||||||
|
console.error("Map instance not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and dispatch the reset event that MapView will listen for
|
||||||
|
const mapboxEvent = new CustomEvent('mapbox_fly', {
|
||||||
|
detail: {
|
||||||
|
duration: duration,
|
||||||
|
resetCamera: true
|
||||||
|
},
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.dispatchEvent(mapboxEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener
|
||||||
|
document.addEventListener('mapbox_reset', handleMapReset as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mapbox_reset', handleMapReset as EventListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update the popup close handler to reset the map view
|
||||||
|
const handlePopupClose = () => {
|
||||||
|
setSelectedIncident(null);
|
||||||
|
|
||||||
|
// Dispatch map reset event to reset zoom, pitch, and bearing
|
||||||
|
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
|
||||||
|
|
||||||
|
if (mapInstance) {
|
||||||
|
const resetEvent = new CustomEvent('mapbox_reset', {
|
||||||
|
detail: {
|
||||||
|
duration: 1500,
|
||||||
|
},
|
||||||
|
bubbles: true
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.dispatchEvent(resetEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle year-month timeline change
|
// Handle year-month timeline change
|
||||||
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
|
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
|
||||||
setSelectedYear(year)
|
setSelectedYear(year)
|
||||||
|
@ -262,11 +314,6 @@ export default function CrimeMap() {
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle sidebar function
|
|
||||||
const toggleSidebar = useCallback(() => {
|
|
||||||
setSidebarCollapsed(!sidebarCollapsed)
|
|
||||||
}, [sidebarCollapsed])
|
|
||||||
|
|
||||||
// Handle control changes from the top controls component
|
// Handle control changes from the top controls component
|
||||||
const handleControlChange = (controlId: ITooltips) => {
|
const handleControlChange = (controlId: ITooltips) => {
|
||||||
setActiveControl(controlId)
|
setActiveControl(controlId)
|
||||||
|
@ -327,7 +374,7 @@ export default function CrimeMap() {
|
||||||
<CrimePopup
|
<CrimePopup
|
||||||
longitude={selectedIncident.longitude}
|
longitude={selectedIncident.longitude}
|
||||||
latitude={selectedIncident.latitude}
|
latitude={selectedIncident.latitude}
|
||||||
onClose={() => setSelectedIncident(null)}
|
onClose={handlePopupClose}
|
||||||
incident={selectedIncident}
|
incident={selectedIncident}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -362,7 +409,7 @@ export default function CrimeMap() {
|
||||||
selectedMonth={selectedMonth}
|
selectedMonth={selectedMonth}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-20 right-0 z-10 p-2">
|
<div className="absolute bottom-20 right-0 z-10 p-2">
|
||||||
<MapLegend position="bottom-right" />
|
<MapLegend position="bottom-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -88,16 +88,29 @@ export default function MapView({
|
||||||
const handleMapFly = (e: CustomEvent) => {
|
const handleMapFly = (e: CustomEvent) => {
|
||||||
if (!e.detail || !mapRef.current) return;
|
if (!e.detail || !mapRef.current) return;
|
||||||
|
|
||||||
const { center, zoom, bearing, pitch, duration } = e.detail;
|
const { center, zoom, bearing, pitch, duration, resetCamera } = e.detail;
|
||||||
|
|
||||||
mapRef.current.flyTo({
|
if (resetCamera) {
|
||||||
center,
|
// Reset to default view
|
||||||
zoom,
|
mapRef.current.flyTo({
|
||||||
bearing,
|
center: [BASE_LONGITUDE, BASE_LATITUDE],
|
||||||
pitch,
|
zoom: BASE_ZOOM,
|
||||||
duration,
|
bearing: BASE_BEARING,
|
||||||
essential: true
|
pitch: BASE_PITCH,
|
||||||
});
|
duration,
|
||||||
|
essential: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fly to specific location
|
||||||
|
mapRef.current.flyTo({
|
||||||
|
center,
|
||||||
|
zoom,
|
||||||
|
bearing,
|
||||||
|
pitch,
|
||||||
|
duration,
|
||||||
|
essential: true
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mapElement.addEventListener('mapbox_fly', handleMapFly as EventListener);
|
mapElement.addEventListener('mapbox_fly', handleMapFly as EventListener);
|
||||||
|
|
|
@ -27,31 +27,31 @@ interface IncidentPopupProps {
|
||||||
|
|
||||||
export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) {
|
export default function IncidentPopup({ longitude, latitude, onClose, incident }: IncidentPopupProps) {
|
||||||
const formatDate = (date?: Date) => {
|
const formatDate = (date?: Date) => {
|
||||||
if (!date) return "Unknown date"
|
if (!date) return "Unknown date"
|
||||||
return new Date(date).toLocaleDateString()
|
return new Date(date).toLocaleDateString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTime = (date?: Date) => {
|
const formatTime = (date?: Date) => {
|
||||||
if (!date) return "Unknown time"
|
if (!date) return "Unknown time"
|
||||||
return new Date(date).toLocaleTimeString()
|
return new Date(date).toLocaleTimeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusBadge = (status?: string) => {
|
const getStatusBadge = (status?: string) => {
|
||||||
if (!status) return <Badge variant="outline">Unknown</Badge>
|
if (!status) return <Badge variant="outline">Unknown</Badge>
|
||||||
|
|
||||||
const statusLower = status.toLowerCase()
|
const statusLower = status.toLowerCase()
|
||||||
if (statusLower.includes("resolv") || statusLower.includes("closed")) {
|
if (statusLower.includes("resolv") || statusLower.includes("closed")) {
|
||||||
return <Badge className="bg-emerald-600 text-white">Resolved</Badge>
|
return <Badge className="bg-emerald-600 text-white">Resolved</Badge>
|
||||||
}
|
}
|
||||||
if (statusLower.includes("progress") || statusLower.includes("invest")) {
|
if (statusLower.includes("progress") || statusLower.includes("invest")) {
|
||||||
return <Badge className="bg-amber-500 text-white">In Progress</Badge>
|
return <Badge className="bg-amber-500 text-white">In Progress</Badge>
|
||||||
}
|
}
|
||||||
if (statusLower.includes("open") || statusLower.includes("new")) {
|
if (statusLower.includes("open") || statusLower.includes("new")) {
|
||||||
return <Badge className="bg-blue-600 text-white">Open</Badge>
|
return <Badge className="bg-blue-600 text-white">Open</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Badge variant="outline">{status}</Badge>
|
return <Badge variant="outline">{status}</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine border color based on status
|
// Determine border color based on status
|
||||||
const getBorderColor = (status?: string) => {
|
const getBorderColor = (status?: string) => {
|
||||||
|
@ -75,108 +75,108 @@ export default function IncidentPopup({ longitude, latitude, onClose, incident }
|
||||||
<Popup
|
<Popup
|
||||||
longitude={longitude}
|
longitude={longitude}
|
||||||
latitude={latitude}
|
latitude={latitude}
|
||||||
closeButton={false} // Hide default close button
|
closeButton={false}
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
maxWidth="320px"
|
maxWidth="320px"
|
||||||
className="incident-popup z-50"
|
className="incident-popup z-50"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`}
|
className={`bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 ${getBorderColor(incident.status)}`}
|
||||||
>
|
>
|
||||||
<div className="p-4 relative">
|
<div className="p-4 relative">
|
||||||
{/* Custom close button */}
|
{/* Custom close button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
className="absolute top-2 right-2 h-6 w-6 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="font-bold text-base flex items-center gap-1.5">
|
<h3 className="font-bold text-base flex items-center gap-1.5">
|
||||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||||
{incident.category || "Unknown Incident"}
|
{incident.category || "Unknown Incident"}
|
||||||
</h3>
|
</h3>
|
||||||
{getStatusBadge(incident.status)}
|
{getStatusBadge(incident.status)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{incident.description && (
|
{incident.description && (
|
||||||
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
<div className="mb-3 bg-slate-50 dark:bg-slate-900/40 p-3 rounded-lg">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
<FileText className="inline-block h-3.5 w-3.5 mr-1.5 align-text-top text-slate-500" />
|
||||||
{incident.description}
|
{incident.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator className="my-3" />
|
<Separator className="my-3" />
|
||||||
|
|
||||||
{/* Improved section headers */}
|
{/* Improved section headers */}
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
{incident.district && (
|
{incident.district && (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">District</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
<Bookmark className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-purple-500" />
|
||||||
<span className="font-medium">{incident.district}</span>
|
<span className="font-medium">{incident.district}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incident.address && (
|
{incident.address && (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Location</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
<MapPin className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-red-500" />
|
||||||
<span className="font-medium">{incident.address}</span>
|
<span className="font-medium">{incident.address}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incident.timestamp && (
|
{incident.timestamp && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Date</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
<Calendar className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-blue-500" />
|
||||||
<span className="font-medium">{formatDate(incident.timestamp)}</span>
|
<span className="font-medium">{formatDate(incident.timestamp)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Time</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
<Clock className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-amber-500" />
|
||||||
<span className="font-medium">{formatTime(incident.timestamp)}</span>
|
<span className="font-medium">{formatTime(incident.timestamp)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{incident.type_category && (
|
{incident.type_category && (
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
|
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Type</p>
|
||||||
<p className="flex items-center">
|
<p className="flex items-center">
|
||||||
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
<Tag className="inline-block h-3.5 w-3.5 mr-1.5 shrink-0 text-green-500" />
|
||||||
<span className="font-medium">{incident.type_category}</span>
|
<span className="font-medium">{incident.type_category}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-border">
|
<div className="mt-3 pt-3 border-t border-border">
|
||||||
<p className="text-xs text-muted-foreground flex items-center">
|
<p className="text-xs text-muted-foreground flex items-center">
|
||||||
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
<Navigation className="inline-block h-3 w-3 mr-1 shrink-0" />
|
||||||
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
Coordinates: {latitude.toFixed(6)}, {longitude.toFixed(6)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
<p className="text-xs text-muted-foreground mt-1">ID: {incident.id}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue