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)