'use client' import L from 'leaflet' import 'leaflet/dist/leaflet.css' export interface MapBounds { swLat: number swLng: number neLat: number neLng: number } export interface MarkerOptions { type?: 'place' | 'studio' | 'visitor' | 'visitor_registered' | 'location' title?: string onClick?: () => void ghost?: boolean color?: string } const TILE_URL = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' const MARKER_COLORS: Record = { place: '#FFF82A', studio: '#FF80DF', visitor: '#FFF82A', visitor_registered: '#FF4B4B', location: '#FFF82A', } const MARKER_SIZE = 7 function createIcon(color: string, ghost?: boolean): L.DivIcon { const bg = ghost ? 'radial-gradient(circle, rgba(255, 238, 0, 1) 0%, rgba(255, 238, 0, 0) 75%)' : color return L.divIcon({ className: '', iconSize: [MARKER_SIZE, MARKER_SIZE], iconAnchor: [MARKER_SIZE / 2, MARKER_SIZE / 2], html: `
`, }) } export interface MapProvider { init(container: HTMLElement, center: [number, number], zoom: number): Promise destroy(): void isReady(): boolean addMarker(id: string, lat: number, lng: number, options?: MarkerOptions): void removeMarker(id: string): void setCenter(lat: number, lng: number): void fitBounds(bounds: [[number, number], [number, number]]): void onMove(cb: (center: [number, number], zoom: number, bounds: MapBounds) => void): void getBounds(): MapBounds | null } export function createLeafletMapProvider(): MapProvider { let map: L.Map | null = null let markers = new Map() let moveHandler: ((center: [number, number], zoom: number, bounds: MapBounds) => void) | null = null return { async init(container, center, zoom) { try { map = L.map(container, { zoomControl: false, attributionControl: false, }).setView(center, zoom) L.tileLayer(TILE_URL, { maxZoom: 19, crossOrigin: 'anonymous', opacity: 0.92, }).addTo(map) map.on('moveend', () => { if (!moveHandler || !map) return const c = map.getCenter() const b = map.getBounds() moveHandler( [c.lat, c.lng], map.getZoom(), { swLat: b.getSouthWest().lat, swLng: b.getSouthWest().lng, neLat: b.getNorthEast().lat, neLng: b.getNorthEast().lng, }, ) }) } catch { map = null } }, isReady() { return map !== null }, destroy() { markers.clear() map?.remove() map = null }, addMarker(id, lat, lng, options) { if (!map) return const existing = markers.get(id) if (existing) existing.remove() const ghost = options?.ghost const type = options?.type || 'place' const color = options?.color || MARKER_COLORS[type] || '#FFF82A' const icon = createIcon(color, ghost) const interactive = !ghost const marker = L.marker([lat, lng], { icon, interactive }).addTo(map) if (options?.title && !ghost) { marker.bindTooltip(options.title, { direction: 'top' }) } if (options?.onClick && !ghost) { marker.on('click', () => options.onClick?.()) } markers.set(id, marker) }, removeMarker(id) { const marker = markers.get(id) if (marker) { marker.remove() markers.delete(id) } }, setCenter(lat, lng) { map?.setView([lat, lng]) }, fitBounds(bounds) { map?.fitBounds(bounds) }, onMove(cb) { moveHandler = cb }, getBounds() { if (!map) return null const b = map.getBounds() return { swLat: b.getSouthWest().lat, swLng: b.getSouthWest().lng, neLat: b.getNorthEast().lat, neLng: b.getNorthEast().lng, } }, } }