extends Node ## Веб-утилиты для работы с меркаторскими XYZ-плитками. 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: = 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): self.x = x0 self.y = y0 self.z = z0 self.is_root = is_root0 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 func _bound_index(index: int) -> int: var mi: int = 2 ** self.z if index < 0: index *= -1 index %= mi index = mi - index else: index %= mi return index func fix_xy(): self.x = _bound_index(self.x) self.y = _bound_index(self.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 func asinh(val: float): return log(val + sqrt(val * val + 1.0)) ## Lon./lat. to tile numbers func deg2num(lat_deg: float, lon_deg: float, zoom: int): var lat_rad: = radians(lat_deg) var n: = float(1 << zoom) var xtile: = int((lon_deg + 180.0) / 360.0 * n) var ytile: = int((1.0 - asinh(tan(lat_rad)) / PI) / 2.0 * n) return [xtile, ytile] ## Tile numbers to lon./lat. func num2deg(xtile: int, ytile: int, zoom: int): var n: = float(1 << zoom) var lon_deg: = float(xtile) / n * 360.0 - 180.0 var lat_rad: = atan(sinh(PI * (1.0 - 2.0 * float(ytile) / n))) var lat_deg = degrees(lat_rad) return [lat_deg, lon_deg] ## Расстояние по горизонтали в метрах, представленное одним пикселем ## [paramn lat] - Широта, градусы.[br] ## [paramn zoom] - Уровень детализации.[br] ## [paramn size] - Размер плитки в пикселях.[br] func dist_per_pixel(lat: float, zoom: int, size: int = 256) -> float: return (CE / float(size)) * cos(radians(lat)) / float(2 ** zoom) ## Расстояние по горизонтали в метрах, представленное одной плиткой ## [paramn lat] - Широта, градусы.[br] ## [paramn 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 [lat_deg, lon_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] возвращает до восьми ## соседних плиток, при этом плитки будут опущены, если они не являются действительными, например. ## 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: 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 ## Возвращает все плитки вокруг заданной плитки 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 = int(roundf(r * cos(ra))) + tile.x var iy = int(roundf(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])