Compute an isochrone with Thinwrap (Gleam)

Compute a travel-time or travel-distance polygon via thinwrap_location's isochrone/2 in Gleam — one call shape for Mapbox, HERE, ESRI and TomTom.

1. Install

gleam add thinwrap_location gleam_erlang envoy

2. Send the call

import envoy
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/atom
import gleam/float
import gleam/int
import gleam/io
import gleam/list

@external(erlang, "thinwrap_location", "isochrone")
fn isochrone(config: Dynamic, input: Dynamic) -> Result(Dynamic, Dynamic)

pub fn main() -> Nil {
  let assert Ok(token) = envoy.get("MAPBOX_ACCESS_TOKEN")
  let config =
    obj([
      #("provider", atom("mapbox")),
      #("access_token", dynamic.string(token)),
    ])
  let input =
    obj([
      #("center", at(52.52, 13.405)),
      #("type", atom("time")),
      // seconds
      #("values", dynamic.list([dynamic.int(300), dynamic.int(600)])),
      #("travel_mode", atom("driving")),
    ])

  case isochrone(config, input) {
    // Each contour carries a GeoJSON Polygon (or MultiPolygon) of [lng, lat] rings.
    Ok(res) -> {
      let contour = {
        use value <- decode.field(key("value"), decode.float)
        use kind <- decode.subfield(
          [key("geometry"), key("type")],
          decode.string,
        )
        use rings <- decode.subfield(
          [key("geometry"), key("coordinates")],
          decode.list(decode.dynamic),
        )
        decode.success(
          float.to_string(value)
          <> " "
          <> kind
          <> " "
          <> int.to_string(list.length(rings)),
        )
      }
      let assert Ok(lines) =
        decode.run(res, decode.at([key("contours")], decode.list(contour)))
      list.each(lines, io.println)
    }
    Error(err) -> io.println(describe(err))
  }
}

// The library speaks atom-keyed Erlang maps; these helpers build and read them.
fn key(name: String) -> atom.Atom {
  atom.create(name)
}

fn atom(name: String) -> Dynamic {
  atom.to_dynamic(atom.create(name))
}

fn obj(fields: List(#(String, Dynamic))) -> Dynamic {
  dynamic.properties(list.map(fields, fn(f) { #(atom(f.0), f.1) }))
}

fn at(lat: Float, lng: Float) -> Dynamic {
  obj([#("lat", dynamic.float(lat)), #("lng", dynamic.float(lng))])
}

fn describe(err: Dynamic) -> String {
  let error = {
    use code <- decode.field(key("code"), atom.decoder())
    use message <- decode.field(key("message"), decode.string)
    decode.success(atom.to_string(code) <> ": " <> message)
  }
  let assert Ok(line) = decode.run(err, error)
  line
}

Same code works for all supported providers — just change the provider ID.