diff --git a/lib/landmark/helpers.ex b/lib/landmark/helpers.ex index e9f66fe..e459cc2 100644 --- a/lib/landmark/helpers.ex +++ b/lib/landmark/helpers.ex @@ -61,4 +61,22 @@ defmodule Landmark.Helpers do |> length_to_radians(from) |> radians_to_length(to) end + + @doc """ + Get the coordinates from a GeoJSON object. + + ## Examples + iex> Landmark.Helpers.coords(%Geo.Point{coordinates: {1, 2}}) + [{1, 2}] + iex> Landmark.Helpers.coords(%Geo.MultiPoint{coordinates: [{1, 2}, {3, 4}]}) + [{1, 2}, {3, 4}] + iex> Landmark.Helpers.coords(%Geo.Polygon{coordinates: [[{1, 2}, {3, 4}, {5, 6}], [{7, 8}, {9, 10}]]}) + [{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}] + """ + def coords(geojson) + def coords(%Geo.Point{coordinates: coordinates}), do: [coordinates] + def coords(%Geo.MultiPoint{coordinates: coordinates}), do: coordinates + def coords(%Geo.LineString{coordinates: coordinates}), do: coordinates + def coords(%Geo.MultiLineString{coordinates: coordinates}), do: List.flatten(coordinates) + def coords(%Geo.Polygon{coordinates: coordinates}), do: List.flatten(coordinates) end diff --git a/lib/landmark/measurement.ex b/lib/landmark/measurement.ex index 26fa995..b4df801 100644 --- a/lib/landmark/measurement.ex +++ b/lib/landmark/measurement.ex @@ -49,9 +49,20 @@ defmodule Landmark.Measurement do @doc """ Computes the centroid as the mean of all vertices within the object. + + ## Examples + + iex> polygon = %Geo.Polygon{coordinates: [[{2, 2}, {2, 4}, {6, 4}, {6, 2}, {2, 2}]]} + ...> Landmark.Measurement.centroid(polygon) + %Geo.Point{coordinates: {4.0, 3.0}} + + iex> point = %Geo.Point{coordinates: {1, 1}} + ...> Landmark.Measurement.centroid(point) + %Geo.Point{coordinates: {1, 1}} """ @spec centroid(Geo.geometry() | Enumerable.t(Landmark.lng_lat())) :: Geo.Point.t() - def centroid(object) + def centroid(geometry) + def centroid(%Geo.Point{} = p), do: p def centroid(%Geo.Polygon{coordinates: coordinates}) do @@ -78,6 +89,60 @@ defmodule Landmark.Measurement do end) end + @doc """ + Computes an object's center of mass using "Centroid of Polygon" formula + + https://en.wikipedia.org/wiki/Centroid#Of_a_polygon + + ## Examples + + iex> polygon = %Geo.Polygon{coordinates: [[{2, 2}, {2, 4}, {6, 4}, {6, 2}, {2, 2}]]} + ...> Landmark.Measurement.center_of_mass(polygon) + %Geo.Point{coordinates: {4.0, 3.0}} + """ + def center_of_mass(geometry) + + def center_of_mass(geometry) do + coordinates = Helpers.coords(geometry) + + centre = centroid(coordinates) + + %Geo.Point{coordinates: {tx, ty}} = centre + + coordinates + |> Enum.map(fn {x, y} -> {x - tx, y - ty} end) + |> Enum.chunk_every(2, 1, :discard) + |> Enum.reduce( + %{s_area: 0, sx: 0, sy: 0}, + fn [{xi, yi}, {xj, yj}], %{s_area: s_area, sx: sx, sy: sy} -> + # a is the common factor to compute the signed area and the final coordinates + a = xi * yj - xj * yi + + %{ + s_area: s_area + a, + sx: sx + (xi + xj) * a, + sy: sy + (yi + yj) * a + } + end + ) + |> then(fn + %{s_area: a} when a == 0 -> + centre + + %{s_area: s_area, sx: sx, sy: sy} -> + area = s_area * 0.5 + + area_factor = 1 / (6 * area) + + %Geo.Point{ + coordinates: { + tx + area_factor * sx, + ty + area_factor * sy + } + } + end) + end + @doc """ Computes the bounding box for an object. diff --git a/lib/landmark/transformation.ex b/lib/landmark/transformation.ex index 9282600..d50ca18 100644 --- a/lib/landmark/transformation.ex +++ b/lib/landmark/transformation.ex @@ -3,21 +3,65 @@ defmodule Landmark.Transformation do @doc """ Moves a geometry a specified distance along a Rhumb Line in the direction of the provided bearing + + ## Examples + + iex> Landmark.Transformation.translate(%Geo.Point{coordinates: {-75, 39}}, 100, 90) + %Geo.Point{coordinates: {-73.84279077386554, 39.0}} + """ - def transform(geometry, distance, bearing, options \\ [unit: :kilometers]) + def translate(geometry, distance, bearing, options \\ [unit: :kilometers]) - def transform(%Geo.Point{} = point, distance, bearing, options) do + def translate(%Geo.Point{properties: properties} = point, distance, bearing, options) do Measurement.rhumb_destination(point, distance, bearing, options) + |> Map.put(:properties, properties) + end + + def translate( + %Geo.MultiPoint{coordinates: coordinates, properties: properties}, + distance, + bearing, + options + ) do + %Geo.MultiPoint{ + coordinates: translate_coordinates(coordinates, distance, bearing, options), + properties: properties + } end - def transform(%Geo.LineString{coordinates: coordinates}, distance, bearing, options) do + def translate( + %Geo.LineString{coordinates: coordinates, properties: properties}, + distance, + bearing, + options + ) do %Geo.LineString{ + coordinates: translate_coordinates(coordinates, distance, bearing, options), + properties: properties + } + end + + def translate( + %Geo.Polygon{coordinates: coordinates, properties: properties}, + distance, + bearing, + options + ) do + %Geo.Polygon{ coordinates: - Stream.map( - coordinates, - &Measurement.rhumb_destination(%Geo.Point{coordinates: &1}, distance, bearing, options) - ) - |> Enum.map(&Map.get(&1, :coordinates)) + coordinates + |> Enum.map(fn subcoords -> + translate_coordinates(subcoords, distance, bearing, options) + end), + properties: properties } end + + defp translate_coordinates(coordinates, distance, bearing, options) do + coordinates + |> Stream.map( + &Measurement.rhumb_destination(%Geo.Point{coordinates: &1}, distance, bearing, options) + ) + |> Enum.map(&Map.get(&1, :coordinates)) + end end diff --git a/test/measurement_test.exs b/test/measurement_test.exs index 76425bb..1f2194a 100644 --- a/test/measurement_test.exs +++ b/test/measurement_test.exs @@ -85,6 +85,79 @@ defmodule MeasurementTest do end end + describe "center of mass" do + test "getting the center_of_mass of a point" do + point = %Geo.Point{ + coordinates: {2, 3} + } + + assert Measurement.center_of_mass(point) == %Geo.Point{coordinates: {2, 3}} + end + + test "getting the center of mass of a multipoint" do + multipoint = %Geo.MultiPoint{ + coordinates: [{1, 2}, {5, 6}] + } + + assert Measurement.center_of_mass(multipoint) == %Geo.Point{coordinates: {3, 4}} + end + + test "getting the center of mass of a multipoint with a single point" do + multipoint = %Geo.MultiPoint{ + coordinates: [{1, 2}] + } + + assert Measurement.center_of_mass(multipoint) == %Geo.Point{coordinates: {1, 2}} + end + + test "getting the center of mass of a polygon" do + polygon = %Geo.Polygon{ + coordinates: [ + [ + {4.8250579833984375, 45.79398056386735}, + {4.882392883300781, 45.79254427435898}, + {4.910373687744141, 45.76081677972451}, + {4.894924163818359, 45.7271539426975}, + {4.824199676513671, 45.71337148333104}, + {4.773387908935547, 45.74021417890731}, + {4.778022766113281, 45.778418789239055}, + {4.8250579833984375, 45.79398056386735} + ] + ] + } + + assert Measurement.center_of_mass(polygon) == %Geo.Point{ + coordinates: {4.840728965137111, 45.75581209996416} + } + end + + test "getting the center of mass of a polygon with a hole" do + polygon = %Geo.Polygon{ + coordinates: [ + [{2, 2}, {2, 4}, {6, 4}, {6, 2}, {2, 2}], + [{3, 3}, {3, 3.5}, {3.5, 4}, {4, 3}, {3, 3}] + ] + } + + assert Measurement.center_of_mass(polygon) == %Geo.Point{ + coordinates: {3.933050047214353, 3.018791312559018} + } + end + + test "getting the centroid of a lineString" do + linestring = %Geo.LineString{ + coordinates: [ + {4.86020565032959, 45.76884015325622}, + {4.85994815826416, 45.749558161214516} + ] + } + + assert Measurement.centroid(linestring) == %Geo.Point{ + coordinates: {4.860076904296875, 45.75919915723537} + } + end + end + describe "bbox" do test "getting the bbox for a point" do point = %Geo.Point{ diff --git a/test/transformation_test.exs b/test/transformation_test.exs index 40a3e26..ac3efbb 100644 --- a/test/transformation_test.exs +++ b/test/transformation_test.exs @@ -3,20 +3,66 @@ defmodule TransformationTest do describe "translate" do test "translating a point" do - geo = %Geo.Point{coordinates: {-75, 39}} + geo = %Geo.Point{coordinates: {-75, 39}, properties: %{name: "foo"}} - translated = Landmark.Transformation.transform(geo, 100, 90) + translated = Landmark.Transformation.translate(geo, 100, 90) - assert translated == %Geo.Point{coordinates: {-73.84279077386554, 39.0}} + assert translated == %Geo.Point{ + coordinates: {-73.84279077386554, 39.0}, + properties: %{name: "foo"} + } + end + + test "translating a multipoint" do + geo = %Geo.MultiPoint{ + coordinates: [{-75, 39}, {-74, 36}, {-73, 38}], + properties: %{name: "foo"} + } + + translated = Landmark.Transformation.translate(geo, 100, 90) + + assert translated == %Geo.MultiPoint{ + coordinates: [ + {-73.84279077386554, 39.0}, + {-72.88837875730168, 36.0}, + {-71.85874593394195, 38.0} + ], + properties: %{name: "foo"} + } end test "translating a line string" do - geo = %Geo.LineString{coordinates: [{-75, 39}, {-74, 36}]} + geo = %Geo.LineString{coordinates: [{-75, 39}, {-74, 36}], properties: %{name: "foo"}} - translated = Landmark.Transformation.transform(geo, 100, 90) + translated = Landmark.Transformation.translate(geo, 100, 90) assert translated == %Geo.LineString{ - coordinates: [{-73.84279077386554, 39.0}, {-72.88837875730168, 36.0}] + coordinates: [ + {-73.84279077386554, 39.0}, + {-72.88837875730168, 36.0} + ], + properties: %{name: "foo"} + } + end + + test "translating a polygon" do + geo = %Geo.Polygon{ + coordinates: [[{-75, 39}, {-74, 36}, {-73, 38}, {-75, 39}]], + properties: %{name: "foo"} + } + + translated = Landmark.Transformation.translate(geo, 100, 90) + + assert translated == %Geo.Polygon{ + coordinates: [ + [ + {-73.84279077386554, 39.0}, + {-72.88837875730168, 36.0}, + {-71.85874593394195, 38.0}, + {-73.84279077386554, 39.0} + ] + ], + properties: %{name: "foo"} } end end