map.ts 3.9 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}.tile.openstreetmap.org/{z}/{x}/{y}.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[1], center[0]], zoom)
  59. L.tileLayer(TILE_URL, {
  60. maxZoom: 19,
  61. crossOrigin: 'anonymous',
  62. }).addTo(map)
  63. map.on('moveend', () => {
  64. if (!moveHandler || !map) return
  65. const c = map.getCenter()
  66. const b = map.getBounds()
  67. moveHandler(
  68. [c.lng, c.lat],
  69. map.getZoom(),
  70. {
  71. swLat: b.getSouthWest().lat,
  72. swLng: b.getSouthWest().lng,
  73. neLat: b.getNorthEast().lat,
  74. neLng: b.getNorthEast().lng,
  75. },
  76. )
  77. })
  78. } catch {
  79. map = null
  80. }
  81. },
  82. isReady() {
  83. return map !== null
  84. },
  85. destroy() {
  86. markers.clear()
  87. map?.remove()
  88. map = null
  89. },
  90. addMarker(id, lat, lng, options) {
  91. if (!map) return
  92. const ghost = options?.ghost
  93. const type = options?.type || 'place'
  94. const color = options?.color || MARKER_COLORS[type] || '#FFF82A'
  95. const icon = createIcon(color, ghost)
  96. const interactive = !ghost
  97. const marker = L.marker([lat, lng], { icon, interactive }).addTo(map)
  98. if (options?.title && !ghost) {
  99. marker.bindTooltip(options.title, { direction: 'top' })
  100. }
  101. if (options?.onClick && !ghost) {
  102. marker.on('click', () => options.onClick?.())
  103. }
  104. markers.set(id, marker)
  105. },
  106. removeMarker(id) {
  107. const marker = markers.get(id)
  108. if (marker) {
  109. marker.remove()
  110. markers.delete(id)
  111. }
  112. },
  113. setCenter(lat, lng) {
  114. map?.setView([lat, lng])
  115. },
  116. fitBounds(bounds) {
  117. map?.fitBounds([
  118. [bounds[0][1], bounds[0][0]],
  119. [bounds[1][1], bounds[1][0]],
  120. ])
  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. }