BT  ·  2026-06-05

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}