track_map #
A Lustre component for rendering GPS tracks with Leaflet.
track_map targets JavaScript and runs in the browser.
Installation #
gleam add track_map
Usage #
When using Lustre's development tools, add Leaflet to the generated page:
[tools.lustre.html]
stylesheets = [
{ href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" },
]
scripts = [
{ src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" },
]
Register the component before starting the Lustre app:
pub fn main() {
let assert Ok(Nil) = track_map.register()
let app = lustre.simple(init, update, view)
let assert Ok(_) = lustre.start(app, "#app", Nil)
Nil
}
Render a map directly from the app's view function:
fn view(model: Model) -> Element(Message) {
html.main([], [
track_map.component(model.samples),
])
}
That is the complete setup for a regular Lustre SPA; no server rendering or hydration is involved.
Convert application GPS records into Sample values at the browser boundary:
import gleam/list
import gleam/option.{Some}
import track_map
// Replace NavigationSample with the application's GPS record type.
fn map_samples(
samples: List(NavigationSample),
) -> Result(List(track_map.Sample), track_map.SampleError) {
samples
|> list.try_map(fn(sample) {
track_map.sample(
latitude_e7: sample.latitude_e7,
longitude_e7: sample.longitude_e7,
timestamp_ms: Some(sample.timestamp_ms),
speed_mm_s: Some(sample.speed_mm_s),
)
})
}
sample rejects out-of-range coordinates and negative speeds, so invalid data
is handled before an element or Leaflet map is created.
Server-rendered documents #
track_map still runs in the browser. If the server generates the outer HTML
document, include dependencies in its head instead of configuring
tools.lustre.html:
html.head([], [
track_map.dependencies(),
// Other head elements.
])
The browser entrypoint still calls track_map.register() before
lustre.start. The same view works whether the rest of the app starts from
an empty mount point or hydrates server-rendered HTML.
Coordinates use 1e-7 degrees. Timestamps use Unix milliseconds and speeds
use millimetres per second. Speed colors the route from blue (slow) to red
(fast) relative to the supplied samples. Select any sample to inspect its
recorded time and speed. ● marks a track's start and ■ marks its end.
Dates follow the browser locale; the package adds no translatable UI copy.
Use component_unstyled when the host needs custom attributes or sizing.
Unstyled components need a non-zero height; component provides a 320px
minimum height.
With no samples, the component shows ∅.
Runtime errors #
Map setup and tile loading happen after the element is created. Applications
can listen for those failures using on_error:
import lustre/attribute
pub type Message {
MapFailed(track_map.MapError)
}
fn map(samples: List(track_map.Sample)) {
track_map.component_unstyled(samples, [
track_map.on_error(MapFailed),
attribute.styles([
#("display", "block"),
#("width", "100%"),
#("min-height", "320px"),
]),
])
}
MapError distinguishes LeafletUnavailable, TileLoadingFailed, and
RenderingFailed. The component emits the error without adding an error
message to the page; the application decides how to present or localise it.