Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/landmark/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
67 changes: 66 additions & 1 deletion lib/landmark/measurement.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
60 changes: 52 additions & 8 deletions lib/landmark/transformation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
73 changes: 73 additions & 0 deletions test/measurement_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
58 changes: 52 additions & 6 deletions test/transformation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down