Powered by AppSignal & Oban Pro

Climb detection from GPX files

notebooks/introduction.livemd

Climb detection from GPX files

Mix.install([{:roadbook, path: "."}])

Load data from a GPX file

Data can be loaded from a GPX file extracted from Komoot. A first stats from the profile can be computed (Total elevation, total descent, total distance in meters)

route = Roadbook.Positioning.GpxParser.read("priv/gps/test_komoot.gpx")
route = Roadbook.Positioning.Track.compute(route)

stats = Roadbook.Positioning.ElevationProfile.stats(List.first(route.profiles))

Track map rendering

A map can be rendered from the first track segment.

segment = List.first(route.segments)
point = List.first(segment.points)

MapLibre.new(
  style: :terrain,
  center: {point.lon, point.lat},
  zoom: 9
)
|> MapLibre.add_table_source(
  "map",
  %{
    "lat" => Enum.map(segment.points, fn x -> x.lat end),
    "lon" => Enum.map(segment.points, fn x -> x.lon end)
  },
  {:lng_lat, ["lon", "lat"]}
)
|> MapLibre.add_layer(
  id: "map_circle_1",
  source: "map",
  type: :circle,
  paint: [circle_color: "#edd400", circle_radius: 5, circle_opacity: 0.1]
)
|> MapLibre.add_layer(
  id: "map_circle_2",
  source: "map",
  type: :circle,
  paint: [circle_color: "#000000", circle_radius: 2, circle_opacity: 1]
)

Profile map rendering

The profile maps (elevation and elevation gain) can be rendered.

profile =
  route.profiles
  |> List.first()
  |> Map.get(:vectors)
  |> Enum.map(&Map.from_struct/1)
VegaLite.new(width: 800, height: 250, title: "Elevation")
|> VegaLite.data_from_values(profile, only: ["pos", "ele"])
|> VegaLite.mark(:point)
|> VegaLite.encode_field(:x, "pos", type: :quantitative)
|> VegaLite.encode_field(:y, "ele", type: :quantitative)
VegaLite.new(width: 800)
|> VegaLite.data_from_values(profile, only: ["pos", "gai", "is_climb"])
|> VegaLite.mark(:bar)
|> VegaLite.encode_field(:x, "pos", type: :quantitative)
|> VegaLite.encode_field(:y, "gai", type: :quantitative)
|> VegaLite.encode_field(:color, "is_climb", type: :nominal)

Climb detector

Climbs (a continuous segment of positive elevation gains ) can be detected. From the detection, each climb average slope per distance unit (100m, 1km) can be computed.

climbs = Roadbook.Climbs.ClimbDetector.find_climbs(profile)
profile = Roadbook.Climbs.ClimbDetector.annotate_climbs(profile)
VegaLite.new(width: 800)
|> VegaLite.data_from_values(profile, only: ["pos", "ele", "is_climb"])
|> VegaLite.mark(:point)
|> VegaLite.encode_field(:x, "pos", type: :quantitative)
|> VegaLite.encode_field(:y, "ele", type: :quantitative)
|> VegaLite.encode_field(:color, "is_climb", type: :quantitative)
profile1000 =
  climbs
  |> Enum.map(fn range ->
    profile
    |> Enum.filter(fn p -> p.pos in range end)
    |> Enum.map(&struct(Roadbook.Positioning.Point, &1))
    |> then(&%Roadbook.Climbs.ClimbProfile{segment: &1})
    |> Roadbook.Climbs.ClimbProfile.breakdown(1000)
    |> Enum.with_index(fn s, i -> Map.merge(s, %{pos: i}) end)
  end)
  |> Enum.at(2)

profile100 =
  climbs
  |> Enum.map(fn range ->
    profile
    |> Enum.filter(fn p -> p.pos in range end)
    |> Enum.map(&struct(Roadbook.Positioning.Point, &1))
    |> then(&%Roadbook.Climbs.ClimbProfile{segment: &1})
    |> Roadbook.Climbs.ClimbProfile.breakdown(100)
    |> Enum.with_index(fn s, i -> Map.merge(s, %{pos: i}) end)
  end)
  |> Enum.at(2)
VegaLite.new(width: 800)
|> VegaLite.data_from_values(profile100, only: ["pos", "slope"])
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "pos", type: :quantitative)
|> VegaLite.encode_field(:y, "slope", type: :quantitative)
VegaLite.new(width: 800)
|> VegaLite.data_from_values(profile1000, only: ["pos", "slope"])
|> VegaLite.mark(:line)
|> VegaLite.encode_field(:x, "pos", type: :quantitative)
|> VegaLite.encode_field(:y, "slope", type: :quantitative)