748 lines
31 KiB
GDScript
748 lines
31 KiB
GDScript
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])
|