-module(geokit@mercator).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/geokit/mercator.gleam").
-export([zoom/1, x/1, y/1, from_quadkey/1, new/3, tile/3, to_quadkey/1, to_lat_lng/1, bounds/1, from_lat_lng/2]).
-export_type([tile/0, mercator_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Web Mercator (EPSG:3857) tile and quadkey conversion.\n"
"\n"
" Web Mercator is the projection used by every slippy map service:\n"
" Google Maps, OpenStreetMap, Bing Maps, Mapbox. The world at zoom\n"
" level `z` is divided into `2 ^ z` × `2 ^ z` square tiles indexed\n"
" by `(x, y)` with `(0, 0)` at the top-left (north-west) corner.\n"
"\n"
" Tiles outside the supported latitude range (`±85.05112878°`,\n"
" where the projection clips to keep the map square) are not\n"
" representable; [`from_lat_lng`](#from_lat_lng) clamps latitude\n"
" rather than reporting an error.\n"
"\n"
" Quadkeys are Bing Maps' single-string encoding of `(zoom, x, y)`.\n"
).
-opaque tile() :: {tile, integer(), integer(), integer()}.
-type mercator_error() :: {zoom_out_of_range, integer()} |
{tile_coord_out_of_range, integer(), integer(), integer()} |
{invalid_quadkey_char, binary(), integer()} |
empty_quadkey.
-file("src/geokit/mercator.gleam", 73).
?DOC(" Zoom level of `tile`.\n").
-spec zoom(tile()) -> integer().
zoom(Tile) ->
erlang:element(2, Tile).
-file("src/geokit/mercator.gleam", 78).
?DOC(" Tile `x` coordinate.\n").
-spec x(tile()) -> integer().
x(Tile) ->
erlang:element(3, Tile).
-file("src/geokit/mercator.gleam", 83).
?DOC(" Tile `y` coordinate.\n").
-spec y(tile()) -> integer().
y(Tile) ->
erlang:element(4, Tile).
-file("src/geokit/mercator.gleam", 187).
-spec from_quadkey_loop(list(binary()), integer(), integer(), integer()) -> {ok,
{integer(), integer(), integer()}} |
{error, mercator_error()}.
from_quadkey_loop(Chars, Position, X, Y) ->
case Chars of
[] ->
{ok, {X, Y, Position}};
[Head | Tail] ->
Digit_result = case Head of
<<"0"/utf8>> ->
{ok, {0, 0}};
<<"1"/utf8>> ->
{ok, {1, 0}};
<<"2"/utf8>> ->
{ok, {0, 1}};
<<"3"/utf8>> ->
{ok, {1, 1}};
_ ->
{error, {invalid_quadkey_char, Head, Position}}
end,
gleam@result:'try'(
Digit_result,
fun(_use0) ->
{Bit_x, Bit_y} = _use0,
from_quadkey_loop(
Tail,
Position + 1,
(X * 2) + Bit_x,
(Y * 2) + Bit_y
)
end
)
end.
-file("src/geokit/mercator.gleam", 171).
?DOC(
" Decode a quadkey to a [`Tile`](#Tile). The resulting `zoom` equals\n"
" the length of the quadkey, which must be in `[1, 30]` to mirror\n"
" the range accepted by [`new`](#new).\n"
).
-spec from_quadkey(binary()) -> {ok, tile()} | {error, mercator_error()}.
from_quadkey(Quadkey) ->
gleam@bool:guard(
Quadkey =:= <<""/utf8>>,
{error, empty_quadkey},
fun() ->
Length = string:length(Quadkey),
gleam@bool:guard(
Length > 30,
{error, {zoom_out_of_range, Length}},
fun() ->
gleam@result:'try'(
from_quadkey_loop(
gleam@string:to_graphemes(Quadkey),
0,
0,
0
),
fun(_use0) ->
{X_int, Y_int, Parsed_length} = _use0,
{ok, {tile, Parsed_length, X_int, Y_int}}
end
)
end
)
end
).
-file("src/geokit/mercator.gleam", 216).
-spec pow_2(integer()) -> integer().
pow_2(Exponent) ->
gleam@bool:guard(Exponent =< 0, 1, fun() -> 2 * pow_2(Exponent - 1) end).
-file("src/geokit/mercator.gleam", 52).
?DOC(
" Build a [`Tile`](#Tile). `zoom` must be in `[0, 30]`; `x` and `y`\n"
" must each be in `[0, 2^zoom)`.\n"
"\n"
" Matches the constructor naming convention used by every other\n"
" opaque type in geokit (e.g. [`latlng.new`](../latlng.html#new)).\n"
).
-spec new(integer(), integer(), integer()) -> {ok, tile()} |
{error, mercator_error()}.
new(Zoom, X, Y) ->
gleam@bool:guard(
(Zoom < 0) orelse (Zoom > 30),
{error, {zoom_out_of_range, Zoom}},
fun() ->
Max_coord = pow_2(Zoom),
gleam@bool:guard(
(((X < 0) orelse (Y < 0)) orelse (X >= Max_coord)) orelse (Y >= Max_coord),
{error, {tile_coord_out_of_range, Zoom, X, Y}},
fun() -> {ok, {tile, Zoom, X, Y}} end
)
end
).
-file("src/geokit/mercator.gleam", 68).
?DOC(
" Build a [`Tile`](#Tile). Alias for [`new`](#new) kept for backward\n"
" compatibility; new code should call [`new`](#new) instead.\n"
).
-spec tile(integer(), integer(), integer()) -> {ok, tile()} |
{error, mercator_error()}.
tile(Zoom, X, Y) ->
new(Zoom, X, Y).
-file("src/geokit/mercator.gleam", 153).
-spec to_quadkey_loop(tile(), integer(), binary()) -> binary().
to_quadkey_loop(Tile, Level, Acc) ->
gleam@bool:guard(
Level =< 0,
Acc,
fun() ->
Mask = pow_2(Level - 1),
Bit_x = (case Mask of
0 -> 0;
Gleam@denominator -> erlang:element(3, Tile) div Gleam@denominator
end) rem 2,
Bit_y = (case Mask of
0 -> 0;
Gleam@denominator@1 -> erlang:element(4, Tile) div Gleam@denominator@1
end) rem 2,
Digit = Bit_x + (2 * Bit_y),
Char = case Digit of
0 ->
<<"0"/utf8>>;
1 ->
<<"1"/utf8>>;
2 ->
<<"2"/utf8>>;
_ ->
<<"3"/utf8>>
end,
to_quadkey_loop(Tile, Level - 1, <<Acc/binary, Char/binary>>)
end
).
-file("src/geokit/mercator.gleam", 149).
?DOC(
" Encode `tile` as a Bing-style quadkey. The length of the result\n"
" equals `zoom(tile)`.\n"
).
-spec to_quadkey(tile()) -> binary().
to_quadkey(Tile) ->
to_quadkey_loop(Tile, erlang:element(2, Tile), <<""/utf8>>).
-file("src/geokit/mercator.gleam", 221).
-spec float_to_int(float()) -> integer().
float_to_int(Value) ->
erlang:trunc(Value).
-file("src/geokit/mercator.gleam", 225).
-spec int_to_float(integer()) -> float().
int_to_float(Value) ->
erlang:float(Value).
-file("src/geokit/mercator.gleam", 136).
-spec corner_lat_lng(integer(), integer(), integer()) -> geokit@latlng:lat_lng().
corner_lat_lng(Zoom, X, Y) ->
N = int_to_float(pow_2(Zoom)),
Lng = ((case N of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> int_to_float(X) / Gleam@denominator
end) * 360.0) - 180.0,
Lat_rad = gleam_community@maths:atan(
gleam_community@maths:sinh(
gleam_community@maths:pi() * (1.0 - (case N of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator@1 -> 2.0 * int_to_float(Y) / Gleam@denominator@1
end))
)
),
Lat = gleam_community@maths:radians_to_degrees(Lat_rad),
geokit@latlng:wrap(Lat, Lng).
-file("src/geokit/mercator.gleam", 122).
?DOC(
" Top-left (north-west) corner of `tile` as a\n"
" [`LatLng`](../latlng.html#LatLng).\n"
).
-spec to_lat_lng(tile()) -> geokit@latlng:lat_lng().
to_lat_lng(Tile) ->
corner_lat_lng(
erlang:element(2, Tile),
erlang:element(3, Tile),
erlang:element(4, Tile)
).
-file("src/geokit/mercator.gleam", 128).
?DOC(
" The south-west and north-east corners of `tile`, returned as\n"
" `#(sw, ne)`.\n"
).
-spec bounds(tile()) -> {geokit@latlng:lat_lng(), geokit@latlng:lat_lng()}.
bounds(Tile) ->
Nw = corner_lat_lng(
erlang:element(2, Tile),
erlang:element(3, Tile),
erlang:element(4, Tile)
),
Se = corner_lat_lng(
erlang:element(2, Tile),
erlang:element(3, Tile) + 1,
erlang:element(4, Tile) + 1
),
Sw = geokit@latlng:wrap(geokit@latlng:lat(Se), geokit@latlng:lng(Nw)),
Ne = geokit@latlng:wrap(geokit@latlng:lat(Nw), geokit@latlng:lng(Se)),
{Sw, Ne}.
-file("src/geokit/mercator.gleam", 229).
-spec clamp_int(integer(), integer(), integer()) -> integer().
clamp_int(Value, Min, Max) ->
gleam@bool:guard(
Value < Min,
Min,
fun() -> gleam@bool:guard(Value > Max, Max, fun() -> Value end) end
).
-file("src/geokit/mercator.gleam", 95).
?DOC(
" Convert a [`LatLng`](../latlng.html#LatLng) to the [`Tile`](#Tile)\n"
" that contains it at the given zoom level.\n"
"\n"
" Latitude is clamped to `±85.05112878°` (the Web Mercator pole\n"
" limit) before projection, so any input `LatLng` produces a valid\n"
" tile.\n"
).
-spec from_lat_lng(geokit@latlng:lat_lng(), integer()) -> {ok, tile()} |
{error, mercator_error()}.
from_lat_lng(Point, Zoom) ->
gleam@bool:guard(
(Zoom < 0) orelse (Zoom > 30),
{error, {zoom_out_of_range, Zoom}},
fun() ->
Pole_limit = 85.05112878,
Lat = gleam@float:clamp(
geokit@latlng:lat(Point),
+0.0 - Pole_limit,
Pole_limit
),
Lng = geokit@latlng:lng(Point),
N = int_to_float(pow_2(Zoom)),
Lat_rad = gleam_community@maths:degrees_to_radians(Lat),
X_float = ((Lng + 180.0) / 360.0) * N,
Y_float = ((1.0 - (case gleam_community@maths:pi() of
+0.0 -> +0.0;
-0.0 -> -0.0;
Gleam@denominator -> gleam_community@maths:asinh(
gleam_community@maths:tan(Lat_rad)
)
/ Gleam@denominator
end)) / 2.0) * N,
Max_coord = pow_2(Zoom),
X_int = clamp_int(float_to_int(X_float), 0, Max_coord - 1),
Y_int = clamp_int(float_to_int(Y_float), 0, Max_coord - 1),
{ok, {tile, Zoom, X_int, Y_int}}
end
).