@@ -2,12 +2,14 @@ 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 : = 28
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] индексы
@@ -16,15 +18,19 @@ 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 ) :
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 ) :
@@ -38,8 +44,12 @@ class Tile:
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
@@ -51,8 +61,12 @@ class LngLatBbox:
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
@@ -83,6 +97,21 @@ func radians(v: float) -> float: return (v * PI) / 180.0
func rshift ( val : int , n : int ) - > int : return ( val % 0x100000000 ) >> n
## Расстояние по горизонтали в метрах, представленное одним пикселем
## [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]
@@ -130,6 +159,7 @@ func get_degrees_bounds(m: float, lng: float, lat: float) -> Array[float]:
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 )
@@ -182,17 +212,17 @@ func ul2(tile: Tile) -> Array[float]:
## Возвращает граничную область плитки.[br]
## [param tile] - Плитка.[br]
func bounds ( tile : Tile ) - > LngLatBbox :
var Z 2: = pow ( 2 , tile . z )
var ul_lon_deg = tile . x / Z 2 * 360.0 - 180.0
var ul_lat_rad = atan ( sinh ( PI * ( 1 - 2 * tile . y / Z2 ) ) )
var z 2: = pow ( 2 , tile . z )
var ul_lon_deg = tile . x / z 2 * 360.0 - 180.0
var lr_lon_deg = ( tile . x + 1) / z 2 * 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 ) / Z 2 * 360.0 - 180.0
var lr_lat_rad = atan ( sinh ( PI * ( 1 - 2 * ( tile . y + 1 ) / Z2 ) ) )
var lr_lat_rad = atan ( sinh ( PI * ( 1 - 2 * ( tile . y + 1 ) / z 2) ) )
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 :
func truncate_lnglat ( lng : float , lat : float ) - > Array [ float ] :
if lng > 180.0 : lng = 180.0
elif lng < - 180.0 : lng = - 180.0
if lat > 90.0 : lat = 90.0
@@ -200,7 +230,7 @@ func truncate_lnglat(lng: float, lat: float) -> Array:
return [ lng , lat ]
## Преобразование долготы и широты в веб-меркатор x, y.[br]
## Преобразование долготы и широты в веб-меркатор x, y в метрах .[br]
## [param lng] - Долгота в десятичных градусах.[br]
## [param lat] - Широта в десятичных градусах.[br]
## [param truncate] - Нужно ли усекать входные данные до пределов веб-меркатора.[br]
@@ -234,14 +264,17 @@ func lnglat(x: float, y: float, truncate: bool = false) -> LngLat:
## >>> 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 ]
func neighbors ( tile : Tile ) - > Array :
var tiles : = [ ]
var hi = 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
if i == 0 and j == 0 :
continue
elif ( tile . x + i ) < 0 or ( tile . y + j ) < 0 :
continue
elif ( tile . x + i ) > hi or ( tile . y + j ) > hi :
continue
var t = Tile . new ( tile . x + i , tile . y + j , tile . z )
if t . is_valid ( ) :
tiles . append ( t )
@@ -260,9 +293,9 @@ func xy_bounds(tile: Tile, zoom: int) -> Bbox:
return Bbox . new ( left , bottom , right , top )
func _xy ( lng , lat , truncate = false ) - > Array :
func _xy ( lng , lat , truncate = false ) - > Array [ float ] :
var ll : = [ lng , lat ] if not truncate else truncate_lnglat ( lng , lat )
var x = ll [ 0 ] / 360.0 + 0.5
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 ]
@@ -274,25 +307,27 @@ func _xy(lng, lat, truncate=false) -> Array:
## [param truncate] - Нужно ли усекать вводимые данные до границ веб-меркатора.[br]
func create_tile ( lng : float , lat : float , zoom : int , truncate : bool = false ) - > Tile :
var xy0 : = _xy ( lng , lat , truncate )
var Z 2 = pow ( 2 , zoom )
var z 2 = pow ( 2 , zoom )
# Чтобы устранить потерю точности при переходе от одной плитки к другой
# и lng/lat, точки в пределах EPSILON от правой стороны тайла
# и lng/lat, точки в пределах EPSILON от правой стороны плитки
# считаются в следующей за ней плитке.
var tile : = Tile . new ( )
if xy0 [ 0 ] < = 0 : tile . x = 0
elif xy0 [ 0 ] > = 1 : tile . x = int ( Z 2 - 1 )
else : tile . x = int ( floor ( ( xy0 [ 0 ] + EPSILON ) * Z 2) )
elif xy0 [ 0 ] > = 1 : tile . x = int ( z 2 - 1 )
else : tile . x = int ( floor ( ( xy0 [ 0 ] + EPSILON ) * z 2) )
if xy0 [ 1 ] < = 0 : tile . y = 0
elif xy0 [ 1 ] > = 1 : tile . y = int ( Z 2 - 1 )
else : tile . y = int ( floor ( ( xy0 [ 1 ] + EPSILON ) * Z 2) )
elif xy0 [ 1 ] > = 1 : tile . y = int ( z 2 - 1 )
else : tile . y = int ( floor ( ( xy0 [ 1 ] + EPSILON ) * z 2) )
tile . z = zoom
tile . lng = lng
tile . lat = lat
return tile
## Получение четверного ключа плитки.[br]
## [param tile] Плитка
## [param tile] - Плитка
func quadkey ( tile : Tile , zoom : int ) - > String :
var rs : = ' '
for z in range ( zoom , 0 , - 1 ) :
@@ -337,9 +372,7 @@ func quadkey_to_tile(qk: Array) -> Tile:
## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора.[br]
## [i]В параметрах south и east используется небольшой эпсилон, чтобы
## функция выдавала ровно одну плитку при задании границ этой же плитки.[/i]
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 )
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 ) :
@@ -351,7 +384,7 @@ func create_tiles(west: float, south: float, east: float, north: float, zooms, t
var n = box [ 3 ]
# Зажим граничных значений.
w = max ( - 180.0 , w )
s = max ( - 85.051129 , s )
s = max ( MAX_SOUTH , s )
e = min ( 180.0 , e )
n = min ( 85.051129 , n )
for z in zooms :
@@ -412,8 +445,7 @@ func parent(tile: Tile, zoom=null) -> Tile:
## >>> 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) ' )
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 :
@@ -484,33 +516,26 @@ func simplify(tiles: Array) -> Array:
## Tile(x=0, y=0, z=0).
## [param bbox] последовательность [float] значений границ запада, юга, востока,
## севера в десятичных градусах.
func bounding_tile ( bbox , truncate : bool = false ) - > Tile :
if len ( bbox ) == 2 :
bbox += bbox
var w = bbox [ 0 ]
var s = bbox [ 1 ]
var e = bbox [ 2 ]
var n = bbox [ 3 ]
func bounding_tile ( bbox : Bbox , truncate : bool = false ) - > Tile :
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 ]
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 ]
e -= LL_EPSILON
s += LL_EPSILON
bbox . right -= LL_EPSILON
bbox . bottom += LL_EPSILON
var tmin = create_tile ( w , n , 32 )
var tmax = create_tile ( e , s , 32 )
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 = _getB boxZ oom ( cell )
var z = _get_b box_z oom ( cell )
if z == 0 :
return Tile . new ( )
@@ -521,7 +546,7 @@ func bounding_tile(bbox, truncate: bool = false) -> Tile:
return Tile . new ( x , y , z )
func _getB boxZ oom ( bbox : Array ) :
func _get_b box_z oom ( 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 ) :