From 249e6f16058c440a29837ba17e0970ccf23e934d Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Sun, 10 Apr 2022 20:57:02 -0500 Subject: [PATCH 1/5] Added support for parsing standalone waypoints --- lib/gpx_ex/gpx.ex | 7 +++-- lib/gpx_ex/parser.ex | 46 +++++++++++++++++++++++++----- lib/gpx_ex/waypoint.ex | 26 +++++++++++++++++ test/gpx_ex_test.exs | 40 +++++++++++++++++++++++++- test/gpx_files/standalone_wpts.gpx | 22 ++++++++++++++ 5 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 lib/gpx_ex/waypoint.ex create mode 100644 test/gpx_files/standalone_wpts.gpx diff --git a/lib/gpx_ex/gpx.ex b/lib/gpx_ex/gpx.ex index 9d83d4a..a785645 100644 --- a/lib/gpx_ex/gpx.ex +++ b/lib/gpx_ex/gpx.ex @@ -1,5 +1,8 @@ defmodule GpxEx.Gpx do - @type t :: %__MODULE__{tracks: list(GpxEx.Track.t)} + @type t :: %__MODULE__{ + tracks: list(GpxEx.Track.t()), + waypoints: list(GpxEx.Waypoint.t()) + } - defstruct tracks: nil + defstruct tracks: [], waypoints: [] end diff --git a/lib/gpx_ex/parser.ex b/lib/gpx_ex/parser.ex index 926471b..92c663a 100644 --- a/lib/gpx_ex/parser.ex +++ b/lib/gpx_ex/parser.ex @@ -7,7 +7,12 @@ 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) + + {:ok, %GpxEx.Gpx{tracks: tracks, waypoints: standalone_waypoints}} end defp build_track(track_xml_element) do @@ -16,7 +21,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 +44,44 @@ defmodule GpxEx.Parser do } end + defp build_waypoint(waypoint_element) do + %GpxEx.Waypoint{ + lat: get_lat(waypoint_element), + lon: get_lon(waypoint_element), + ele_m: 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_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 optinal_string(maybe_string) when maybe_string == "", do: nil - defp optinal_string(maybe_string), do: maybe_string + defp optional_string(xml, element_type) do + el = xpath(xml, ~x"./#{element_type}") + + 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/waypoint.ex b/lib/gpx_ex/waypoint.ex new file mode 100644 index 0000000..57b554f --- /dev/null +++ b/lib/gpx_ex/waypoint.ex @@ -0,0 +1,26 @@ +defmodule GpxEx.Waypoint do + @moduledoc """ + A single waypoint, which may be part of a route or + a standalone point-of-interest or named feature. + """ + + @enforce_keys [:lat, :lon] + defstruct @enforce_keys ++ + [ + ele_m: nil, + name: nil, + symbol: nil, + description: nil, + url: nil + ] + + @type t :: %__MODULE__{ + lat: float(), + lon: float(), + ele_m: float() | nil, + name: String.t() | nil, + symbol: String.t() | nil, + description: String.t() | nil, + url: String.t() | nil + } +end diff --git a/test/gpx_ex_test.exs b/test/gpx_ex_test.exs index 4685880..933e209 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,42 @@ 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_m: 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_m: nil + }, + %GpxEx.Waypoint{ + lat: 36.4, + lon: -94.2, + name: "Bentonville, AR", + symbol: "Town", + description: nil, + url: "http://bentonvillear.com/", + ele_m: 395 + } + ] + } + + assert {:ok, expected} == GpxEx.parse(gpx_doc) + end end 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 From fb5a14d7eb7b0cec4b7e2f8d060cf290c25ece5f Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Mon, 11 Apr 2022 09:28:08 -0500 Subject: [PATCH 2/5] Add support for routes --- lib/gpx_ex/gpx.ex | 5 +++-- lib/gpx_ex/parser.ex | 20 +++++++++++++++++++- lib/gpx_ex/route.ex | 5 +++++ test/gpx_ex_test.exs | 20 ++++++++++++++++++++ test/gpx_files/route.gpx | 21 +++++++++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 lib/gpx_ex/route.ex create mode 100644 test/gpx_files/route.gpx diff --git a/lib/gpx_ex/gpx.ex b/lib/gpx_ex/gpx.ex index a785645..79080aa 100644 --- a/lib/gpx_ex/gpx.ex +++ b/lib/gpx_ex/gpx.ex @@ -1,8 +1,9 @@ defmodule GpxEx.Gpx do @type t :: %__MODULE__{ tracks: list(GpxEx.Track.t()), - waypoints: list(GpxEx.Waypoint.t()) + waypoints: list(GpxEx.Waypoint.t()), + routes: list(GpxEx.Route.t()) } - defstruct tracks: [], waypoints: [] + defstruct tracks: [], waypoints: [], routes: [] end diff --git a/lib/gpx_ex/parser.ex b/lib/gpx_ex/parser.ex index 92c663a..067d3f0 100644 --- a/lib/gpx_ex/parser.ex +++ b/lib/gpx_ex/parser.ex @@ -12,7 +12,12 @@ defmodule GpxEx.Parser do |> get_top_level_waypoint_elements() |> Enum.map(&build_waypoint/1) - {:ok, %GpxEx.Gpx{tracks: tracks, waypoints: standalone_waypoints}} + 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 @@ -44,6 +49,16 @@ defmodule GpxEx.Parser do } end + defp build_route(route_element) do + %GpxEx.Route{ + name: get_optional_text(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), @@ -62,6 +77,9 @@ defmodule GpxEx.Parser do 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: optional_xpath(xml, ~x"./ele/text()"f) diff --git a/lib/gpx_ex/route.ex b/lib/gpx_ex/route.ex new file mode 100644 index 0000000..9820fe2 --- /dev/null +++ b/lib/gpx_ex/route.ex @@ -0,0 +1,5 @@ +defmodule GpxEx.Route do + @type t :: %__MODULE__{points: list(GpxEx.Waypoint.t()), name: String.t() | nil} + + defstruct points: [], name: nil +end diff --git a/test/gpx_ex_test.exs b/test/gpx_ex_test.exs index 933e209..aa27c85 100644 --- a/test/gpx_ex_test.exs +++ b/test/gpx_ex_test.exs @@ -83,4 +83,24 @@ defmodule GpxExTest do 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_m: 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_m: -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 From 2b82a449069f59e58e2fc0841b757f03da8ae35b Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Mon, 11 Apr 2022 09:37:39 -0500 Subject: [PATCH 3/5] Take the latest SweetXml for use with modern projects --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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"}, } From c13b0869884640065eeaad02ea0bf185828e5fad Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Tue, 12 Apr 2022 06:20:53 -0500 Subject: [PATCH 4/5] Fix post-rebase --- lib/gpx_ex/parser.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gpx_ex/parser.ex b/lib/gpx_ex/parser.ex index 067d3f0..5d857be 100644 --- a/lib/gpx_ex/parser.ex +++ b/lib/gpx_ex/parser.ex @@ -51,7 +51,7 @@ defmodule GpxEx.Parser do defp build_route(route_element) do %GpxEx.Route{ - name: get_optional_text(route_element, "name"), + name: optional_string(route_element, "name"), points: route_element |> get_route_point_elements() From b579703eec02236b64d9135ecc8d9e9083b1dbdd Mon Sep 17 00:00:00 2001 From: Tyler Young Date: Tue, 12 Apr 2022 09:00:57 -0500 Subject: [PATCH 5/5] Docs and style matching prior to a PR --- lib/gpx_ex/gpx.ex | 12 ++++++++++++ lib/gpx_ex/parser.ex | 2 +- lib/gpx_ex/route.ex | 4 ++++ lib/gpx_ex/waypoint.ex | 16 +++++++++------- test/gpx_ex_test.exs | 10 +++++----- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/gpx_ex/gpx.ex b/lib/gpx_ex/gpx.ex index 79080aa..bf10a99 100644 --- a/lib/gpx_ex/gpx.ex +++ b/lib/gpx_ex/gpx.ex @@ -1,4 +1,16 @@ defmodule GpxEx.Gpx do + @typedoc """ + A struct representing a fully parsed GPX file. + + 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()), diff --git a/lib/gpx_ex/parser.ex b/lib/gpx_ex/parser.ex index 5d857be..dca529b 100644 --- a/lib/gpx_ex/parser.ex +++ b/lib/gpx_ex/parser.ex @@ -63,7 +63,7 @@ defmodule GpxEx.Parser do %GpxEx.Waypoint{ lat: get_lat(waypoint_element), lon: get_lon(waypoint_element), - ele_m: get_ele(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"), diff --git a/lib/gpx_ex/route.ex b/lib/gpx_ex/route.ex index 9820fe2..78ed0a3 100644 --- a/lib/gpx_ex/route.ex +++ b/lib/gpx_ex/route.ex @@ -1,4 +1,8 @@ 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 diff --git a/lib/gpx_ex/waypoint.ex b/lib/gpx_ex/waypoint.ex index 57b554f..0e98064 100644 --- a/lib/gpx_ex/waypoint.ex +++ b/lib/gpx_ex/waypoint.ex @@ -1,23 +1,25 @@ defmodule GpxEx.Waypoint do - @moduledoc """ - A single waypoint, which may be part of a route or - a standalone point-of-interest or named feature. - """ - @enforce_keys [:lat, :lon] defstruct @enforce_keys ++ [ - ele_m: nil, + 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_m: float() | nil, + ele: float() | nil, name: String.t() | nil, symbol: String.t() | nil, description: String.t() | nil, diff --git a/test/gpx_ex_test.exs b/test/gpx_ex_test.exs index aa27c85..baff21c 100644 --- a/test/gpx_ex_test.exs +++ b/test/gpx_ex_test.exs @@ -58,7 +58,7 @@ defmodule GpxExTest do symbol: "City", description: "Midwestern city", url: "https://www.visitkc.com", - ele_m: 308.15 + ele: 308.15 }, %GpxEx.Waypoint{ lat: 32.7, @@ -67,7 +67,7 @@ defmodule GpxExTest do symbol: "City", description: "Port city", url: "https://www.sandiego.gov/", - ele_m: nil + ele: nil }, %GpxEx.Waypoint{ lat: 36.4, @@ -76,7 +76,7 @@ defmodule GpxExTest do symbol: "Town", description: nil, url: "http://bentonvillear.com/", - ele_m: 395 + ele: 395 } ] } @@ -92,10 +92,10 @@ defmodule GpxExTest do %GpxEx.Route{ name: "Sample route", points: [ - %GpxEx.Waypoint{name: "Point 1", lon: -94.5, lat: 39.2, ele_m: 388.1}, + %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_m: -0.1} + %GpxEx.Waypoint{name: "Point 4", lon: 0.2, lat: 0.4, ele: -0.1} ] } ]