Packages
A configurable spherical geodesic grid data model designed for simulations GEOF Planet offers an ideal data model for simulations of 2½D spherical phenomena like oceanography and climate. It is ported from the JavaScript project g-e-o-f/peels.
Current section
Files
Jump to
Current section
Files
lib/geometry/geometry.ex
defmodule GEOF.Planet.Geometry do
@moduledoc """
Functions for computing a Planet's geometry.
"""
import :math
###
#
# ATTRIBUTES
#
###
@doc "The apparent accuracy of Erlang's trigonometry."
# It appears Elixir is able to compute these values
# much more precisely than JS (whose delta is 1.0e-10)
@tolerance 1.111e-15
def tolerance, do: @tolerance
###
#
# TYPES
#
###
@typedoc """
`position` encodes coordinates on the Sphere in the Geographic Coordinate System as
tuples of the format `{:pos, latitude, longitude}`, i.e. `{:pos, φ, λ}`, where
-π/2 ≤ φ ≤ π/2 and 0 ≤ λ ≤ 2π.
"""
@type position :: {:pos, float, float}
###
#
# FUNCTIONS
#
###
##
# Basic spherical geometry
##
@doc "Returns the arclength between two points on the sphere."
@spec distance(position, position) :: float
def distance(position_1, position_2)
def distance({:pos, f1_lat, f1_lon}, {:pos, f2_lat, f2_lon}) do
2 *
asin(
sqrt(
pow(sin((f1_lat - f2_lat) / 2), 2) +
cos(f1_lat) * cos(f2_lat) * pow(sin((f1_lon - f2_lon) / 2), 2)
)
)
end
@doc """
Returns the heading and distance from the first position to the second position.
Used only for testing, so documentation is sparse.
"""
@spec course(position, position) :: {:course, float, float}
def course(position_1, position_2)
def course({:pos, f1_lat, f1_lon}, {:pos, f2_lat, f2_lon}) do
d = distance({:pos, f1_lat, f1_lon}, {:pos, f2_lat, f2_lon})
a_relative = acos((sin(f2_lat) - sin(f1_lat) * cos(d)) / (sin(d) * cos(f1_lat)))
a = if sin(f2_lon - f1_lon) < 0, do: a_relative, else: 2 * pi() - a_relative
{:course, a, d}
end
@doc """
Calls `fun` `divisions`-1 times, once for each
point spaced evenly between two points on the sphere, and reduces the
`init_acc` into a final value.
"""
@spec interpolate(
any,
GEOF.Sphere.divisions(),
position,
position,
(any, integer, position -> any)
) :: any
def interpolate(acc, divisions, position_1, position_2, fun)
when is_integer(divisions) and divisions > 1 do
Enum.reduce(1..(divisions - 1), acc, fn i, acc ->
interpolate_step(acc, divisions, position_1, position_2, fun, i)
end)
end
def interpolate(acc, _, _, _, _) do
acc
end
defp interpolate_step(acc, d, position_1, position_2, fun, i) do
{:pos, f1_lat, f1_lon} = position_1
{:pos, f2_lat, f2_lon} = position_2
f = i / d
d = distance(position_1, position_2)
a = sin((1 - f) * d) / sin(d)
b = sin(f * d) / sin(d)
x = a * cos(f1_lat) * cos(f1_lon) + b * cos(f2_lat) * cos(f2_lon)
z = a * cos(f1_lat) * sin(f1_lon) + b * cos(f2_lat) * sin(f2_lon)
y = a * sin(f1_lat) + b * sin(f2_lat)
lat = atan2(y, sqrt(pow(x, 2) + pow(z, 2)))
lon = atan2(z, x)
fun.(acc, i, {:pos, lat, lon})
end
@doc """
Computes the centroid of a polygon on the surface of the sphere defined
by a list of `position`s.
"""
@spec centroid(nonempty_list(position)) :: position | {:error, String.t()}
def centroid(positions) do
n = length(positions)
x = Enum.reduce(positions, 0, fn {:pos, lat, lon}, sum -> sum + cos(lat) * cos(lon) / n end)
z = Enum.reduce(positions, 0, fn {:pos, lat, lon}, sum -> sum + cos(lat) * sin(lon) / n end)
y = Enum.reduce(positions, 0, fn {:pos, lat, _}, sum -> sum + sin(lat) / n end)
r = sqrt(x * x + z * z + y * y)
if abs(r) <= @tolerance do
{:error, "Can't compute centroid from these points."}
else
{:pos, asin(y / r), atan2(z, x)}
end
end
end