diff --git a/lib/gpx_ex/gpx.ex b/lib/gpx_ex/gpx.ex
index 9d83d4a..bf10a99 100644
--- a/lib/gpx_ex/gpx.ex
+++ b/lib/gpx_ex/gpx.ex
@@ -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
diff --git a/lib/gpx_ex/parser.ex b/lib/gpx_ex/parser.ex
index 926471b..dca529b 100644
--- a/lib/gpx_ex/parser.ex
+++ b/lib/gpx_ex/parser.ex
@@ -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
@@ -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
@@ -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
+
+ defp optional_xpath(xml, path) do
+ try do
+ xpath(xml, path)
+ catch
+ _, _ -> nil
+ end
+ end
end
diff --git a/lib/gpx_ex/route.ex b/lib/gpx_ex/route.ex
new file mode 100644
index 0000000..78ed0a3
--- /dev/null
+++ b/lib/gpx_ex/route.ex
@@ -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
diff --git a/lib/gpx_ex/waypoint.ex b/lib/gpx_ex/waypoint.ex
new file mode 100644
index 0000000..0e98064
--- /dev/null
+++ b/lib/gpx_ex/waypoint.ex
@@ -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
diff --git a/mix.exs b/mix.exs
index a812e17..c675aad 100644
--- a/mix.exs
+++ b/mix.exs
@@ -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"}
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
diff --git a/mix.lock b/mix.lock
index 4f167bc..8d2b8e6 100644
--- a/mix.lock
+++ b/mix.lock
@@ -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"},
}
diff --git a/test/gpx_ex_test.exs b/test/gpx_ex_test.exs
index 4685880..baff21c 100644
--- a/test/gpx_ex_test.exs
+++ b/test/gpx_ex_test.exs
@@ -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{
@@ -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
diff --git a/test/gpx_files/route.gpx b/test/gpx_files/route.gpx
new file mode 100644
index 0000000..1d80593
--- /dev/null
+++ b/test/gpx_files/route.gpx
@@ -0,0 +1,21 @@
+
+
+
+
+ Fake route
+
+ 388.1
+ Point 1
+
+
+ Point 2
+
+
+ Point 3
+
+
+ Point 4
+ -0.1
+
+
+
\ No newline at end of file
diff --git a/test/gpx_files/standalone_wpts.gpx b/test/gpx_files/standalone_wpts.gpx
new file mode 100644
index 0000000..5ac5ebd
--- /dev/null
+++ b/test/gpx_files/standalone_wpts.gpx
@@ -0,0 +1,22 @@
+
+
+
+ Kansas City, MO
+ City
+ Midwestern city
+ https://www.visitkc.com
+ 308.15
+
+
+ City
+ San Diego, CA
+ https://www.sandiego.gov/
+ Port city
+
+
+ 395
+ http://bentonvillear.com/
+ Bentonville, AR
+ Town
+
+
\ No newline at end of file