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