'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 } const TILE_URL = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' const MARKER_COLORS: Record = { place: '#3b82f6', studio: '#a855f7', visitor: '#22c55e', visitor_registered: '#FF4907', location: '#3b82f6', } function createIcon(type: string, size: number, ghost?: boolean): L.DivIcon { const color = ghost ? 'radial-gradient(circle, rgba(255, 238, 0, 1) 0%, rgba(255, 238, 0, 0) 75%)' : MARKER_COLORS[type] || '#3b82f6' if (type === 'location') { const locStyle = ghost ? `width:10px;height:10px;background:radial-gradient(circle, rgba(255, 238, 0, 1) 0%, rgba(255, 238, 0, 0) 75%);border-radius:50%` : `width:12px;height:12px;background:#3b82f6;border-radius:50%;border:3px solid white;box-shadow:0 0 0 4px rgba(59,130,246,0.3)` const s = ghost ? 10 : 20 return L.divIcon({ className: '', iconSize: [s, s], iconAnchor: [s / 2, s / 2], html: `
`, }) } const ghostSize = 10 const style = ghost ? `width:${ghostSize}px;height:${ghostSize}px;background:${color};border-radius:50%` : `width:${size}px;height:${size}px;background:${color};border-radius:50%;border:${size > 20 ? '3px' : '2px'} solid white;box-shadow:0 2px 8px rgba(0,0,0,0.3)` return L.divIcon({ className: '', iconSize: ghost ? [ghostSize, ghostSize] : [size, size], iconAnchor: ghost ? [ghostSize / 2, ghostSize / 2] : [size / 2, 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[1], center[0]], zoom) L.tileLayer(TILE_URL, { maxZoom: 19, crossOrigin: 'anonymous', }).addTo(map) map.on('moveend', () => { if (!moveHandler || !map) return const c = map.getCenter() const b = map.getBounds() moveHandler( [c.lng, c.lat], 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 ghost = options?.ghost const type = options?.type || 'place' const size = type === 'visitor' || type === 'visitor_registered' || type === 'location' ? 12 : 32 const icon = createIcon(type, size, 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[0][1], bounds[0][0]], [bounds[1][1], bounds[1][0]], ]) }, 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, } }, } }