Route.res
1/** Route module for handling route data
2 *
3 * Provides functions for extracting route information from waypoints
4 * and decoding GeoJSON feature properties.
5 */
6/** Route type matching Go/TS structure */
7type t = {
8 id: option<string>,
9 d: option<float>,
10 t: option<float>,
11 bbox: option<array<float>>,
12}
13
14/** Extract all route hashes from an array of waypoints
15 *
16 * Iterates through all waypoints and collects route IDs from the `from` field.
17 * Returns unique route hashes for rendering route sources.
18 */
19let extractRouteHashes = (waypoints: array<Waypoint.attributes>): array<string> => {
20 waypoints
21 ->Array.map(waypoint => {
22 switch waypoint.from {
23 | Some(routeDict) =>
24 routeDict
25 ->Dict.toArray
26 ->Array.filterMap(((_key, route)) => {
27 switch route.id {
28 | Some(id) => Some(id)
29 | None => None // Fallback to the key if id is not present
30 }
31 })
32 | None => []
33 }
34 })
35 ->Array.flat
36 ->Array.filter(hash => hash != "") // Filter out empty hashes
37}
38
39/**
40* Strongly typed route properties from GeoJSON features. These must be kept in sync
41* with the backend where they are set
42*/
43type properties = {
44 id: string,
45 distance: float,
46 time: float,
47 bbox: array<float>,
48}
49
50/** Decode route properties from JSON.t
51 *
52 * Returns None if properties are missing or malformed.
53 * Expected shape: {"id": string, "distance": float, "time": float, "bbox": [float, float, float, float]}
54 */
55let decodeProperties: JSON.t => option<properties> = json => {
56 switch json {
57 | Object(dict) => {
58 let id = dict->Dict.get("id")->Option.flatMap(JSON.Decode.string)
59 let distance = dict->Dict.get("distance")->Option.flatMap(JSON.Decode.float)
60 let time = dict->Dict.get("time")->Option.flatMap(JSON.Decode.float)
61 let bbox =
62 dict
63 ->Dict.get("bbox")
64 ->Option.flatMap(JSON.Decode.array)
65 ->Option.map(arr => arr->Array.filterMap(JSON.Decode.float))
66
67 switch (id, distance, time, bbox) {
68 | (Some(id), Some(distance), Some(time), Some(bbox)) => Some({id, distance, time, bbox})
69 | _ => None
70 }
71 }
72 | _ => None
73 }
74}
75
76/** Convenience function to extract route properties from a mouse event
77 *
78 * Returns an array of successfully decoded route properties from all features.
79 * Features with missing or invalid properties are filtered out.
80 */
81let extractFromEvent: Maplibre.mapLayerMouseEvent => array<properties> = event => {
82 switch event.features {
83 | Some(features) =>
84 features->Array.filterMap(feature => {
85 decodeProperties(feature.properties)
86 })
87 | None => []
88 }
89}
90
91/** Extract the first route property from a mouse event
92 *
93 * Useful when expecting a single feature (e.g., hovering over one route segment).
94 * Returns None if no features or if properties can't be decoded.
95 */
96let extractFirst: Maplibre.mapLayerMouseEvent => option<properties> = event => {
97 switch event.features {
98 | Some(features) =>
99 features
100 ->Array.get(0)
101 ->Option.flatMap(feature => decodeProperties(feature.properties))
102 | None => None
103 }
104}
105
106/** Generate the URL for a route GeoJSON file
107 *
108 * Routes are served from /route/{hash}.geojson
109 * The service worker caches these with CacheFirst strategy.
110 */
111let getGeoJSONUrl = (hash: string): string => {
112 `/route/${hash}.geojson`
113}