map.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. 'use client'
  2. import L from 'leaflet'
  3. import 'leaflet/dist/leaflet.css'
  4. export interface MapBounds {
  5. swLat: number
  6. swLng: number
  7. neLat: number
  8. neLng: number
  9. }
  10. export interface MarkerOptions {
  11. type?: 'place' | 'studio' | 'visitor' | 'visitor_registered' | 'location'
  12. title?: string
  13. onClick?: () => void
  14. ghost?: boolean
  15. color?: string
  16. }
  17. const TILE_URL = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
  18. const MARKER_COLORS: Record<string, string> = {
  19. place: '#FFF82A',
  20. studio: '#FF80DF',
  21. visitor: '#FFF82A',
  22. visitor_registered: '#FF4B4B',
  23. location: '#FFF82A',
  24. }
  25. const MARKER_SIZE = 7
  26. function createIcon(color: string, ghost?: boolean): L.DivIcon {
  27. const bg = ghost
  28. ? 'radial-gradient(circle, rgba(255, 238, 0, 1) 0%, rgba(255, 238, 0, 0) 75%)'
  29. : color
  30. return L.divIcon({
  31. className: '',
  32. iconSize: [MARKER_SIZE, MARKER_SIZE],
  33. iconAnchor: [MARKER_SIZE / 2, MARKER_SIZE / 2],
  34. html: `<div style="width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;background:${bg};border-radius:50%"></div>`,
  35. })
  36. }
  37. export interface MapProvider {
  38. init(container: HTMLElement, center: [number, number], zoom: number): Promise<void>
  39. destroy(): void
  40. isReady(): boolean
  41. addMarker(id: string, lat: number, lng: number, options?: MarkerOptions): void
  42. removeMarker(id: string): void
  43. setCenter(lat: number, lng: number): void
  44. fitBounds(bounds: [[number, number], [number, number]]): void
  45. onMove(cb: (center: [number, number], zoom: number, bounds: MapBounds) => void): void
  46. getBounds(): MapBounds | null
  47. }
  48. export function createLeafletMapProvider(): MapProvider {
  49. let map: L.Map | null = null
  50. let markers = new Map<string, L.Marker>()
  51. let moveHandler: ((center: [number, number], zoom: number, bounds: MapBounds) => void) | null = null
  52. return {
  53. async init(container, center, zoom) {
  54. try {
  55. map = L.map(container, {
  56. zoomControl: false,
  57. attributionControl: false,
  58. }).setView(center, zoom)
  59. L.tileLayer(TILE_URL, {
  60. maxZoom: 19,
  61. crossOrigin: 'anonymous',
  62. opacity: 0.92,
  63. }).addTo(map)
  64. map.on('moveend', () => {
  65. if (!moveHandler || !map) return
  66. const c = map.getCenter()
  67. const b = map.getBounds()
  68. moveHandler(
  69. [c.lat, c.lng],
  70. map.getZoom(),
  71. {
  72. swLat: b.getSouthWest().lat,
  73. swLng: b.getSouthWest().lng,
  74. neLat: b.getNorthEast().lat,
  75. neLng: b.getNorthEast().lng,
  76. },
  77. )
  78. })
  79. } catch {
  80. map = null
  81. }
  82. },
  83. isReady() {
  84. return map !== null
  85. },
  86. destroy() {
  87. markers.clear()
  88. map?.remove()
  89. map = null
  90. },
  91. addMarker(id, lat, lng, options) {
  92. if (!map) return
  93. const existing = markers.get(id)
  94. if (existing) existing.remove()
  95. const ghost = options?.ghost
  96. const type = options?.type || 'place'
  97. const color = options?.color || MARKER_COLORS[type] || '#FFF82A'
  98. const icon = createIcon(color, ghost)
  99. const interactive = !ghost
  100. const marker = L.marker([lat, lng], { icon, interactive }).addTo(map)
  101. if (options?.title && !ghost) {
  102. marker.bindTooltip(options.title, { direction: 'top' })
  103. }
  104. if (options?.onClick && !ghost) {
  105. marker.on('click', () => options.onClick?.())
  106. }
  107. markers.set(id, marker)
  108. },
  109. removeMarker(id) {
  110. const marker = markers.get(id)
  111. if (marker) {
  112. marker.remove()
  113. markers.delete(id)
  114. }
  115. },
  116. setCenter(lat, lng) {
  117. map?.setView([lat, lng])
  118. },
  119. fitBounds(bounds) {
  120. map?.fitBounds(bounds)
  121. },
  122. onMove(cb) {
  123. moveHandler = cb
  124. },
  125. getBounds() {
  126. if (!map) return null
  127. const b = map.getBounds()
  128. return {
  129. swLat: b.getSouthWest().lat,
  130. swLng: b.getSouthWest().lng,
  131. neLat: b.getNorthEast().lat,
  132. neLng: b.getNorthEast().lng,
  133. }
  134. },
  135. }
  136. }