178 lines
5.6 KiB
TypeScript
178 lines
5.6 KiB
TypeScript
"use client"
|
|
|
|
import type React from "react"
|
|
|
|
import { useState, useCallback, 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, MAPBOX_STYLES, MapboxStyle } 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 TimeControls from "./controls/time-control"
|
|
import SeverityIndicator from "./controls/severity-indicator"
|
|
import MapFilterControl from "./controls/map-filter-control"
|
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
|
import MapControls from "./controls/map-control"
|
|
|
|
interface MapViewProps {
|
|
children?: React.ReactNode
|
|
initialViewState?: Partial<ViewState>
|
|
mapStyle?: MapboxStyle
|
|
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
|
|
availableYears?: (number | null)[]
|
|
yearsLoading?: boolean
|
|
onYearChange?: (year: number) => void
|
|
onMonthChange?: (month: number | "all") => void
|
|
onApplyFilters?: () => void
|
|
onResetFilters?: () => void
|
|
}
|
|
|
|
export default function MapView({
|
|
children,
|
|
initialViewState,
|
|
mapStyle = MAPBOX_STYLES.Standard,
|
|
className = "w-full h-96",
|
|
width = "100%",
|
|
height = "100%",
|
|
mapboxApiAccessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN,
|
|
onMoveEnd,
|
|
customControls,
|
|
crimes = [],
|
|
selectedYear,
|
|
selectedMonth,
|
|
availableYears = [],
|
|
yearsLoading = false,
|
|
onYearChange = () => { },
|
|
onMonthChange = () => { },
|
|
onApplyFilters = () => { },
|
|
onResetFilters = () => { },
|
|
}: 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)
|
|
|
|
// Use the custom fullscreen hook instead of manual event listeners
|
|
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)
|
|
}
|
|
|
|
const handleTimeChange = (time: string) => {
|
|
setActiveTime(time)
|
|
}
|
|
|
|
const toggleSidebar = () => {
|
|
setSidebarOpen(!sidebarOpen)
|
|
}
|
|
|
|
return (
|
|
<div ref={mapContainerRef} className={`relative ${className}`}>
|
|
{/* 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: "100%", height: "100%" }}
|
|
>
|
|
{children}
|
|
<FullscreenControl position="top-right" />
|
|
<NavigationControl position="top-right" />
|
|
|
|
{/* Custom controls that depend on fullscreen state */}
|
|
{isFullscreen && <GeolocateControl position="top-right" />}
|
|
|
|
{/* Custom Filter Control with isFullscreen prop */}
|
|
{/* {isFullscreen && <MapFilterControl
|
|
position="top-right"
|
|
selectedYear={Number(selectedYear) || 2024}
|
|
selectedMonth={selectedMonth === "all" ? "all" : Number(selectedMonth) || "all"}
|
|
availableYears={availableYears}
|
|
yearsLoading={yearsLoading}
|
|
onYearChange={onYearChange}
|
|
onMonthChange={onMonthChange}
|
|
onApplyFilters={onApplyFilters}
|
|
onResetFilters={onResetFilters}
|
|
isFullscreen={isFullscreen}
|
|
/>
|
|
} */}
|
|
{/* Sidebar and other controls only in fullscreen */}
|
|
{isFullscreen && (
|
|
<>
|
|
<MapSidebar
|
|
isOpen={sidebarOpen}
|
|
onToggle={toggleSidebar}
|
|
crimes={crimes}
|
|
selectedYear={selectedYear}
|
|
selectedMonth={selectedMonth}
|
|
/>
|
|
<SidebarToggle isOpen={sidebarOpen} onToggle={toggleSidebar} />
|
|
<MapControls onControlChange={handleControlChange} activeControl={activeControl} />
|
|
<TimeControls onTimeChange={handleTimeChange} activeTime={activeTime} />
|
|
<SeverityIndicator />
|
|
</>
|
|
)}
|
|
</ReactMapGL>
|
|
</div>
|
|
|
|
{/* Debug indicator - uncomment for debugging
|
|
<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>
|
|
)
|
|
}
|