File size: 2,841 Bytes
008947a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7604e0d
 
 
 
 
008947a
 
 
 
 
 
 
7604e0d
008947a
 
7604e0d
008947a
 
 
 
 
 
 
 
 
 
7604e0d
 
 
 
008947a
 
 
 
 
 
 
 
 
 
 
7604e0d
008947a
 
 
 
 
 
7604e0d
 
008947a
7604e0d
008947a
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { useEffect, useRef } from 'react'
import { AttributionControl, Map, NavigationControl, type Map as MapLibreMap } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import type { BBox } from '../types'

type Props = {
  longitude: number
  latitude: number
  bbox: BBox
  onViewportChange: (bbox: BBox, zoom: number) => void
}

const tileUrl = import.meta.env.VITE_MAP_TILE_URL ?? 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'

export function MapViewport({ longitude, latitude, bbox, onViewportChange }: Props) {
  const container = useRef<HTMLDivElement>(null)
  const mapRef = useRef<MapLibreMap | null>(null)
  const targetViewport = useRef({ longitude, latitude, bbox })

  useEffect(() => {
    targetViewport.current = { longitude, latitude, bbox }
  }, [bbox, latitude, longitude])

  useEffect(() => {
    const element = container.current
    if (!element) return
    let map: MapLibreMap | null = null
    const mount = () => {
      if (map || element.clientWidth === 0 || element.clientHeight === 0) return
      const initial = targetViewport.current
      map = new Map({
        container: element,
        center: [initial.longitude, initial.latitude],
        zoom: 10.8,
        attributionControl: false,
        style: {
          version: 8,
          sources: { osm: { type: 'raster', tiles: [tileUrl], tileSize: 256, attribution: '© OpenStreetMap contributors' } },
          layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
        },
      })
      map.addControl(new NavigationControl({ showCompass: false }), 'top-right')
      map.addControl(new AttributionControl({ compact: true }), 'bottom-right')
      map.fitBounds(
        [[initial.bbox.west, initial.bbox.south], [initial.bbox.east, initial.bbox.north]],
        { padding: 14, animate: false },
      )
      map.on('moveend', () => {
        if (!map) return
        const bounds = map.getBounds()
        onViewportChange({ west: bounds.getWest(), south: bounds.getSouth(), east: bounds.getEast(), north: bounds.getNorth() }, map.getZoom())
      })
      mapRef.current = map
    }
    mount()
    const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(mount)
    observer?.observe(element)
    return () => { observer?.disconnect(); map?.remove(); mapRef.current = null }
  }, [onViewportChange])

  useEffect(() => {
    const map = mapRef.current
    if (!map) return
    const center = map.getCenter()
    if (Math.abs(center.lng - longitude) > 0.01 || Math.abs(center.lat - latitude) > 0.01) {
      const target = targetViewport.current.bbox
      map.fitBounds([[target.west, target.south], [target.east, target.north]], { padding: 14, duration: 450 })
    }
  }, [latitude, longitude])

  return <div className="map-viewport" ref={container} aria-label="Interactive map viewport" />
}