Files
uarep-ctl/scripts/mercator.gd

665 lines
27 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Node
## Web mercator XYZ tile utilities
const R2D: = 180.0 / PI
const RE: = 6378137.0
const CE: = 2.0 * PI * RE
const EPSILON: = 1e-14
const LL_EPSILON: = 1e-11
const MAX_ZOOM: = 28
## An XYZ web mercator tile
## x, y, z : int
## x and y indexes of the tile and zoom level z.
class Tile:
var x: int
var y: int
var z: int
func as_array(): return [self.x, self.y, self.z]
func _to_string() -> String: return '[%d, %d, %d]' % [self.x, self.y, self.z]
func _init(x0: int = 0, y0: int = 0, z0: int = 0):
self.x = x0
self.y = y0
self.z = z0
func is_valid() -> bool:
if (self.z >= 0) and (self.z < MAX_ZOOM):
var mz: int = 2 ** self.z
return (self.x >= 0) and (self.x < mz) \
and (self.y >= 0) and (self.y < mz)
return false
## Пара долготы и широты.
class LngLat:
var lng: float = 0.0 ## Долгота в десятичных градусах восточной долготы.
var lat: float = 0.0 ## Широта в десятичных градусах северной широты.
func _init(lng0, lat0):
self.lng = lng0
self.lat = lat0
## Географическое ограничивающее поле.
## запад, юг, восток, север : float
## Значения границ в десятичных градусах.
class LngLatBbox:
var west: float
var south: float
var east: float
var north: float
func as_array(): return [self.west, self.south, self.east, self.north]
func _init(w: float = 0.0, s: float = 0.0, e: float = 0.0, n: float = 0.0):
self.west = w
self.south = s
self.east = e
self.north = n
## Граничная область веб-меркатора.
## Атрибуты: left, bottom, right, top : float.
## Значения границ в метрах.
class Bbox:
var left: float
var bottom: float
var right: float
var top: float
func as_array(): return [self.left, self.bottom, self.right, self.top]
func _init(l: float = 0.0, b: float = 0.0, r: float = 0.0, t: float = 0.0):
self.left = l
self.bottom = b
self.right = r
self.top = t
func degrees(v: float): return (v * 180.0) / PI
func radians(v: float): return (v * PI) / 180.0
func rshift(val: int, n: int): return (val % 0x100000000) >> n
## Возвращает плитки и занимаемый ими размер области в пикселях:
## ширина, высота, левая граница, верхняя граница, плитки
## [param west] [float] Западная граница области
## [param east] [float] Восточная граница области
## [param south] [float] Южная граница области
## [param north] [float] Северная граница области
## [param size] [float] Размер плитки в пикселях
## [param zoom] [float] Детализация
func get_tiles(west, south, east, north, zoom, size):
var tiles = [create_tiles(west, south, east, north, zoom)]
var mm = []
for t in tiles: mm.append(t.x)
var min_x = mm.min()
var max_x = mm.max()
mm.clear()
for t in tiles: mm.append(t.y)
var min_y = mm.min()
var max_y = mm.max()
var w = size * (max_x - min_x + 1)
var h = size * (max_y - min_y + 1)
return [w, h, min_x, min_y, tiles, max_x - min_x + 1]
## Возвращает размер области в метрах
## [param west] [float] - Западная граница области
## [param param] [float] - Восточная граница области
## [param south] [float] - Южная граница области
## [param north] [float] - Северная граница области
## [return] [Array]
func get_size_from_degrees(west, south, east, north) -> Array[float]:
var xy0: = xy(west, north, true)
var xy1: = xy(east, south, true)
return [xy0[0] - xy1[0], xy0[1] - xy1[1]]
## Возвращает границы области в градусах
## [param m] [float] - Размер карты в метрах.
## [param lng] [float] - Долгота центра области в градусах (-180...180).
## [param lat] [float] - Широта центра области в градусах (-90...90).
## [return] [float, float, float, float]
func get_degrees_bounds(m: float, lng: float, lat: float):
m /= 2.0
var x_y = xy(lng, lat, true)
var ws = lnglat(x_y[0] - m, x_y[1] - m)
var en = lnglat(x_y[0] + m, x_y[1] + m)
return [ws[0], ws[1], en[0], en[1]]
func get_tiles_bounds(tiles):
var mm = []
for t in tiles: mm.append(bounds(t).west)
var mmw = mm.min(); mm.clear()
for t in tiles: mm.append(bounds(t).south)
var mms = mm.min(); mm.clear()
for t in tiles: mm.append(bounds(t).east)
var mme = mm.max(); mm.clear()
for t in tiles: mm.append(bounds(t).north)
var mmn = mm.max(); mm.clear()
return [mmw, mms, mme, mmn]
## Возвращает левую верхнюю долготу и широту плитки.
## [param tile] Плитка или последовательность int. Может быть либо экземпляр
## Tile, либо 3 ints, X, Y, Z.
## [return] LngLat
## [codeblock]
## >>> ul(Tile(x=0, y=0, z=1))
## LngLat(lng=-180.0, lat=85.0511287798066)
## >>> mercantile.ul(1, 1, 1)
## LngLat(lng=0.0, lat=0.0)
## [/codeblock]
func ul(tile: Tile, zoom: int):
var Z2 = pow(2, zoom)
var lon_deg = tile.x / Z2 * 360.0 - 180.0
var lat_rad = atan(sinh(PI * (1 - 2 * tile.y / Z2)))
var lat_deg = degrees(lat_rad)
return LngLat.new(lon_deg, lat_deg)
## Возвращает левую верхнюю долготу и широту плитки.
## [param tile] [Плитка] Может быть либо экземпляром Tile, либо 3 интами, X, Y, Z.
## [return] [LngLat]
## [codeblock]
## >>> ul(Tile(x=0, y=0, z=1))
## LngLat(lng=-180.0, lat=85.0511287798066)
## >>> mercantile.ul(1, 1, 1)
## LngLat(lng=0.0, lat=0.0)
## [/codeblock]
func ul2(tile: Tile) -> Array[float]:
var z2 = pow(2, tile.z)
var lon_deg = tile.x / z2 * 360.0 - 180.0
var lat_rad = atan(sinh(PI * (1 - 2 * tile.y / z2)))
var lat_deg = degrees(lat_rad)
return [lat_deg, lon_deg]
## Возвращает граничную область плитки.
## [param tile] [Tile] или [Array]. Может быть либо экземпляром [Tile], либо 3
## целыми числами (X, Y, Z).
## [return] [LngLatBbox]
func bounds(tile: Tile) -> LngLatBbox:
var Z2: = pow(2, tile.z)
var ul_lon_deg = tile.x / Z2 * 360.0 - 180.0
var ul_lat_rad = atan(sinh(PI * (1 - 2 * tile.y / Z2)))
var ul_lat_deg = degrees(ul_lat_rad)
var lr_lon_deg = (tile.x + 1) / Z2 * 360.0 - 180.0
var lr_lat_rad = atan(sinh(PI * (1 - 2 * (tile.y + 1) / Z2)))
var lr_lat_deg = degrees(lr_lat_rad)
return LngLatBbox.new(ul_lon_deg, lr_lat_deg, lr_lon_deg, ul_lat_deg)
func truncate_lnglat(lng: float, lat: float) -> Array:
if lng > 180.0: lng = 180.0
elif lng < -180.0: lng = -180.0
if lat > 90.0: lat = 90.0
elif lat < -90.0: lat = -90.0
return [lng, lat]
## Преобразование долготы и широты в веб-меркатор x, y.
## [param lng] [param lat]: float
## Долгота и широта в десятичных градусах [param truncate] [bool] -Нужно ли
## усекать или обрезать входные данные до пределов веб-меркатора.
## [return] [x: float, y: float]. [param y] будет [b]inf[/b] на северном полюсе.
## ([param lat] >= 90) и [b]-inf[/b] на южном полюсе ([param lat] <= -90).
func xy(lng: float, lat: float, truncate: bool = false) -> Array[float]:
var ll = truncate_lnglat(lng, lat) if truncate else [0.0, 0.0]
var x: float = RE * radians(lng)
var y: float = 0.0
if ll[1] <= -90.0: y = float('-inf')
elif ll[1] >= 90.0: y = float('inf')
else: y = RE * log(tan((PI * 0.25) + (0.5 * radians(lat))))
return [x, y]
## Преобразование координат веб-меркатора x, y в долготу и широту.
## [param x] [float], [param y] [float] - Координаты веб-меркатора в метрах.
## [param truncate] [bool] - Нужно ли усекать или обрезать входные данные до границ веб-меркатора.
## [return] [LngLat]
func lnglat(x, y, truncate=false):
var lng = x * R2D / RE
var lat = ((PI * 0.5) - 2.0 * atan(exp(-y / RE))) * R2D
var ll = truncate_lnglat(lng, lat) if truncate else [0.0, 0.0]
return LngLat.new(ll[0], ll[1])
## Соседи плитки. [func neighbors] возвращает до восьми
## соседних плиток, при этом плитки будут опущены, если они не являются действительными, например.
## Tile(-1, -1, z). [param tile]: Плитка или последовательность int
## Может быть либо экземпляр Tile, либо 3 ints, X, Y, Z.
## [return] Массив
## [codeblock]
## >>> neighbors(Tile(486, 332, 10))
## [Tile(x=485, y=331, z=10), Tile(x=485, y=332, z=10), Tile(x=485, y=333, z=10), Tile(x=486, y=331, z=10), Tile(x=486, y=333, z=10), Tile(x=487, y=331, z=10), Tile(x=487, y=332, z=10), Tile(x=487, y=333, z=10)]
## [/codeblock]
func neighbors(tile: Tile) -> Array[Tile]:
var tiles: = Array()
var lo_hi = [0, 2 ** tile.z - 1]
for i in [-1, 0, 1]:
for j in [-1, 0, 1]:
if i == 0 and j == 0: continue
elif tile.x + i < 0 or tile.y + j < 0: continue
elif tile.x + i > lo_hi[1] or tile.y + j > lo_hi[i]: continue
var t = Tile.new(tile.x + i, tile.y + j, tile.z)
if t.is_valid():
tiles.append(t)
return tiles
## Получение меркаторской граничной рамки веб-плитки
## [param tile] - Плитка или последовательность int. Может быть либо экземпляр
## [Tile], либо 3 ints, X, Y, Z.
## [return] [Bbox]
## Примечания. Эпсилон вычитается из правой границы и прибавляется к нижней.
func xy_bounds(tile: Tile, zoom: int) -> Bbox:
var tile_size = CE / pow(2, zoom)
var left = tile.x * tile_size - CE / 2
var right = left + tile_size
var top = CE / 2 - tile.y * tile_size
var bottom = top - tile_size
return Bbox.new(left, bottom, right, top)
func _xy(lng, lat, truncate=false) -> Array:
var ll: = [lng, lat] if not truncate else truncate_lnglat(lng, lat)
var x = ll[0] / 360.0 + 0.5
var sinlat: = sin(radians(ll[1]))
var y: = 0.5 - 0.25 * log((1.0 + sinlat) / (1.0 - sinlat)) / PI
return [x, y]
## Получение плитки, содержащей долготу и широту
## [param lng], [param lat] [float] - Пара долготы и широты в десятичных градусах.
## [param zoom] [int] - Уровень масштабирования веб-меркатора.
## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора.
## [return] [Tile]
func create_tile(lng: float, lat: float, zoom: int, truncate: bool=false) -> Tile:
var xy0: = _xy(lng, lat, truncate)
var Z2 = pow(2, zoom)
# Чтобы устранить потерю точности при переходе от одной плитки к другой
# и lng/lat, точки в пределах EPSILON от правой стороны тайла
# считаются в следующей за ней плитке.
var tile: = Tile.new()
if xy0[0] <= 0: tile.x = 0
elif xy0[0] >= 1: tile.x = int(Z2 - 1)
else: tile.x = int(floor((xy0[0] + EPSILON) * Z2))
if xy0[1] <= 0: tile.y = 0
elif xy0[1] >= 1: tile.y = int(Z2 - 1)
else: tile.y = int(floor((xy0[1] + EPSILON) * Z2))
tile.z = zoom
return tile
## Получение четверного ключа плитки
## [param tile] [Tile] или последовательность [int]. Может быть либо экземпляр
## [Tile] или 3 целых, X, Y, Z.
## [return] [String]
func quadkey(tile: Tile, zoom) -> String:
var rs: = String()
for z in range(zoom, 0, -1):
var digit = 0
var mask = 1 << (z - 1)
if tile.x & mask:
digit += 1
if tile.y & mask:
digit += 2
rs += str(digit)
return rs
## Получение плитки, соответствующей четверному ключу.
## [param qk] [String] - Строка четверного ключа.
## [return] [Tile]
func quadkey_to_tile(qk: Array):
var tile: = Tile.new()
if len(qk) == 0: return tile
var i = 0
qk.reverse()
for digit in qk:
var mask = 1 << i
i += 1
if digit == '1':
tile.x |= mask
elif digit == '2':
tile.y |= mask
elif digit == '3':
tile.x |= mask
tile.y |= mask
elif digit == '0':
continue
else:
push_error('Не ожидаемый четырёхзначный знак: \"%s\"' % digit)
tile.z = i
return tile
## Получение плиток, перекрываемых географической рамкой
## [param west], [param south], [param east], [param north]: последовательность [float]
## - Значения границ в десятичных градусах.
## [param zooms] [int] или [Array[int]]. - Один или несколько уровней масштабирования.
## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора.
## [return] Tile
## Примечания. В параметрах south и east используется небольшой эпсилон, чтобы
## функция выдавала ровно одну плитку при задании границ этой же плитки.
func create_tiles(west: float, south: float, east: float, north: float, zooms, truncate: bool=false) -> Array[Tile]:
var west_south = [west, south] if truncate else truncate_lnglat(west, south)
var east_north = [east, north] if truncate else truncate_lnglat(east, north)
var bboxes = [[west, south, east, north]] if west <= east else [[-180.0, south, east, north], [west, south, 180.0, north]]
var tiles = []
if not is_instance_of(zooms, TYPE_ARRAY):
zooms = [zooms]
for box in bboxes:
var w = box[0]
var s = box[1]
var e = box[2]
var n = box[3]
# Зажим граничных значений.
w = max(-180.0, w)
s = max(-85.051129, s)
e = min(180.0, e)
n = min(85.051129, n)
for z in zooms:
var ul_tile = create_tile(w, n, z)
var lr_tile = create_tile(e - LL_EPSILON, s + LL_EPSILON, z)
for i in range(ul_tile.x, lr_tile.x + 1):
for j in range(ul_tile.y, lr_tile.y + 1):
tiles.append(Tile.new(i, j, z))
return tiles
## Получение родителя плитки. Родительская плитка - это плитка на один уровень масштабирования ниже, которая
## содержит данную "дочернюю" плитку.
## [param tile] [Tile] или последовательность int - Может быть либо экземпляром Tile.
## или 3 ints, X, Y, Z.
## [param zoom] [int] - Определяет уровень *масштабирования* возвращаемой родительской плитки.
## По умолчанию это значение на один ниже, чем у плитки (непосредственного родителя).
## [return] [Tile]
## [codeblock]
## >>> parent(Tile(0, 0, 2))
## Tile(x=0, y=0, z=1)
## >>> parent(Tile(0, 0, 2), zoom=0)
## Tile(x=0, y=0, z=0)
## [/codeblock]
func parent(tile: Tile, zoom=null):
if tile.z == 0: return null
assert((zoom != null) and (tile.z > zoom or zoom == int(zoom)), 'Масштаб (zoom) должен быть целым числом и меньше масштаба входной плитки (tile).')
assert(tile.x == int(tile.x) and tile.y == int(tile.y) and tile.z == int(tile.z), 'Родитель нецелочисленной плитки (tile) является неуточнённым')
var target_zoom = tile.z - 1 if zoom == null else zoom
# Алгоритм, в значительной степени вдохновленный https://github.com/mapbox/tilebelt
var return_tile = Tile.new(tile.x, tile.y, tile.z)
while return_tile.z > target_zoom:
tile.x = return_tile.x
tile.y = return_tile.y
tile.z = return_tile.z
if tile.x % 2 == 0 and tile.y % 2 == 0:
return_tile = Tile.new(tile.x / 2, tile.y / 2, tile.z - 1)
elif tile.x % 2 == 0:
return_tile = Tile.new(tile.x / 2, (tile.y - 1) / 2, tile.z - 1)
elif not tile.x % 2 == 0 and tile.y % 2 == 0:
return_tile = Tile.new((tile.x - 1) / 2, tile.y / 2, tile.z - 1)
else:
return_tile = Tile.new((tile.x - 1) / 2, (tile.y - 1) / 2, tile.z - 1)
return return_tile
## Получение дочерних элементов плитки. Дочерние элементы упорядочены:
## верхний-левый, верхний-правый, bottom-right, bottom-left.
## [tile] [Tile] или последовательность int. Может быть либо экземпляром Tile,
## либо 3 интами, X, Y, Z.
## [param zoom] [int] Возвращает все дочерние элементы с масштабом *zoom*, в порядке убывания глубины.
## в порядке намотки по часовой стрелке. Если не указано, возвращаются ближайшие (т.е. zoom + 1)
## дочерние элементы плитки.
## [return] [Array]
## [codeblock]
## >>> children(Tile(0, 0, 0))
## [Tile(x=0, y=0, z=1), Tile(x=0, y=1, z=1), Tile(x=1, y=0, z=1), Tile(x=1, y=1, z=1)]
## >>> children(Tile(0, 0, 0), zoom=2)
## [Tile(x=0, y=0, z=2), Tile(x=0, y=1, z=2), Tile(x=0, y=2, z=2), Tile(x=0, y=3, z=2), ...]
## [/codeblock]
func children(tile, zoom) -> Array:
assert((tile.z <= zoom) and zoom == int(zoom), 'Масштаб (zoom) должен быть целым числом и превышать масштаб входной плитки (tile)')
var target_zoom = zoom if zoom != null else tile.z + 1
var tiles: Array[Tile] = [tile]
while tiles[0].z >= target_zoom:
tile = tiles.pop_at(0)
tiles += [
Tile.new(tile.x * 2, tile.y * 2, tile.z + 1),
Tile.new(tile.x * 2 + 1, tile.y * 2, tile.z + 1),
Tile.new(tile.x * 2 + 1, tile.y * 2 + 1, tile.z + 1),
Tile.new(tile.x * 2, tile.y * 2 + 1, tile.z + 1) ]
return tiles
## Проверяется, есть ли в [param merge_set] 4 плитки, которые могут быть объединены.
## Если есть, то происходит их слияние. Возвращает список плиток, а также булево
## число, указывающее, были ли они объединены. При многократном применяя
## [method merge], можно упростить набор плиток.
func merge(merge_set):
var upwards_merge: = {}
for tile in merge_set.keys():
var tile_parent = parent(tile)
if not upwards_merge.has(tile_parent):
upwards_merge[tile_parent] = {}
upwards_merge[tile_parent].update({tile: false})
var current_tileset: = []
var changed: = false
for supertile in upwards_merge.keys():
var tile_children = upwards_merge[supertile]
if len(tile_children) == 4:
current_tileset += [supertile]
changed = true
else:
current_tileset += [tile_children]
return [current_tileset, changed]
## Максимально уменьшает размер набора плиток путем слияния листьев с родителями.
## [param tiles] - Последовательность плиток для слияния.
## [return] [Array]
func simplify(tiles: Array):
# Проверьте, существует ли уже плитка и ее родитель.
# Убедитесь, что плитки отсортированы по масштабу, поэтому родительские
# плитки встречаются первыми. Если да, отбросьте дочернюю плитку
# (она покрыта родительской).
var root_set = {}
tiles.sort_custom(func(a, b): return a[2] < b[2])
for tile in tiles:
var is_new_tile: bool = true
var supertiles = []
for i in tile.z:
supertiles.append(parent(tile, i))
for supertile in supertiles:
if supertile in root_set:
is_new_tile = false
continue
if is_new_tile:
root_set.update({tile: false})
# Повторять слияние до тех пор, пока дальнейшее упрощение перестанет быть невозможным.
var is_merging: bool = true
while is_merging:
var root_set_is_merging = merge(root_set)
root_set = root_set_is_merging[0]
is_merging = root_set_is_merging[1]
return root_set
## Получение наименьшей плитки, содержащей географическую границу.
## Если bbox охватывает линии lng 0 или lat 0, ограничивающая плитка будет иметь вид
## Tile(x=0, y=0, z=0).
## [param bbox] последовательность [float] значений границ запада, юга, востока,
## севера в десятичных градусах.
## [return] [Tile]
func bounding_tile(bbox, truncate: bool = false):
if len(bbox) == 2:
bbox += bbox
var w = bbox[0]
var s = bbox[1]
var e = bbox[2]
var n = bbox[3]
if truncate:
var w_s = truncate_lnglat(w, s)
w = w_s[0]
s = w_s[1]
var e_n = truncate_lnglat(e, n)
n = e_n[0]
e = e_n[1]
e -= LL_EPSILON
s += LL_EPSILON
var tmin = create_tile(w, n, 32)
var tmax = create_tile(e, s, 32)
assert(tmin.is_valid())
assert(tmax.is_valid())
var cell = [tmin.x, tmin.y, tmax.x, tmax.y]
var z = _getBboxZoom(cell)
if z == 0:
return Tile.new()
var x = rshift(cell[0], 32 - z)
var y = rshift(cell[1], 32 - z)
return Tile.new(x, y, z)
func _getBboxZoom(bbox: Array):
for z in MAX_ZOOM:
var mask: int = 1 << (32 - (z + 1))
if (bbox[0] & mask) != (bbox[2] & mask) or (bbox[1] & mask) != (bbox[3] & mask):
return z
return MAX_ZOOM
## Получение GeoJSON-функции, соответствующей плитке
## [param tile] [Плитка] или последовательность int - Может быть либо экземпляром [Плитка], либо 3 int, X, Y, Z.
## [Tile] или 3 целых, X, Y, Z.
## [param fid] [String] - Идентификатор объекта.
## [param props] [Dictionary] - Необязательные дополнительные свойства признака.
## [param projected] [String] - Можно создать нестандартный веб-меркатор GeoJSON.
## передав 'mercator'.
## [param buffer] [float] - Необязательное буферное расстояние для полигона GeoJSON.
## [param precision] [int] - Координаты GeoJSON будут усечены до данного
## количества знаков после запятой.
## [return] [Dictionary]
func feature(tile: Tile, fid=null, props=null, projected='geographic', buffer=null):
var west_south_east_north = bounds(tile).as_array()
var west = west_south_east_north[0]
var south = west_south_east_north[1]
var east = west_south_east_north[2]
var north = west_south_east_north[3]
if projected == 'mercator':
var west_south = xy(west, south, false)
west = west_south[0]
south = west_south[1]
var east_north = xy(east, north, false)
east = east_north[0]
north = east_north[1]
if buffer:
west -= buffer
south -= buffer
east += buffer
north += buffer
west = round(west)
south = round(south)
east = round(east)
north = round(north)
var bbox: = [min(west, east), min(south, north), max(west, east), max(south, north)]
var geom: = {'type': 'Polygon', 'coordinates': [[[west, south], [west, north], [east, north], [east, south], [west, south]]] }
var xyz: = str(tile)
var feat: = {
'type': 'Feature',
'bbox': bbox,
'id': xyz,
'geometry': geom,
'properties': {'title': 'XYZ tile %s' % xyz} }
if props:
feat['properties'].update(props)
if fid != null:
feat['id'] = fid
return feat
func isinstance(obj, types: Array):
for type in types:
if is_instance_of(obj, type):
return true
return false
## Все кортежи координат из геометрии, объекта или коллекции
## [param lng] [float] - Долгота
## [param lat] [float] - широта
## [return] [Array]
func _coords(obj) -> Array:
var coordinates = []
if is_instance_of(obj, TYPE_ARRAY):
coordinates = obj
elif 'features' in obj:
for feat in obj['features']:
coordinates = feat['geometry']['coordinates']
elif 'geometry' in obj:
coordinates = obj['geometry']['coordinates']
else:
coordinates = obj.get('coordinates', obj)
var v: = []
for e in coordinates:
if isinstance(e, [TYPE_FLOAT, TYPE_INT]):
v.append(coordinates)
break
else:
for f in _coords(e):
v.append([f[0], f[1]])
return v
func func1(bbox: Array, coords: Array):
var w = bbox[0]
var s = bbox[1]
var e = bbox[2]
var n = bbox[3]
var lng = coords[0]
var lat = coords[1]
return [min(w, lng), min(s, lat), max(e, lng), max(n, lat)]
## Возвращает граничную область объекта GeoJSON.
## [param obj] - Отображение, геометрия GeoJSON, объект или коллекция объектов.
## [returns] [LngLatBbox]
func geojson_bounds(obj):
var coords: Array = _coords(obj)
var wsen = coords.reduce(func1, [180.0, 90.0, -180.0, -90.0])
return LngLatBbox.new(wsen[0], wsen[1], wsen[2], wsen[3])