Packages
phoenix_kit
2.60.2
2.60.2
2.60.1
2.60.0
2.59.0
2.58.0
2.57.1
2.57.0
2.56.1
2.56.0
2.55.1
2.55.0
2.54.2
2.54.1
2.54.0
2.53.0
2.52.2
2.52.1
2.52.0
2.51.0
2.50.0
2.49.1
2.49.0
2.48.0
2.47.0
2.46.0
2.45.0
2.44.0
2.43.1
2.43.0
2.42.1
2.42.0
2.41.6
2.41.4
2.41.3
2.41.2
2.41.1
2.41.0
2.40.1
2.40.0
2.39.0
2.38.1
2.38.0
2.37.5
2.37.4
2.37.3
2.37.2
2.37.1
2.37.0
2.36.1
2.36.0
2.35.0
2.34.0
2.33.0
2.32.1
2.32.0
2.31.1
2.31.0
2.30.0
2.29.1
2.29.0
2.28.2
2.28.1
2.28.0
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.22.24
2.22.23
2.22.22
2.22.21
2.22.20
2.22.19
2.22.18
2.22.17
2.22.16
2.22.15
2.22.14
2.22.13
2.22.12
2.22.11
2.22.10
2.22.9
2.22.8
2.22.7
2.22.6
2.22.5
2.22.4
2.22.3
2.22.2
2.22.1
2.22.0
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.0
2.19.0
2.18.1
2.18.0
2.17.0
2.16.0
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.19
2.13.18
2.13.17
2.13.16
2.13.15
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/storage/geo.ex
defmodule PhoenixKit.Modules.Storage.Geo do
@moduledoc """
Photos on a map: "what was taken in this area?" as one indexed lookup.
A photo's EXIF position is in `phoenix_kit_files.latitude` / `longitude`
(V215). The index is a GiST index on `point(longitude, latitude)`, which
Postgres has built in (no PostGIS), and a box on the map is
point(longitude, latitude) <@ box(point(west, south), point(east, north))
A **bounds** is `{south, west, north, east}` in degrees, the corners of the
visible map: latitude -90..90, longitude -180..180. A box that crosses the
antimeridian (the Pacific, Fiji, eastern Russia) has `west > east`, and is
searched as two.
Storage.list_files_in_scope(nil, bounds: {46.0, 14.0, 48.0, 16.0})
Geo.within(File, {46.0, 14.0, 48.0, 16.0})
Geo.parse_bounds("46,14,48,16")
A file with no position is never inside any box.
"""
import Ecto.Query, only: [dynamic: 2, where: 3]
@type bounds :: {number(), number(), number(), number()}
@doc """
Narrows a query over files to those inside `bounds`. `nil` leaves it as it is.
A bounds that is not four numbers raises, so a typo does not become "no filter".
"""
@spec within(Ecto.Queryable.t(), bounds() | nil) :: Ecto.Query.t()
def within(query, nil), do: query
def within(query, {south, west, north, east} = bounds)
when is_number(south) and is_number(west) and is_number(north) and is_number(east) do
condition =
case boxes(bounds) do
[box] -> box_condition(box)
[a, b] -> dynamic([f], ^box_condition(a) or ^box_condition(b))
end
where(query, [f], ^condition)
end
# The longitude ranges the box covers: one, or two when it crosses ±180 (after
# a longitude past the edge of the world is wrapped back), or the whole world
# when it spans it. The latitudes are clamped to the globe.
defp boxes({south, west, north, east}) do
south = clamp(south, -90, 90)
north = clamp(north, -90, 90)
if east - west >= 360 do
[{south, -180.0, north, 180.0}]
else
{west, east} = {wrap(west), wrap(east)}
if west <= east,
do: [{south, west, north, east}],
else: [{south, west, north, 180.0}, {south, -180.0, north, east}]
end
end
defp box_condition({south, west, north, east}) do
dynamic(
[f],
fragment(
"point(?, ?) <@ box(point(?, ?), point(?, ?))",
f.longitude,
f.latitude,
^(west * 1.0),
^(south * 1.0),
^(east * 1.0),
^(north * 1.0)
)
)
end
defp clamp(value, low, high), do: value |> max(low) |> min(high) |> Kernel.*(1.0)
# A longitude past ±180 (a map panned around the world) is the same place.
defp wrap(lon) when lon > 180 or lon < -180 do
remainder = :math.fmod(lon + 180.0, 360.0)
Float.round(if(remainder < 0, do: remainder + 360.0, else: remainder) - 180.0, 9)
end
defp wrap(lon), do: lon * 1.0
@doc """
A bounds from what a URL or a form carries: `"south,west,north,east"`, a list
of four, or a map with `"south"`, `"west"`, `"north"`, `"east"`. `nil` when it
is not four numbers, or the south is above the north.
"""
@spec parse_bounds(term()) :: bounds() | nil
def parse_bounds(value) when is_binary(value), do: value |> String.split(",") |> parse_bounds()
def parse_bounds(%{} = map) do
parse_bounds(Enum.map(~w(south west north east), &Map.get(map, &1)))
end
def parse_bounds([_, _, _, _] = parts) do
case Enum.map(parts, &to_number/1) do
[s, w, n, e]
when is_number(s) and is_number(w) and is_number(n) and is_number(e) and s <= n ->
{s, w, n, e}
_ ->
nil
end
end
def parse_bounds(_), do: nil
defp to_number(value) when is_number(value), do: value
defp to_number(value) when is_binary(value) do
case Float.parse(String.trim(value)) do
{number, ""} -> number
_ -> nil
end
rescue
# Float.parse/1 raises on a few hundred digits; a URL can carry them.
ArgumentError -> nil
end
defp to_number(_), do: nil
end