forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMapView.tsx
More file actions
94 lines (84 loc) · 3.38 KB
/
Copy pathMapView.tsx
File metadata and controls
94 lines (84 loc) · 3.38 KB
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// The map canvas itself. Chrome (header, ribbon, tab bar, controls) composes
// around this rather than living inside it.
//
// Everything delicate here is lifecycle. A map built twice means two WebGL
// contexts, two GPS watchers and doubled range reads against an on-device
// archive that can be 1.18 GB; a map never torn down leaks all of the same.
// React StrictMode mounts, unmounts and remounts on purpose in development to
// surface exactly that, so the effect below is written to survive it: build
// once per effect run, and fully undo the build on cleanup.
import { useEffect, useRef, useState } from 'react'
import { Map as MapLibreMap } from 'maplibre-gl'
import { registerPMTilesProtocol } from './protocol'
import { buildMapStyle } from './style'
import { attachMapChrome, type ScaleUnits } from './mapChrome'
export interface MapViewProps {
/** `pmtiles://` URL for the downloaded topo archive. */
topoArchiveUrl: string
/** Local URL of the exported trail lines. */
trailsUrl: string
/** Initial centre only - later camera moves go through the map imperatively. */
center?: [number, number]
/** Initial zoom only. */
zoom?: number
/** Web only; touch platforms rely on pinch (see mapChrome.ts). */
showZoomButtons?: boolean
units?: ScaleUnits
}
const DEFAULT_CENTER: [number, number] = [-77.1, 39.3]
const DEFAULT_ZOOM = 12
export function MapView({
topoArchiveUrl,
trailsUrl,
center,
zoom,
showZoomButtons = false,
units = 'imperial',
}: MapViewProps) {
const containerRef = useRef<HTMLDivElement | null>(null)
const [map, setMap] = useState<MapLibreMap | null>(null)
// `center`/`zoom` are deliberately NOT dependencies. A parent writing
// center={[x, y]} inline hands over a new array identity on every render; if
// that drove this effect the map would be destroyed and rebuilt each time the
// parent re-rendered. They seed the initial camera, and nothing more.
useEffect(() => {
const container = containerRef.current
if (container === null) return
// The style resolves pmtiles:// URLs, so the protocol has to exist first.
registerPMTilesProtocol()
const created = new MapLibreMap({
container,
style: buildMapStyle({ topoArchiveUrl, trailsUrl }),
center: center ?? DEFAULT_CENTER,
zoom: zoom ?? DEFAULT_ZOOM,
// Attribution is rendered by the app's own chrome, positioned per
// WIREFRAMES.md, rather than by MapLibre's default control.
attributionControl: false,
})
setMap(created)
return () => {
created.remove()
setMap(null)
}
// Intentionally omitting `center`/`zoom` - see the note above. Including
// them would rebuild the whole map whenever a parent re-rendered with an
// inline array, which is the bug this omission exists to avoid.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [topoArchiveUrl, trailsUrl])
// Chrome lives in its own effect so that changing a display preference -
// switching the scale bar to metric, say - re-attaches three controls
// instead of tearing down and rebuilding the entire map underneath the hiker.
useEffect(() => {
if (map === null) return
return attachMapChrome(map, { showZoomButtons, units })
}, [map, showZoomButtons, units])
return (
<div
ref={containerRef}
className="map-view"
role="region"
aria-label="Trail map"
data-testid="map-view"
/>
)
}