175 lines
5.4 KiB
TypeScript
175 lines
5.4 KiB
TypeScript
"use client"
|
|
|
|
import type React from "react"
|
|
|
|
import { useState, useCallback, useEffect, useRef } from "react"
|
|
import ReactMapGL, {
|
|
type ViewState,
|
|
NavigationControl,
|
|
ScaleControl,
|
|
type MapRef,
|
|
FullscreenControl,
|
|
GeolocateControl,
|
|
} from "react-map-gl/mapbox"
|
|
import { BASE_LATITUDE, BASE_LONGITUDE, BASE_ZOOM, MAP_STYLE } from "@/app/_utils/const/map"
|
|
import { Search } from "lucide-react"
|
|
import "mapbox-gl/dist/mapbox-gl.css"
|
|
import MapSidebar from "./controls/map-sidebar"
|
|
import SidebarToggle from "./controls/map-toggle"
|
|
import MapControls from "./controls/map-control"
|
|
import TimeControls from "./controls/time-control"
|
|
import SeverityIndicator from "./controls/severity-indicator"
|
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
|
import MapFilterControl from "./controls/map-filter-control"
|
|
|
|
interface MapViewProps {
|
|
children?: React.ReactNode
|
|
initialViewState?: Partial<ViewState>
|
|
mapStyle?: string
|
|
className?: string
|
|
width?: string | number
|
|
height?: string | number
|
|
mapboxApiAccessToken?: string
|
|
onMoveEnd?: (viewState: ViewState) => void
|
|
customControls?: React.ReactNode
|
|
crimes?: Array<{
|
|
id: string
|
|
district_name: string
|
|
district_id?: string
|
|
number_of_crime?: number
|
|
level?: "low" | "medium" | "high" | "critical"
|
|
incidents: any[]
|
|
}>
|
|
selectedYear?: number | string
|
|
selectedMonth?: number | string
|
|
}
|
|
|
|
export default function MapView({
|
|
children,
|
|
initialViewState,
|
|
mapStyle = MAP_STYLE,
|
|
className = "w-full h-96",
|
|
width = "100%",
|
|
height = "100%",
|
|
mapboxApiAccessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN,
|
|
onMoveEnd,
|
|
customControls,
|
|
crimes = [],
|
|
selectedYear,
|
|
selectedMonth,
|
|
}: MapViewProps) {
|
|
const [mapRef, setMapRef] = useState<MapRef | null>(null)
|
|
const [activeControl, setActiveControl] = useState<string>("crime-rate")
|
|
const [activeTime, setActiveTime] = useState<string>("today")
|
|
const [sidebarOpen, setSidebarOpen] = useState<boolean>(false)
|
|
const mapContainerRef = useRef<HTMLDivElement>(null)
|
|
const { isFullscreen } = useFullscreen(mapContainerRef)
|
|
|
|
const defaultViewState: Partial<ViewState> = {
|
|
longitude: BASE_LONGITUDE,
|
|
latitude: BASE_LATITUDE,
|
|
zoom: BASE_ZOOM,
|
|
bearing: 0,
|
|
pitch: 0,
|
|
...initialViewState,
|
|
}
|
|
|
|
const handleMapLoad = useCallback((event: any) => {
|
|
setMapRef(event.target)
|
|
}, [])
|
|
|
|
const handleMoveEnd = useCallback(
|
|
(event: any) => {
|
|
if (onMoveEnd) {
|
|
onMoveEnd(event.viewState)
|
|
}
|
|
},
|
|
[onMoveEnd],
|
|
)
|
|
|
|
const handleControlChange = (control: string) => {
|
|
setActiveControl(control)
|
|
// Here you would implement logic to change the map display based on the selected control
|
|
}
|
|
|
|
const handleTimeChange = (time: string) => {
|
|
setActiveTime(time)
|
|
// Here you would implement logic to change the time period of data shown
|
|
}
|
|
|
|
const toggleSidebar = () => {
|
|
setSidebarOpen(!sidebarOpen)
|
|
}
|
|
|
|
return (
|
|
<div ref={mapContainerRef} className={`relative ${className}`}>
|
|
{/* Custom controls - only show when in fullscreen mode */}
|
|
{isFullscreen && (
|
|
<>
|
|
{/* Sidebar */}
|
|
<MapSidebar
|
|
isOpen={sidebarOpen}
|
|
onToggle={toggleSidebar}
|
|
crimes={crimes}
|
|
selectedYear={selectedYear}
|
|
selectedMonth={selectedMonth}
|
|
/>
|
|
<SidebarToggle isOpen={sidebarOpen} onToggle={toggleSidebar} />
|
|
|
|
{/* Additional controls that should only appear in fullscreen */}
|
|
<div className="absolute top-2 right-2 z-10 flex items-center bg-white rounded-md shadow-md">
|
|
<input
|
|
type="text"
|
|
placeholder="Search location..."
|
|
className="px-3 py-2 rounded-l-md border-0 focus:outline-none w-64"
|
|
/>
|
|
<button className="bg-gray-100 p-2 rounded-r-md">
|
|
<Search size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<MapControls onControlChange={handleControlChange} activeControl={activeControl} />
|
|
<TimeControls onTimeChange={handleTimeChange} activeTime={activeTime} />
|
|
<SeverityIndicator />
|
|
|
|
{/* Make sure customControls is displayed in fullscreen mode */}
|
|
{customControls}
|
|
</>
|
|
)}
|
|
|
|
{/* Main content with left padding when sidebar is open */}
|
|
<div className={`relative h-full transition-all duration-300 ${isFullscreen && sidebarOpen ? "ml-80" : "ml-0"}`}>
|
|
<ReactMapGL
|
|
ref={(ref) => setMapRef(ref)}
|
|
mapStyle={mapStyle}
|
|
mapboxAccessToken={mapboxApiAccessToken}
|
|
initialViewState={defaultViewState}
|
|
onLoad={handleMapLoad}
|
|
onMoveEnd={handleMoveEnd}
|
|
interactiveLayerIds={["district-fill", "clusters", "unclustered-point"]}
|
|
attributionControl={false}
|
|
style={{ width, height }}
|
|
>
|
|
{children}
|
|
<NavigationControl position="right" />
|
|
<FullscreenControl
|
|
position="right"
|
|
containerId={mapContainerRef.current?.id}
|
|
/>
|
|
<ScaleControl position="bottom-left" />
|
|
|
|
{/* GeolocateControl only shown in fullscreen mode */}
|
|
{isFullscreen && (
|
|
<GeolocateControl position="right" />
|
|
)}
|
|
</ReactMapGL>
|
|
</div>
|
|
|
|
{/* Debug indicator - remove in production */}
|
|
<div className="absolute bottom-4 left-4 bg-black bg-opacity-70 text-white text-xs p-1 rounded z-50">
|
|
Fullscreen: {isFullscreen ? "Yes" : "No"}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|