| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- '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<string, string> = {
- 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: `<div style="width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;background:${bg};border-radius:50%"></div>`,
- })
- }
- export interface MapProvider {
- init(container: HTMLElement, center: [number, number], zoom: number): Promise<void>
- 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<string, L.Marker>()
- 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,
- }
- },
- }
- }
|