Files
uarep-ctl/scripts/mercator.gd

748 lines
31 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.

class_name Mercator extends Node
## Утилиты для работы с меркаторовскими XYZ-плитками.
const R2D: = 180.0 / PI
const RE: = 6378137.0 ## Радиус Земли экваториальный в метрах.
const RP: = 6356752.31424518 ## Радиус Земли полярный в метрах.
const CE: = 2.0 * PI * RE ## Длина окружности Земли в метрах по экватору.
const CP: = 2.0 * PI * RP ## Длина окружности Земли в метрах через полюсы.
const EPSILON: = 1e-14
const LL_EPSILON: = 1e-11
const MAX_ZOOM: = 20
const MAX_NORTH = 85.051129
const MAX_SOUTH = -85.051129
## XYZ-плитка веб-меркатора [param x], [param y], [param z] индексы.
## [b]x[/b] и [b]y[/b] плитки и уровень масштабирования [b]z[/b].
class Tile:
var x: int
var y: int
var z: int
var lng: float ## Координаты, которые плитка должна покрывать, долгота, градусы.
var lat: float ## Широта, градусы.
var is_root: bool ## Признак, что плитка корневая (покрывает координаты).
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, is_root0: bool = true):
x = x0
y = y0
z = z0
is_root = is_root0
func is_valid() -> bool:
if (z >= 0) and (z < MAX_ZOOM):
var mz: int = 2 ** z
return (x >= 0) and (x < mz) \
and (y >= 0) and (y < mz)
return false
func _bound_index(index: int) -> int:
var mi: int = 2 ** z
if index < 0:
index = -index - 1;
index = int(mi - 1) - (index % int(mi))
else:
index %= mi
return index
func fix_xy():
x = _bound_index(x)
y = _bound_index(y)
## Пара долготы и широты.
class LngLat:
var lng: float = 0.0 ## Долгота в десятичных градусах восточной долготы.
var lat: float = 0.0 ## Широта в десятичных градусах северной широты.
## Возвращает в виде массива.
func as_array() -> Array: return [self.lng, self.lat]
func _to_string() -> String: return '[%f, %f, %f]' % [self.x, self.y, self.z]
func _init(lng0, lat0):
self.lng = lng0
self.lat = lat0
## Географические границы. Хранит значения границ в десятичных градусах.
class LngLatBbox:
var west: float ## Запад
var south: float ## Юг
var east: float ## Восток
var north: float ## Север
## Возвращает в виде массива.
func as_array() -> Array: return [self.west, self.south, self.east, self.north]
func _to_string() -> String: return '[%f, %f, %f, %f]' % [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
## Граничная область веб-меркатора. Хранит значения границ в метрах.
class Bbox:
var left: float
var bottom: float
var right: float
var top: float
func as_array() -> 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) -> float: return (v * 180.0) / PI
func radians(v: float) -> float: return (v * PI) / 180.0
func rshift(val: int, n: int) -> int: return (val % 0x100000000) >> n
## Возвращает арксинус гиперболический от значения [param val].
func asinh(val: float): return log(val + sqrt(val * val + 1.0))
## Преобразует [param lon] в градусах восточной долготы, [param lat] в градусах.
## северной долготы, [param alt] высоты в метрах в фиксированные координаты (x, y, z) Земли.
func lnglat_to_ecef(lng: float, lat: float, alt: float = 0.0) -> Array:
alt += (RE + RP) / 2.0
var latrad: float = radians(lat)
var lonrad: float = radians(lng)
var coslat: float = cos(latrad)
var sinlat: float = sin(latrad)
var coslon: float = cos(lonrad)
var sinlon: float = sin(lonrad)
var term1: float = (RE * coslat) / tools.hypot(RE * coslat, RP * sinlat)
var term2: float = alt * coslat + term1;
var x: float = coslon * term2
var y: float = sinlon * term2
var z: float = alt * sinlat + (RP * sinlat) / tools.hypot(RE * coslat, RP * sinlat)
return [x, y, z]
func find_up_left(tiles: Array):
var x = 0
var y = 0
for tile in tiles:
if x < tile.x: x = tile.x
if y < tile.y: y = tile.y
for tile in tiles:
if tile.x == x and tile.y == y:
return tile
return null
## [param lon]/[param lat]. для номера плитки.
func deg2num(lng: float, lat: float, zoom: int):
var n: = float(1 << zoom)
var xtile: = int((lng + 180.0) / 360.0 * n)
var ytile: = int((1.0 - asinh(tan(radians(lat))) / PI) / 2.0 * n)
return [xtile, ytile]
## Номера плитки для [param lon]/[param lat].
func num2deg(x: int, y: int, zoom: int):
var n: = float(1 << zoom)
var lng: = float(x) / n * 360.0 - 180.0
var lat: = atan(sinh(PI * (1.0 - 2.0 * float(y) / n)))
return [lng, degrees(lat)]
## Расстояние по горизонтали в метрах, представленное одним пикселем.
## [param lat] - Широта, градусы.[br]
## [param zoom] - Уровень детализации.[br]
## [param size] - Размер плитки в пикселях.[br]
func dist_per_pixel(lat: float, zoom: int, size: int = 256) -> float:
return (CE / float(size)) * cos(radians(lat)) / float(2 ** zoom)
## Расстояние по горизонтали в метрах, представленное одной плиткой.
## [param lat] - Широта, градусы.[br]
## [param zoom] - Уровень детализации.[br]
func dist_per_tile(lat: float, zoom: int) -> float:
return CE * cos(radians(lat)) / float(2 ** zoom)
## Возвращает плитки и занимаемый ими размер области в пикселях:
## ширина, высота, левая граница, верхняя граница, плитки.[br]
## [param west] - Западная граница области.[br]
## [param east] - Восточная граница области.[br]
## [param south] - Южная граница области.[br]
## [param north] - Северная граница области.[br]
## [param size] - Размер плитки в пикселях.[br]
## [param zoom] - Детализация.[br]
func get_tiles(west: float, south: float, east: float, north: float, zoom: int, size: int) -> Array:
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]
## Возвращает размер области в метрах.[br]
## [param west] - Западная граница области.[br]
## [param param] - Восточная граница области.[br]
## [param south] - Южная граница области.[br]
## [param north] - Северная граница области.[br]
func get_size_from_degrees(west: float, south: float, east: float, north: float) -> Array[float]:
var xy0: = xy(west, north, true)
var xy1: = xy(east, south, true)
return [xy0[0] - xy1[0], xy0[1] - xy1[1]]
## Возвращает границы области в градусах.[br]
## [param m] - Размер карты в метрах.[br]
## [param lng] - Долгота центра области в градусах (-180...180).[br]
## [param lat] - Широта центра области в градусах (-90...90).[br]
func get_degrees_bounds(m: float, lng: float, lat: float) -> Array[float]:
m /= 2.0
var x_y = xy(lng, lat, true)
return [x_y[0] - m, x_y[1] - m, x_y[0] + m, x_y[1] + m]
## Возвращает границы плиток в градусах.
func get_tiles_bounds(tiles: Array[Tile]) -> Array:
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]
## Возвращает левую верхнюю долготу и широту плитки.[br]
## [param tile] - Плитка.[br]
## [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) -> LngLat:
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)
## Возвращает левую верхнюю долготу и широту плитки.[br]
## [param tile] - Плитка.[br]
## [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 [lon_deg, lat_deg]
## Возвращает граничную область плитки.[br]
## [param tile] - Плитка.[br]
func bounds(tile: Tile) -> LngLatBbox:
var z2: = pow(2, tile.z)
var ul_lon_deg = float(tile.x) / z2 * 360.0 - 180.0
var lr_lon_deg = (float(tile.x) + 1.0) / z2 * 360.0 - 180.0
var ul_lat_rad = atan(sinh(PI * (1.0 - 2.0 * float(tile.y) / z2)))
var ul_lat_deg = degrees(ul_lat_rad)
var lr_lat_rad = atan(sinh(PI * (1.0 - 2.0 * (float(tile.y) + 1.0) / 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 в метрах.[br]
## [param lng] - Долгота в десятичных градусах.[br]
## [param lat] - Широта в десятичных градусах.[br]
## [param truncate] - Нужно ли усекать входные данные до пределов веб-меркатора.[br]
## ([param lat] >= 90) [b]inf[/b] на северном и [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 [lng, lat]
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 в долготу и широту.[br]
## [param x], [param y] - Координаты веб-меркатора в метрах.[br]
## [param truncate] - Нужно ли усекать или обрезать входные данные до границ веб-меркатора.
func lnglat(x: float, y: float, truncate: bool = false) -> LngLat:
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 [lng, lat]
return LngLat.new(ll[0], ll[1])
## Соседи плитки. [method neighbors] возвращает до восьми.[br]
## Cоседних плиток, при этом плитки будут опущены, если они не являются действительными, например:[br]
## Tile(-1, -1, z). [param tile]: Плитка или последовательность int.[br]
## Может быть либо экземпляр Tile, либо 3 ints, X, Y, Z.[br]
## [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:
var tiles: = []
for i in [-1, 0, 1]:
for j in [-1, 0, 1]:
if i == 0 and j == 0:
continue
var t = Tile.new(tile.x + i, tile.y + j, tile.z)
t.fix_xy()
tiles.append(t)
return tiles
## Возвращает все плитки в радиусе [param r] вокруг заданной плитки [param tile].
func get_allaround_tiles(tile: Tile, mr: int = 1) -> Array:
var xys = {}
var mi: int = 2 ** tile.z
for r in mr:
for a in range(0, 360, 180 / mi):
var ra = radians(a)
var ix = roundi(r * cos(ra)) + tile.x
var iy = roundi(r * sin(ra)) + tile.y
var t = Tile.new(ix, iy, tile.z)
t.fix_xy()
xys[[t.x, t.y]] = false
var tiles = []
for txy in xys.keys():
tiles.append(Tile.new(txy[0], txy[1], tile.z))
return tiles
## Возвращает все плитки по периметру заданной плитки.
func get_perimetr_tiles(tile: Tile, mr: int = 1) -> Array:
var tiles = []
for i in range(-mr, mr + 1):
for j in range(-mr, mr + 1):
if i == 0 and j == 0: continue
var t = Tile.new(i + tile.x, j + tile.y, tile.z)
t.fix_xy()
tiles.append(t)
return tiles
## Получение меркаторской граничной рамки веб-плитки.[br]
## [param tile] - Плитка.[br]
## [i]Эпсилон вычитается из правой границы и прибавляется к нижней.[/i]
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: float, lat: float, truncate=false) -> Array:
var ll: = truncate_lnglat(lng, lat) if truncate else [lng, lat]
var x: float = 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] - Пара долготы и широты в десятичных градусах.[br]
## [param zoom] - Уровень масштабирования веб-меркатора.[br]
## [param truncate] - Нужно ли усекать вводимые данные до границ веб-меркатора.[br]
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
tile.lng = lng
tile.lat = lat
return tile
## Получение четверного ключа плитки.[br]
## [param tile] - Плитка.
func quadkey(tile: Tile, zoom: int) -> String:
var rs: = ''
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
## Получение плитки, соответствующей четверному ключу.[br]
## [param qk] - Строка четверного ключа.
func quadkey_to_tile(qk: Array) -> Tile:
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
## Получение плиток, перекрываемых географической рамкой.[br]
## [param west], [param south], [param east], [param north] - Значения границ в десятичных градусах.[br]
## [param zooms] - Один или несколько уровней масштабирования.[br]
## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора.[br]
## [i]В параметрах south и east используется небольшой эпсилон, чтобы
## функция выдавала ровно одну плитку при задании границ этой же плитки.[/i]
func create_tiles(west: float, south: float, east: float, north: float, zooms) -> Array[Tile]:
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(MAX_SOUTH, 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
## Получение родителя плитки. Родительская плитка - это плитка на один уровень масштабирования ниже, которая
## содержит данную "дочернюю" плитку.[br]
## [param tile] - Плитка.[br]
## [param zoom] - Определяет уровень *масштабирования* возвращаемой родительской плитки.
## По умолчанию это значение на один ниже, чем у плитки (непосредственного родителя).[br]
## [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) -> Tile:
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
## Получение дочерних элементов плитки. Дочерние элементы упорядочены:
## верхний-левый, верхний-правый, нижний-правый, нижний-левый. Возвращает все
## дочерние элементы с масштабом *zoom*, в порядке убывания глубины в порядке
## вращения по часовой стрелке. Если не указано, возвращаются ближайшие
## (т.е. zoom + 1) дочерние элементы плитки.[br]
## [param tile] - Плитка.[br]
## [param zoom] - Уровень детализации.
## [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: Tile, zoom: int) -> Array:
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) -> Array:
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] - Последовательность плиток для слияния.
func simplify(tiles: Array) -> 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] значений границ запада, юга, востока,
## севера в десятичных градусах.
func bounding_tile(bbox: Bbox, truncate: bool = false) -> Tile:
if truncate:
var w_s = truncate_lnglat(bbox.left, bbox.bottom)
bbox.left = w_s[0]
bbox.bottom = w_s[1]
var e_n = truncate_lnglat(bbox.right, bbox.north)
bbox.right = e_n[0]
bbox.top = e_n[1]
bbox.right -= LL_EPSILON
bbox.bottom += LL_EPSILON
var tmin = create_tile(bbox.left, bbox.top, 32)
var tmax = create_tile(bbox.right, bbox.bottom, 32)
assert(tmin.is_valid())
assert(tmax.is_valid())
var cell = [tmin.x, tmin.y, tmax.x, tmax.y]
var z = _get_bbox_zoom(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 _get_bbox_zoom(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-функции, соответствующей плитке.[br]
## [param tile] - Плитка.[br]
## [param fid] - Идентификатор объекта.[br]
## [param props] - Необязательные дополнительные свойства признака.[br]
## [param projected] - Можно создать нестандартный веб-меркатор GeoJSON передав 'mercator'.[br]
## [param buffer] - Необязательное буферное расстояние для полигона GeoJSON.[br]
## [param precision] - Координаты GeoJSON будут усечены до данного количества знаков после запятой.[br]
func feature(tile: Tile, fid:String = 'null', props=null, projected='geographic', buffer=null) -> Dictionary:
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) -> bool:
for type in types:
if is_instance_of(obj, type):
return true
return false
## Все кортежи координат из геометрии, объекта или коллекции.[br]
## [param lng] - Долгота.[br]
## [param lat] - широта.[br]
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) -> Array[float]:
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.[br]
## [param obj] - Отображение, геометрия GeoJSON, объект или коллекция объектов.[br]
func geojson_bounds(obj) -> LngLatBbox:
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])