Current section
Files
Jump to
Current section
Files
lib/events/events.ex
defmodule Solar.Events do
@moduledoc """
The `Solar.Events` module provides the calculations for sunrise and sunset
times. This is likely to be refactored as other events are added.
"""
@doc """
The event function takes a minimum of two parameters, the event of interest
which can be either :rise or :set and the latitude and longitude. Additionally
a list of options can be provided as follows:
* `date:` allows a value of either `:today` or an Elixir date. The default
if this option is not provided is the current day.
* `zenith:` can be set to define the sunrise or sunset. See the `Zeniths`
module for a set of standard zeniths that are used. The default if a
zenith is not provided is `:official` most commonly used for sunrise and
sunset.
* `timezone:` can be provided and should be a standard timezone identifier
such as "America/Chicago". If the option is not provided, the timezone is
taken from the system and used.
## Examples
The following, with out any options and run on December 25:
iex> Solar.event (:rise, {39.1371, -88.65})
{:ok,~T[07:12:26]}
iex> Solar.event (:set, {39.1371, -88.65})
{:ok,~T[16:38:01]}
The coordinates are for Lake Sara, IL where sunrise on this day will be at 7:12:26AM and sunset will be at 4:38:01PM.
"""
@type latitude :: number
@type longitude :: number
@type message :: String.t
@spec event(:rise | :set, {latitude, longitude}) ::
{:ok, Time.t} |
{:error, message}
def event(type, location, opts \\ []) do
zenith = case opts[:zenith] do
nil -> Zeniths.official
_ -> opts[:zenith]
end
date = case opts[:date] do
:today -> Timex.to_date(Timex.local())
nil -> Timex.to_date(Timex.local())
_ -> opts[:date]
end
timezone = case opts[:timezone] do
:local -> Timex.local().timezone
nil -> Timex.local().time_zone
_ -> opts[:timezone]
end
{ latitude, longitude } = location
state = %{type: type, zenith: zenith, location: location,
latitude: latitude, longitude: longitude,
date: date, timezone: timezone}
with { :ok, state } <- verify_type_parameter(state),
{ :ok, state } <- get_base_longitude_hour(state),
{ :ok, state } <- get_longitude_hour(state),
{ :ok, state } <- get_mean_anomaly(state),
{ :ok, state } <- get_sun_true_longitude(state),
{ :ok, state } <- get_cos_sun_local_hour(state),
{ :ok, state } <- get_sun_local_hour(state),
{ :ok, state } <- get_right_ascension(state),
{ :ok, state } <- get_local_mean_time(state),
{ :ok, state } <- get_local_time(state) do
state[:local_time]
else
error -> error
end
end
defp verify_type_parameter state do
type = state[:type]
cond do
type != :rise && type != :set ->
{ :error, "Type parameter must be either :rise or :set"}
true -> { :ok, state }
end
end
# Computes the longitude time.
# Uses: location, date and type
# Sets: longitude_hour
defp get_longitude_hour state do
offset = case state[:type] do
:rise -> 6.0
_ -> 18.0
end
dividend = offset - state[:longitude] / 15.0
addend = dividend / 24.0
longitude_hour = Timex.day(state[:date]) + addend
{ :ok, Map.put(state, :longitude_hour, longitude_hour) }
end
# Computes the base longitude hour, lngHour in the algorithm. The longitude
# of the location of the solar event divided by 15 (deg/hour).
defp get_base_longitude_hour state do
base_longitude_hour = state[:longitude] / 15.0
{ :ok, Map.put(state, :base_longitude_hour, base_longitude_hour)}
end
# Computes the mean anomaly of the Sun, M in the algorithm.
defp get_mean_anomaly state do
mean_anomaly = state[:longitude_hour] * 0.9856 - 3.289
{ :ok, Map.put(state, :mean_anomaly, mean_anomaly) }
end
# Computes the true longitude of the sun, L in the algorithm, at the
# given location, adjusted to fit in the range [0-360].
defp get_sun_true_longitude state do
mean_anomaly = state[:mean_anomaly]
sin_mean_anomaly = :math.sin(deg_to_rad(mean_anomaly))
sin_double_mean_anomoly = :math.sin(deg_to_rad(mean_anomaly * 2.0))
first_part = mean_anomaly + (sin_mean_anomaly * 1.916)
second_part = sin_double_mean_anomoly * 0.020 + 282.634
true_longitude = first_part + second_part
sun_true_longitude = case true_longitude > 360.0 do
true -> true_longitude - 360.0
false -> true_longitude
end
{ :ok, Map.put(state, :sun_true_longitude, sun_true_longitude)}
end
defp get_cos_sun_local_hour state do
latitude = state[:latitude]
sin_sun_declination = :math.sin(deg_to_rad(state[:sun_true_longitude])) * 0.39782
cos_sun_declination = :math.cos(:math.asin(sin_sun_declination))
cos_zenith = :math.cos(deg_to_rad(state[:zenith]))
sin_latitude = :math.sin(deg_to_rad(latitude))
cos_latitude = :math.cos(deg_to_rad(latitude))
cos_sun_local_hour =
(cos_zenith - sin_sun_declination * sin_latitude) /
(cos_sun_declination * cos_latitude)
cond do
cos_sun_local_hour < -1.0 -> {:error, "cos_sun_local_hour < -1.0"}
cos_sun_local_hour > +1.0 -> {:error, "cos_sun_local_hour > +1.0"}
true -> { :ok, Map.put(state, :cos_sun_local_hour, cos_sun_local_hour)}
end
end
defp get_sun_local_hour state do
local_hour = rad_to_deg(:math.acos(state[:cos_sun_local_hour]))
local_hour = case state[:type] do
:rise -> (360.0 - local_hour) / 15
_ -> local_hour / 15
end
{:ok, Map.put(state, :sun_local_hour, local_hour)}
end
defp get_local_mean_time state do
local_mean_time = state[:sun_local_hour] + state[:right_ascension] -
(state[:longitude_hour] * 0.06571) - 6.622
val = cond do
local_mean_time < 0 -> local_mean_time + 24.0
local_mean_time > 24 -> local_mean_time - 24.0
true -> local_mean_time
end
{ :ok, Map.put(state, :local_mean_time, val) }
end
# Computes the suns right ascension, RA in the algorithm, adjusting for
# the quadrant of L and turning it into degree-hours. Will be in the
# range [0,360].
defp get_right_ascension state do
tanl = :math.tan(deg_to_rad(state[:sun_true_longitude]))
inner = rad_to_deg(tanl) * 0.91764
right_ascension = :math.atan(deg_to_rad(inner))
right_ascension = rad_to_deg(right_ascension)
right_ascension = cond do
right_ascension < 0.0 -> right_ascension + 360.0
right_ascension > 360.0 -> right_ascension - 360.0
true -> right_ascension
end
long_quad = Kernel.trunc(state[:sun_true_longitude] / 90.0) * 90.0
right_quad = Kernel.trunc(right_ascension / 90.0) * 90.0
val = (right_ascension + (long_quad - right_quad)) / 15.0
{ :ok, Map.put(state, :right_ascension, val)}
end
defp get_local_time state do
utc_time = state[:local_mean_time] - state[:base_longitude_hour]
tzi = Timex.timezone(state[:timezone], state[:date])
offset_minutes = Timex.Timezone.total_offset(tzi)
local_time = utc_time + offset_minutes/3600.0
time = local_time
hour = Kernel.trunc(time)
tmins = (time-hour) * 60
minute = Kernel.trunc(tmins)
tsecs = (tmins-minute) * 60
seconds = Kernel.trunc(tsecs)
time = Time.new(hour,minute,seconds)
{ :ok, Map.put(state, :local_time, time)}
end
# Converts degrees to radians.
defp deg_to_rad degrees do
degrees / 180.0 * :math.pi
end
defp rad_to_deg radians do
radians * 180.0 / :math.pi
end
@doc """
Calculates the hours of daylight returning as a time with hours, minutes and seconds.
"""
def daylight rise, set do
hours_to_time(time_to_hours(set) - time_to_hours(rise))
end
defp time_to_hours time do
time.hour + time.minute/60.0 + time.second / (60.0 * 60.0)
end
defp hours_to_time hours do
h = Kernel.trunc(hours)
value = (hours - h) * 60
m = Kernel.trunc(value)
value = (value - m) * 60
s = Kernel.trunc(value)
{ :ok, time} = Time.new(h,m,s)
time
end
end