diff --git a/apps/transport/lib/netex/geojson_converter.ex b/apps/transport/lib/netex/geojson_converter.ex new file mode 100644 index 0000000000..2937e626cc --- /dev/null +++ b/apps/transport/lib/netex/geojson_converter.ex @@ -0,0 +1,58 @@ +defmodule Transport.NeTEx.GeoJSONConverter do + @moduledoc """ + A first converter to extract stop places and display it as a GeoJSON. + """ + + @doc """ + Reads a NeTEx zip archive stored on disk, and returns a GeoJSON. + """ + @spec convert(binary()) :: binary() + def convert(zip_file_name) do + {:ok, result} = + zip_file_name + |> collect_stop_places() + |> to_geojson_feature_collection() + |> Jason.encode() + + result + end + + def collect_stop_places(zip_file_name) do + zip_file_name + |> Transport.NeTEx.read_all_stop_places() + |> Enum.flat_map(&keep_stop_places_with_location/1) + end + + @spec keep_stop_places_with_location({binary(), list()}) :: list() + defp keep_stop_places_with_location({_filename, stop_places}) do + Enum.filter(stop_places, &has_location?/1) + end + + @spec has_location?(map()) :: boolean() + defp has_location?(stop_place) do + Map.has_key?(stop_place, :latitude) && Map.has_key?(stop_place, :longitude) + end + + defp to_geojson_feature_collection(features) do + %{ + type: "FeatureCollection", + features: Enum.map(features, &to_geojson_feature/1) + } + end + + defp to_geojson_feature(%{latitude: latitude, longitude: longitude, name: name}) do + %{ + type: "Feature", + geometry: %{ + type: "Point", + coordinates: [longitude, latitude] + }, + properties: %{ + name: name + } + } + end + + defp to_geojson_feature(%{id: id, latitude: latitude, longitude: longitude}), + do: to_geojson_feature(%{latitude: latitude, longitude: longitude, name: id}) +end diff --git a/apps/transport/test/netex/geojson_converter_test.exs b/apps/transport/test/netex/geojson_converter_test.exs new file mode 100644 index 0000000000..e189cf6eb7 --- /dev/null +++ b/apps/transport/test/netex/geojson_converter_test.exs @@ -0,0 +1,73 @@ +defmodule Transport.NeTEx.GeoJSONConverterTest do + use ExUnit.Case, async: true + + alias Transport.ZipCreator + + # not fully correct XML, but close enough for what we want to test + def some_netex_content do + """ + + + + Poyartin + + + 43.6690 + -0.9190 + + + + + Dax + + + 43.7154 + -1.0530 + + + + + + """ + end + + test "valid JSON from valid NeTEx" do + tmp_file = System.tmp_dir!() |> Path.join("temp-netex-#{Ecto.UUID.generate()}.zip") + + netex_files = [ + {"arrets_1.xml", some_netex_content()}, + {"arrets_2.xml", some_netex_content()} + ] + + ZipCreator.create!(tmp_file, netex_files) + + result = Transport.NeTEx.GeoJSONConverter.convert(tmp_file) + + assert {:ok, + %{ + type: "FeatureCollection", + features: [ + %{ + type: "Feature", + geometry: %{type: "Point", coordinates: [-0.919, 43.669]}, + properties: %{name: "Poyartin"} + }, + %{ + type: "Feature", + geometry: %{type: "Point", coordinates: [-1.053, 43.7154]}, + properties: %{name: "Dax"} + }, + %{ + type: "Feature", + geometry: %{type: "Point", coordinates: [-0.919, 43.669]}, + properties: %{name: "Poyartin"} + }, + %{ + type: "Feature", + geometry: %{type: "Point", coordinates: [-1.053, 43.7154]}, + properties: %{name: "Dax"} + } + ] + }} = Jason.decode(result, keys: :atoms) + end +end diff --git a/apps/transport/test/netex/netex_archive_parser_test.exs b/apps/transport/test/netex/netex_archive_parser_test.exs index a619424ad8..f421cfa179 100644 --- a/apps/transport/test/netex/netex_archive_parser_test.exs +++ b/apps/transport/test/netex/netex_archive_parser_test.exs @@ -1,21 +1,7 @@ defmodule Transport.NeTEx.ArchiveParserTest do use ExUnit.Case, async: true - defmodule ZipCreator do - @moduledoc """ - A light wrapper around OTP `:zip` features. Does not support streaming here, - but massages the string <-> charlist differences. - """ - @spec create!(String.t(), [{String.t(), binary()}]) :: no_return() - def create!(zip_filename, file_data) do - {:ok, ^zip_filename} = - :zip.create( - zip_filename, - file_data - |> Enum.map(fn {name, content} -> {name |> to_charlist(), content} end) - ) - end - end + alias Transport.ZipCreator # not fully correct XML, but close enough for what we want to test def some_netex_content do diff --git a/apps/transport/test/support/zip_creator.ex b/apps/transport/test/support/zip_creator.ex new file mode 100644 index 0000000000..1c041c8e9e --- /dev/null +++ b/apps/transport/test/support/zip_creator.ex @@ -0,0 +1,16 @@ +defmodule Transport.ZipCreator do + @moduledoc """ + A light wrapper around OTP `:zip` features. Does not support streaming here, + but massages the string <-> charlist differences. + """ + def create!(zip_filename, file_data) do + {:ok, ^zip_filename} = + :zip.create( + zip_filename, + file_data + |> Enum.map(fn {name, content} -> {name |> to_charlist(), content} end) + ) + + :ok + end +end