160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo } from "react"
|
|
import { Layer, Source, useMap, Popup } from "react-map-gl/mapbox"
|
|
import { useState } from "react"
|
|
import { IGeoJSONPolygon } from "@/app/_utils/types/map"
|
|
import { CRIME_COLORS, CRIME_RATES } from "@/app/_utils/const/crime"
|
|
|
|
export type DistrictFeature = {
|
|
id: string
|
|
name: string
|
|
cityName: string
|
|
code: string
|
|
polygon: IGeoJSONPolygon
|
|
crimeRate: "low" | "medium" | "high" | "no_data"
|
|
crimeCount: number
|
|
year: number
|
|
}
|
|
|
|
type DistrictLayerProps = {
|
|
data: DistrictFeature[]
|
|
visible?: boolean
|
|
onClick?: (feature: any) => void
|
|
}
|
|
|
|
type hoverInfoType = {
|
|
feature: {
|
|
properties: {
|
|
crimeRate: "low" | "medium" | "high" | "no_data"
|
|
name: string
|
|
cityName: string
|
|
crimeCount: number
|
|
}
|
|
geometry: {
|
|
coordinates: number[][][]
|
|
}
|
|
}
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
export default function DistrictLayer({ data, visible = true, onClick }: DistrictLayerProps) {
|
|
const { current: map } = useMap()
|
|
const [hoverInfo, setHoverInfo] = useState<hoverInfoType | null>(null)
|
|
|
|
// Convert data to GeoJSON
|
|
const geojson = useMemo(() => {
|
|
return {
|
|
type: "FeatureCollection",
|
|
features: data
|
|
.filter((district) => district.polygon) // Only include districts with polygon data
|
|
.map((district) => ({
|
|
type: "Feature",
|
|
properties: {
|
|
id: district.id,
|
|
name: district.name,
|
|
cityName: district.cityName,
|
|
crimeRate: district.crimeRate,
|
|
crimeCount: district.crimeCount,
|
|
color: CRIME_COLORS[district.crimeRate],
|
|
},
|
|
geometry: district.polygon,
|
|
})),
|
|
}
|
|
}, [data])
|
|
|
|
// Handle hover events
|
|
useEffect(() => {
|
|
if (!map) return
|
|
|
|
const onHover = (event: any) => {
|
|
const { features, point } = event
|
|
const hoveredFeature = features && features[0]
|
|
|
|
// Update hover state
|
|
setHoverInfo(
|
|
hoveredFeature
|
|
? {
|
|
feature: hoveredFeature,
|
|
x: point.x,
|
|
y: point.y,
|
|
}
|
|
: null,
|
|
)
|
|
}
|
|
|
|
// Change cursor on hover
|
|
const onMouseEnter = () => {
|
|
if (map) map.getCanvas().style.cursor = "pointer"
|
|
}
|
|
|
|
const onMouseLeave = () => {
|
|
if (map) map.getCanvas().style.cursor = ""
|
|
setHoverInfo(null)
|
|
}
|
|
|
|
// Add event listeners
|
|
map.on("mousemove", "district-fills", onHover)
|
|
map.on("mouseenter", "district-fills", onMouseEnter)
|
|
map.on("mouseleave", "district-fills", onMouseLeave)
|
|
map.on("click", "district-fills", (e) => {
|
|
if (onClick && e.features && e.features[0]) {
|
|
onClick(e.features[0])
|
|
}
|
|
})
|
|
|
|
// Clean up
|
|
return () => {
|
|
map.off("mousemove", "district-fills", onHover)
|
|
map.off("mouseenter", "district-fills", onMouseEnter)
|
|
map.off("mouseleave", "district-fills", onMouseLeave)
|
|
map.off("click", "district-fills", onClick as any)
|
|
}
|
|
}, [map, onClick])
|
|
|
|
if (!visible) return null
|
|
|
|
return (
|
|
<>
|
|
<Source id="districts" type="geojson" data={geojson as any}>
|
|
<Layer
|
|
id="district-fills"
|
|
type="fill"
|
|
paint={{
|
|
"fill-color": ["get", "color"],
|
|
"fill-opacity": 0.6,
|
|
}}
|
|
/>
|
|
<Layer
|
|
id="district-borders"
|
|
type="line"
|
|
paint={{
|
|
"line-color": "#627D98",
|
|
"line-width": 1,
|
|
}}
|
|
/>
|
|
</Source>
|
|
|
|
{/* Popup on hover */}
|
|
{hoverInfo && (
|
|
<Popup
|
|
longitude={hoverInfo.feature.geometry.coordinates[0][0][0]}
|
|
latitude={hoverInfo.feature.geometry.coordinates[0][0][1]}
|
|
closeButton={false}
|
|
closeOnClick={false}
|
|
anchor="bottom"
|
|
offset={[0, -10]}
|
|
>
|
|
<div className="p-2">
|
|
<h3 className="font-bold">{hoverInfo.feature.properties.name}</h3>
|
|
<p>City: {hoverInfo.feature.properties.cityName}</p>
|
|
<p>Crime Rate: {CRIME_RATES[hoverInfo.feature.properties.crimeRate]}</p>
|
|
<p>Crime Count: {hoverInfo.feature.properties.crimeCount}</p>
|
|
</div>
|
|
</Popup>
|
|
)}
|
|
</>
|
|
)
|
|
}
|