MapInstance.res
1module WaypointMarker = {
2 @jsx.component
3 let make = (~id: string, ~lon: float, ~lat: float, ~label: string, ~error: array<string>) => {
4 <>
5 <Maplibre.Marker
6 key={id}
7 longitude={lon}
8 latitude={lat}
9 anchor="center"
10 opacity={1.0}
11 opacityWhenCovered={1.0}
12 >
13 <Icons.Waypoint
14 size={24}
15 color={error->Array.length == 0 ? "var(--color-blue)" : "var(--color-red)"}
16 strokeWidth={3}
17 className="rounded-full bg-[#FFFFFF66]"
18 />
19 </Maplibre.Marker>
20
21 // hide labels when zoomed out at world level
22 {State.zoom->Preact.Signal.get > 3.0
23 ? <Maplibre.Marker
24 key={id ++ "-text"}
25 longitude={lon}
26 latitude={lat}
27 anchor="top-left"
28 offset=(6, 6)
29 color="#333333"
30 opacity={1.0}
31 opacityWhenCovered={1.0}
32 >
33 <div className="bg-[#333333] text-white rounded-sm px-1"> {label->Preact.string} </div>
34 </Maplibre.Marker>
35 : Preact.null}
36 </>
37 }
38}
39
40// container for the distance popup, created outside to maintain state across renders
41let popup: Preact.Signal.t<option<Jsx.element>> = Preact.Signal.make(None)
42let initialRender = Preact.Signal.make(true)
43
44@jsx.component
45let make = (~doc: string) => {
46 let waypoints = State.getOrCreateWaypointsSignal(doc)
47 let cmap = Preact.useRef(Nullable.null)
48
49 // Helper to safely fly to a point when map is available
50 let flyToPoint = (point: (float, float), padding: option<(int, int)>) => {
51 switch (cmap.current->Nullable.toOption, padding) {
52 | (Some(map), Some(padding)) => {
53 let (bottom, left) = padding
54 map->Maplibre.flyTo({
55 center: point,
56 padding: {"bottom": bottom, "left": left, "top": 0, "right": 0},
57 })
58 }
59 | (Some(map), _) => map->Maplibre.flyTo({center: point})
60 | (None, _) => ()
61 }
62 }
63
64 // Extract route hashes from waypoints for rendering
65 let routeHashes = Route.extractRouteHashes(waypoints->Preact.Signal.get)
66 let currentWaypoints = waypoints->Preact.Signal.get
67
68 <Maplibre.Map
69 id="map"
70 cursor={#crosshair}
71 initialViewState={
72 latitude: 27.70972,
73 longitude: 83.85382,
74 zoom: 8.0,
75 }
76 onContextMenu={evt => ContextMenu.show->Preact.Signal.set(Some(evt))}
77 onMoveEnd={event => {
78 // update map state for other components read-only view
79 State.center->Preact.Signal.set((event.viewState.longitude, event.viewState.latitude))
80 State.zoom->Preact.Signal.set(event.viewState.zoom)
81 Log.debug(
82 "move",
83 ~kv={
84 "lat": event.viewState.latitude->Float.toFixed(~digits=6),
85 "lng": event.viewState.longitude->Float.toFixed(~digits=6),
86 "zoom": event.viewState.zoom->Float.toFixed(~digits=1),
87 },
88 )
89 }}
90 onLoad={event => {
91 // on initial render or reload, focus the whole route around the waypoints bbox
92 if initialRender->Preact.Signal.get && currentWaypoints->Array.length > 0 {
93 let map = event.target
94 let lats = currentWaypoints->Array.map(p => {
95 let (_, lat) = p.point
96 lat
97 })
98 let lons = currentWaypoints->Array.map(p => {
99 let (lon, _) = p.point
100 lon
101 })
102 let bbox = (lons->Math.minMany, lats->Math.minMany, lons->Math.maxMany, lats->Math.maxMany)
103 let isSinglePoint = currentWaypoints->Array.length == 1
104 if Window.isMobile->Preact.Signal.get {
105 // small screens, fit it above the half-height sheet
106 switch (Window.getSize(), isSinglePoint) {
107 | (Some(size), false) =>
108 map->Maplibre.fitBoundsWithOptions(
109 bbox,
110 {padding: {"bottom": size.height / 2 + 50, "left": 50, "top": 50, "right": 50}},
111 )
112 | (Some(size), true) =>
113 currentWaypoints
114 ->Array.get(0)
115 ->Option.map(wp => flyToPoint(wp.point, Some((size.height / 2, 0))))
116 ->Option.getOr()
117 | (None, _) => map->Maplibre.fitBounds(bbox)
118 }
119 } else {
120 // large screens, estimate it about 1/4 width, could be partially covered by the sheet
121 switch (Window.getSize(), isSinglePoint) {
122 | (Some(size), false) =>
123 map->Maplibre.fitBoundsWithOptions(
124 bbox,
125 {padding: {"bottom": 50, "left": size.width / 4, "top": 50, "right": 50}},
126 )
127 | (Some(size), true) =>
128 currentWaypoints
129 ->Array.get(0)
130 ->Option.map(wp => flyToPoint(wp.point, Some((0, size.width / 4))))
131 ->Option.getOr()
132 | (None, _) => map->Maplibre.fitBounds(bbox)
133 }
134 }
135 }
136 // prevent controlled movement after initial render
137 initialRender->Preact.Signal.set(false)
138
139 // set initial map center and zoom
140 event.target
141 ->Maplibre.getCenter
142 ->((center: Maplibre.center) => {State.center->Preact.Signal.set((center.lng, center.lat))})
143 State.zoom->Preact.Signal.set(event.target->Maplibre.getZoom)
144
145 // set map at top level for use in calling map methods
146 cmap.current = event.target->Nullable.make
147 ()
148 }}
149 onMouseLeave={_event => popup->Preact.Signal.set(None)}
150 onMouseEnter={event => MapEvents.handleDistance(~event, ~popup, ~waypoints=currentWaypoints)}
151 //onMouseDown={event => MapEvents.handleDistance(~event, ~popup, ~waypoints=currentWaypoints)}
152 interactiveLayerIds={routeHashes->Array.map(hash => `route-layer-${hash}`)}
153 //mapStyle=Maplibre.defaultStyle
154 mapStyle=Style.rtwStyle
155 >
156 // Render route sources and layers with URL-based GeoJSON
157 // Service worker handles caching with CacheFirst strategy
158 {routeHashes
159 ->Array.map(hash => {
160 <Preact.jsxFragment key={`route-fragment-${hash}`}>
161 <Maplibre.Source
162 id={`route-${hash}`} type_="geojson" data={Route.getGeoJSONUrl(hash)->Obj.magic}
163 />
164 <Maplibre.Layer
165 id={`route-layer-${hash}`}
166 type_="line"
167 source={`route-${hash}`}
168 paint={
169 let paintDict = Dict.make()
170 paintDict->Dict.set("line-color", JSON.Encode.string("#205ea6"))
171 paintDict->Dict.set("line-width", JSON.Encode.float(8.0))
172 paintDict->JSON.Encode.object
173 }
174 />
175 </Preact.jsxFragment>
176 })
177 ->Preact.array}
178
179 // Render waypoint markers on top
180 {switch currentWaypoints {
181 | [] => <> </>
182 | [wp1] => {
183 // one point = initial departure point => center map for better planning
184 // add some padding about 25% screen size based on form factor to move the
185 // centered point off the panel edge in worst case dimensions
186 let (lon, lat) = wp1.point
187 if Window.isMobile->Preact.Signal.get {
188 switch Window.getSize() {
189 | Some(size) => flyToPoint(wp1.point, Some((size.height / 4, 0))) // bottom padding on mobile
190 | None => flyToPoint(wp1.point, None)
191 }
192 } else {
193 switch Window.getSize() {
194 | Some(size) => flyToPoint(wp1.point, Some((0, size.width / 4))) // left padding
195 | None => flyToPoint(wp1.point, None)
196 }
197 }
198 <WaypointMarker
199 lon={lon} lat={lat} id={wp1.id} label={wp1.label} error={wp1.error->Option.getOr([])}
200 />
201 }
202 // multiple points, remain uncontrolled, expect user to adjust
203 // initial render relies on server setting the initData for view state to bbox
204 | wp =>
205 wp
206 ->Array.map((wp: Waypoint.attributes) => {
207 let (lon, lat) = wp.point
208 <WaypointMarker
209 lon={lon} lat={lat} id={wp.id} label={wp.label} error={wp.error->Option.getOr([])}
210 />
211 })
212 ->Preact.array
213 }}
214 {popup->Preact.Signal.get->Preact.option}
215 <ContextMenu doc={doc} />
216 </Maplibre.Map>
217}