import {
AdvancedMarker,
Map as GMap,
InfoWindow,
Pin,
useMap,
} from "@vis.gl/react-google-maps";
import { Layers, Map as MapIcon, RotateCcw, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { twMerge } from "tailwind-merge";
import { type LatLng, usePreloadGeodataLayers } from "@/entities/geodata";
import { debounce } from "@/shared/lib/debounce.ts";
import {
Sheet,
SheetContent,
SheetTitle,
SheetTrigger,
} from "@/shared/ui/sheet.tsx";
import { getPlacemarkPosition } from "../lib/getCenterPosition.ts";
import { useMapState } from "../model/slice.ts";
import { EventsLayerControl } from "./EventsLayerControl.tsx";
import { FeatureAttributes } from "./FeatureAttributes.tsx";
import { LayerControl } from "./LayerControl.tsx";
import { MapLongPressHandler } from "./MapLongPressHandler.tsx";
import { Placemark } from "./Placemark.tsx";
import { PlacemarkEventList } from "./PlacemarkEventList.tsx";
type EbaliMapProps = {
initialCenter?: LatLng;
className?: string;
longPressHandler?: (latLng: LatLng) => void;
children?: React.ReactNode;
requireTwoFingerScroll?: boolean;
layersOpacity?: number;
};
export const EbaliMap: React.FC<EbaliMapProps> = ({
initialCenter,
className,
longPressHandler,
children,
requireTwoFingerScroll = true,
layersOpacity = 1,
}) => {
const {
isInitial,
resetState,
zoom,
center,
selectedPlacemark,
setZoom,
setCenter,
setSelectedPlacemark,
getDisplayedLayers,
selectEventPlace,
selectedPlace,
eventPlaceLayer,
} = useMapState();
const map = useMap();
const [showGroups, setShowGroups] = useState(false);
// Preload all geodata layers in the background on map load
// This ensures layers are instantly accessible when clicked
const { layerTitles, layers: preloadedLayers } = usePreloadGeodataLayers();
// Pass preloaded layers from TanStack Query cache to get displayed layers
const displayedLayers = getDisplayedLayers(preloadedLayers);
const onMapClick = useCallback(() => {
setSelectedPlacemark(null);
selectEventPlace(null);
}, [setSelectedPlacemark, selectEventPlace]);
useEffect(() => {
if (!map) return;
if (isInitial) {
map.setZoom(zoom);
map.setCenter(center);
}
}, [isInitial, zoom, center]);
const debSetZoom = useMemo(() => debounce(setZoom, 500), [setZoom]);
const debSetCenter = useMemo(() => debounce(setCenter, 500), [setCenter]);
return (
<div className={twMerge("w-full h-[50dvh] flex relative", className)}>
<button
className="top-3 right-3 z-10 absolute h-14 rounded-full aspect-square bg-black flex items-center justify-center"
type="button"
onClick={resetState}
>
<RotateCcw className="stroke-primary" />
</button>
<Sheet open={showGroups} onOpenChange={setShowGroups}>
<SheetTrigger>
<div className="absolute z-10 top-3 left-3 aspect-square h-14 rounded-full bg-black flex items-center justify-center">
<Layers className="w-6 h-6 text-primary" />
</div>
</SheetTrigger>
<SheetContent
side="left"
className="w-[320px] sm:w-[380px] p-0"
aria-describedby={undefined}
>
{/* Header */}
<div className="border-b bg-background px-4 py-4">
<SheetTitle className="flex items-center gap-2 text-xl font-semibold text-primary">
<MapIcon className="h-5 w-5 text-primary" />
Map Layers
</SheetTitle>
<p className="text-sm text-muted-foreground mt-1">
Toggle visibility and explore locations
</p>
</div>
{/* Scrollable content */}
<div className="h-[calc(100%-85px)] overflow-y-auto px-4 py-3">
<EventsLayerControl
closeGroups={() => {
setShowGroups(false);
}}
/>
{/* Divider */}
{layerTitles.length > 0 && (
<div className="relative py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center">
<span className="bg-background px-3 text-xs text-muted-foreground uppercase tracking-wider">
Geodata Layers
</span>
</div>
</div>
)}
{layerTitles
.filter((title) => ["trails", "places"].includes(title))
.map((title) => (
<LayerControl
key={title}
layerName={title}
showInfo={setSelectedPlacemark}
closeGroups={() => setShowGroups(false)}
/>
))}
</div>
</SheetContent>
</Sheet>
<GMap
className="flex-1"
mapId="fb99876bf33e90419a932304"
defaultCenter={
initialCenter ?? center ?? { lat: -8.2980705, lng: 115.088186 }
}
defaultZoom={zoom ?? 15}
onClick={onMapClick}
gestureHandling={requireTwoFingerScroll ? "cooperative" : "greedy"}
colorScheme="DARK"
reuseMaps={true}
mapTypeId="satellite"
disableDefaultUI
restriction={{
latLngBounds: {
north: -8.0,
south: -8.9,
west: 114.4,
east: 115.8,
},
strictBounds: false,
}}
onZoomChanged={(ev) => {
debSetZoom(ev.detail.zoom);
}}
onCenterChanged={(ev) => {
debSetCenter(ev.detail.center as LatLng);
}}
>
{Object.values(displayedLayers).map((layer) => {
return layer.map((placemark, idx) => (
<Placemark
onClick={(placemark) => setSelectedPlacemark(placemark)}
// Fix: Use unique identifier instead of index for stable key
key={`${placemark.name}-${placemark.type}-${JSON.stringify(placemark.coordinates[0])}-${idx}`}
placemark={placemark}
opacity={layersOpacity}
/>
));
})}
{Object.values(eventPlaceLayer).map((place) => {
const coord = place.geometry.coordinates[0];
if (!coord) return null;
return (
<AdvancedMarker
onClick={() => selectEventPlace(place.id)}
key={place.id}
position={coord}
>
<Pin
background={"#76FF05"}
borderColor={"#000000"}
glyphColor={"#000000"}
scale={2}
/>
</AdvancedMarker>
);
})}
{selectedPlacemark && (
<InfoWindow
pixelOffset={selectedPlacemark.type === "point" ? [0, -25] : [0, 0]}
headerDisabled
disableAutoPan
position={getPlacemarkPosition(selectedPlacemark)}
onCloseClick={() => setSelectedPlacemark(null)}
className="text-lg"
>
<div className="min-w-[200px]">
<div className="flex justify-between items-center gap-12">
<h2 className="text-2xl">{selectedPlacemark.name}</h2>
<button
className="h-8 aspect-square flex items-center justify-center ml-auto mr-0"
type="button"
onClick={() => setSelectedPlacemark(null)}
>
<X className="w-full h-full stroke-secondary/70" />
</button>
</div>
{selectedPlacemark.attributes && (
<FeatureAttributes attributes={selectedPlacemark.attributes} />
)}
<div className="mt-2 text-xs text-muted-foreground capitalize">
Type: {selectedPlacemark.type}
</div>
</div>
</InfoWindow>
)}
{selectedPlace && (
<InfoWindow
headerDisabled
position={selectedPlace.geometry.coordinates[0]}
>
<div className="flex justify-between items-center gap-12">
<h2 className="text-primary text-xl font-bold">
{selectedPlace.title}
</h2>
<button
className="h-8 aspect-square flex items-center justify-center ml-auto mr-0"
type="button"
onClick={() => selectEventPlace(null)}
>
<X className="w-full h-full stroke-secondary/70" />
</button>
</div>
<PlacemarkEventList events={selectedPlace.events} />
</InfoWindow>
)}
<MapLongPressHandler
onLongPressMs={700}
onLongPress={longPressHandler}
/>
{children}
</GMap>
</div>
);
};