109 lines
3.1 KiB
TypeScript
109 lines
3.1 KiB
TypeScript
import { CRIME_RATE_COLORS } from "@/app/_utils/const/map"
|
|
import { ControlPosition, IControl, Map } from "mapbox-gl"
|
|
import { useControl } from "react-map-gl/mapbox"
|
|
import React, { useEffect } from "react"
|
|
import { createRoot } from "react-dom/client"
|
|
|
|
interface MapLegendProps {
|
|
position?: ControlPosition
|
|
isFullscreen?: boolean
|
|
}
|
|
|
|
// React component for legend content
|
|
const LegendContent = () => {
|
|
return (
|
|
<div className="flex flex-row text-xs font-semibold">
|
|
<div className={`flex items-center gap-1.5 py-0 px-8 rounded-l-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.low}90` }}>
|
|
<span>Low</span>
|
|
</div>
|
|
<div className={`flex items-center gap-1.5 py-0 px-8 border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.medium}90` }}>
|
|
<span>Medium</span>
|
|
</div>
|
|
<div className={`flex items-center gap-1.5 py-0 px-8 rounded-r-md border-y border-1 `} style={{ backgroundColor: `${CRIME_RATE_COLORS.high}90` }}>
|
|
<span>High</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
class MapLegendControl implements IControl {
|
|
private container: HTMLElement;
|
|
private map?: Map;
|
|
private props: MapLegendProps;
|
|
private root: ReturnType<typeof createRoot> | null = null;
|
|
private isUnmounting: boolean = false;
|
|
|
|
constructor(props: MapLegendProps) {
|
|
this.props = props;
|
|
this.container = document.createElement("div");
|
|
this.container.className = "mapboxgl-ctrl";
|
|
}
|
|
|
|
onAdd(map: Map): HTMLElement {
|
|
this.map = map;
|
|
this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-legend';
|
|
this.render();
|
|
return this.container;
|
|
}
|
|
|
|
onRemove(): void {
|
|
// Prevent multiple unmounting attempts
|
|
if (this.isUnmounting) return;
|
|
|
|
this.isUnmounting = true;
|
|
|
|
// Schedule unmounting to happen after current render cycle completes
|
|
requestAnimationFrame(() => {
|
|
if (this.root) {
|
|
this.root.unmount();
|
|
this.root = null;
|
|
}
|
|
|
|
if (this.container.parentNode) {
|
|
this.container.parentNode.removeChild(this.container);
|
|
}
|
|
|
|
this.map = undefined;
|
|
this.isUnmounting = false;
|
|
});
|
|
}
|
|
|
|
updateProps(props: MapLegendProps): void {
|
|
this.props = props;
|
|
this.render();
|
|
}
|
|
|
|
render(): void {
|
|
// Only render if in fullscreen or if isFullscreen prop is not provided
|
|
if (this.props.isFullscreen === false) {
|
|
if (this.container.style.display !== 'none') {
|
|
this.container.style.display = 'none';
|
|
}
|
|
return;
|
|
} else {
|
|
this.container.style.display = 'block';
|
|
}
|
|
|
|
// Create a root if it doesn't exist
|
|
if (!this.root) {
|
|
this.root = createRoot(this.container);
|
|
}
|
|
|
|
// Render using the createRoot API
|
|
this.root.render(<LegendContent />);
|
|
}
|
|
}
|
|
|
|
export function MapLegend({ position = 'bottom-right', isFullscreen }: MapLegendProps) {
|
|
const control = useControl<MapLegendControl>(
|
|
() => new MapLegendControl({ position, isFullscreen }),
|
|
{ position }
|
|
);
|
|
|
|
// Update control when props change
|
|
useEffect(() => {
|
|
control.updateProps({ position, isFullscreen });
|
|
}, [control, position, isFullscreen]);
|
|
|
|
return null;
|
|
} |