Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for waypoints and routes #3

Merged
merged 5 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions lib/gpx_ex/gpx.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
defmodule GpxEx.Gpx do
@type t :: %__MODULE__{tracks: list(GpxEx.Track.t)}
@typedoc """
A struct representing a fully parsed GPX file.

defstruct tracks: nil
GPX files may contain zero or more:

- Tracks—ordered lists of points describing a path someone traveled.
- Routes—ordered lists of waypoints representing a series of turn points
leading to a destination. Whereas a track might sample the GPS location
of actual movements at some interval, a route represents directions
such as you might get from a routing application.
- Waypoints—a point of interest or named feature on a map.
"""
@type t :: %__MODULE__{
tracks: list(GpxEx.Track.t()),
waypoints: list(GpxEx.Waypoint.t()),
routes: list(GpxEx.Route.t())
}

defstruct tracks: [], waypoints: [], routes: []
end
64 changes: 57 additions & 7 deletions lib/gpx_ex/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ defmodule GpxEx.Parser do
|> get_track_elements()
|> Enum.map(&build_track/1)

{:ok, %GpxEx.Gpx{tracks: tracks}}
standalone_waypoints =
gpx_document
|> get_top_level_waypoint_elements()
|> Enum.map(&build_waypoint/1)

routes =
gpx_document
|> get_route_elements()
|> Enum.map(&build_route/1)

{:ok, %GpxEx.Gpx{tracks: tracks, waypoints: standalone_waypoints, routes: routes}}
end

defp build_track(track_xml_element) do
Expand All @@ -16,7 +26,7 @@ defmodule GpxEx.Parser do
|> get_segment_elements()
|> Enum.map(&build_segment/1)

track_name = get_track_name(track_xml_element)
track_name = optional_string(track_xml_element, "name")

%GpxEx.Track{segments: segments, name: track_name}
end
Expand All @@ -39,17 +49,57 @@ defmodule GpxEx.Parser do
}
end

defp build_route(route_element) do
%GpxEx.Route{
name: optional_string(route_element, "name"),
points:
route_element
|> get_route_point_elements()
|> Enum.map(&build_waypoint/1)
}
end

defp build_waypoint(waypoint_element) do
%GpxEx.Waypoint{
lat: get_lat(waypoint_element),
lon: get_lon(waypoint_element),
ele: get_ele(waypoint_element),
name: optional_string(waypoint_element, "name"),
symbol: optional_string(waypoint_element, "sym"),
description: optional_string(waypoint_element, "desc"),
url: optional_string(waypoint_element, "url")
}
end

defp get_track_elements(xml), do: xpath(xml, ~x"//trk"l)
defp get_segment_elements(xml), do: xpath(xml, ~x"./trkseg"l)
defp get_point_elements(xml), do: xpath(xml, ~x"./trkpt"l)

defp get_track_name(xml), do: xpath(xml, ~x"./name/text()"s) |> optinal_string
defp get_top_level_waypoint_elements(xml), do: xpath(xml, ~x"//wpt"l)

defp get_route_elements(xml), do: xpath(xml, ~x"//rte"l)
defp get_route_point_elements(xml), do: xpath(xml, ~x"./rtept"l)

defp get_lat(xml), do: xpath(xml, ~x"./@lat"f)
defp get_lon(xml), do: xpath(xml, ~x"./@lon"f)
defp get_ele(xml), do: xpath(xml, ~x"./ele/text()"Fo)
defp get_time(xml), do: xpath(xml, ~x"./time/text()"s) |> optinal_string
defp get_ele(xml), do: optional_xpath(xml, ~x"./ele/text()"f)
defp get_time(xml), do: optional_string(xml, "time")

defp optional_string(xml, element_type) do
el = xpath(xml, ~x"./#{element_type}")

defp optinal_string(maybe_string) when maybe_string == "", do: nil
defp optinal_string(maybe_string), do: maybe_string
if is_nil(el) do
nil
else
xpath(el, ~x"./text()"s)
end
end
Comment on lines +88 to +96
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting an exception thrown when trying to query ./whatever/text() when the element in question didn't exist, so I moved to checking for the element, then asking for its text.


defp optional_xpath(xml, path) do
try do
xpath(xml, path)
catch
_, _ -> nil
end
end
end
9 changes: 9 additions & 0 deletions lib/gpx_ex/route.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule GpxEx.Route do
@typedoc """
A struct representing a route, an idealized list of turn points that describe
the directions one should follow to go from one place to another.
"""
@type t :: %__MODULE__{points: list(GpxEx.Waypoint.t()), name: String.t() | nil}

defstruct points: [], name: nil
end
28 changes: 28 additions & 0 deletions lib/gpx_ex/waypoint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule GpxEx.Waypoint do
@enforce_keys [:lat, :lon]
defstruct @enforce_keys ++
[
ele: nil,
name: nil,
symbol: nil,
description: nil,
url: nil
]

