| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- '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' | '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<string, string> = {
- place: '#3b82f6',
- studio: '#a855f7',
- visitor: '#22c55e',
- location: '#3b82f6',
- }
- function createIcon(type: string, size: number, ghost?: boolean): L.DivIcon {
- const color = ghost
- ? 'radial-gradient(circle, rgba(255, 215, 0, 0.9) 0%, rgba(255, 215, 0, 0.3) 50%, rgba(255, 215, 0, 0) 70%)'
- : MARKER_COLORS[type] || '#3b82f6'
- if (type === 'location') {
- const locStyle = ghost
- ? `width:30px;height:30px;background:radial-gradient(circle, rgba(255, 215, 0, 0.9) 0%, rgba(255, 215, 0, 0.3) 50%, rgba(255, 215, 0, 0) 70%);border-radius:50%;box-shadow:0 0 20px rgba(255, 215, 0, 0.4);pointer-events:none`
- : `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 size = ghost ? 30 : 20
- return L.divIcon({
- className: '',
- iconSize: [size, size],
- iconAnchor: [size / 2, size / 2],
- html: `<div style="${locStyle}"></div>`,
- })
- }
- const style = ghost
- ? `width:${size * 2.5}px;height:${size * 2.5}px;background:${color};border-radius:50%;box-shadow:0 0 20px rgba(255, 215, 0, 0.4);pointer-events:none`
- : `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 ? [size * 2.5, size * 2.5] : [size, size],
- iconAnchor: ghost ? [size * 2.5 / 2, size * 2.5 / 2] : [size / 2, size / 2],
- html: `<div style="${style}"></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[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 === '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,
- }
- },
- }
- }
|