@typedoc """
A single waypoint, which may be part of a route or
a standalone point-of-interest or named feature.

- Lon and lat are in decimal degrees
- Elevation is in meters (presumably above mean sea level)
"""
@type t :: %__MODULE__{
lat: float(),
lon: float(),
ele: float() | nil,
name: String.t() | nil,
symbol: String.t() | nil,
description: String.t() | nil,
url: String.t() | nil
}
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule GpxEx.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:sweet_xml, "~> 0.6.0"}
{:sweet_xml, "~> 0.7.0"}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this library's purposes, I think 0.6.6 that we were using previously is actually compatible with 0.7.3, so we could potentially specify a broader supported range here, albeit at the cost of additional testing. 0.6.x is incompatible with a lot of libraries in the wild, including ExAws.

# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
%{
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"},
}
60 changes: 59 additions & 1 deletion test/gpx_ex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule GpxExTest do
use ExUnit.Case
doctest GpxEx

test "parser" do
test "parses a track" do
{:ok, gpx_doc} = File.read("./test/gpx_files/gdynia.gpx")

expected = %GpxEx.Gpx{
Expand Down Expand Up @@ -45,4 +45,62 @@ defmodule GpxExTest do

assert {:ok, expected} == GpxEx.parse(gpx_doc)
end

test "parses standalone waypoints" do
{:ok, gpx_doc} = File.read("./test/gpx_files/standalone_wpts.gpx")

expected = %GpxEx.Gpx{
waypoints: [
%GpxEx.Waypoint{
lat: 39.2,
lon: -94.5,
name: "Kansas City, MO",
symbol: "City",
description: "Midwestern city",
url: "https://www.visitkc.com",
ele: 308.15
},
%GpxEx.Waypoint{
lat: 32.7,
lon: -117.2,
name: "San Diego, CA",
symbol: "City",
description: "Port city",
url: "https://www.sandiego.gov/",
ele: nil
},
%GpxEx.Waypoint{
lat: 36.4,
lon: -94.2,
name: "Bentonville, AR",
symbol: "Town",
description: nil,
url: "http://bentonvillear.com/",
ele: 395
}
]
}

assert {:ok, expected} == GpxEx.parse(gpx_doc)
end

test "parses routes" do
{:ok, gpx_doc} = File.read("./test/gpx_files/route.gpx")

expected = %GpxEx.Gpx{
routes: [
%GpxEx.Route{
name: "Sample route",
points: [
%GpxEx.Waypoint{name: "Point 1", lon: -94.5, lat: 39.2, ele: 388.1},
%GpxEx.Waypoint{name: "Point 2", lon: 120.4, lat: -19.9},
%GpxEx.Waypoint{name: "Point 3", lon: -0.1, lat: -0.2},
%GpxEx.Waypoint{name: "Point 4", lon: 0.2, lat: 0.4, ele: -0.1}
]
}
]
}

assert {:ok, expected} == GpxEx.parse(gpx_doc)
end
end
21 changes: 21 additions & 0 deletions test/gpx_files/route.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0"?>
<gpx xmlns="http://www.topografix.com/GPX/1/0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:gpx="http://www.topografix.com/GPX/1/0" version="1.0" xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">
<rte>
<name><![CDATA[Sample route]]></name>
<desc>Fake route</desc>
<rtept lat="39.2" lon="-94.5">
<ele>388.1</ele>
<name>Point 1</name>
</rtept>
<rtept lat="-19.9" lon="120.4">
<name>Point 2</name>
</rtept>
<rtept lat="-0.2" lon="-0.1">
<name>Point 3</name>
</rtept>
<rtept lat="0.4" lon="0.2">
<name>Point 4</name>
<ele>-0.1</ele>
</rtept>
</rte>
</gpx>
22 changes: 22 additions & 0 deletions test/gpx_files/standalone_wpts.gpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<gpx xmlns="http://www.topografix.com/GPX/1/0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:gpx="http://www.topografix.com/GPX/1/0" version="1.0" xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">
<wpt lat="39.2" lon="-94.5">
<name>Kansas City, MO</name>
<sym>City</sym>
<desc>Midwestern city</desc>
<url>https://www.visitkc.com</url>
<ele>308.15</ele>
</wpt>
<wpt lat="32.7" lon="-117.2">
<sym>City</sym>
<name>San Diego, CA</name>
<url>https://www.sandiego.gov/</url>
<desc>Port city</desc>
</wpt>
<wpt lat="36.4" lon="-94.2">
<ele>395</ele>
<url>http://bentonvillear.com/</url>
<name>Bentonville, AR</name>
<sym>Town</sym>
</wpt>
</gpx>