Files
uarep-ctl/scenes/tilemap/tilemap.gd

651 lines
18 KiB
GDScript

@tool
extends CanvasGroup
class_name MercatorTileMap
## Отображение карт WEB Mercator
#http://10.1.1.220/osm_tiles/0/0/0.png
#https://tile.openstreetmap.org/{z}/{x}/{y}.png
@export var base_url: String = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png':
set(v):
if base_url != v:
base_url = v
_clean_all()
queue_redraw()
@export var zoom_step: float = 0.025
@export var tile_width: float = 256.0
@export var tile_height: float = 256.0
@export_range(1, 1000, 1) var max_concurrent_requests:int = 5
@export_range(100, 100000, 1) var max_cached_tiles: int = 200
@export_range(0, 25, 1) var max_zoom_level: int = 19
@export_range(0, 25, 1) var min_zoom_level: int = 0
@export_color_no_alpha() var back_color: = Color.BLACK
@export var animation_time: float = 1.0
const default_size: = 400.0
@export var size: Vector2i = Vector2i(default_size, default_size):
set(v):
$canvas.size = v
_on_resize()
queue_redraw()
get:
return $canvas.size
@export var ax: float = 0.0:
set(v):
_xyz.x = v
queue_redraw()
get:
return _xyz.x
@export var ay: float = 0.0:
set(v):
_xyz.y = v
queue_redraw()
get:
return _xyz.y
@export var az: float = 0.0:
set(v):
_xyz.z = v
queue_redraw()
get:
return _xyz.z
var _xyz: Vector3 = Vector3(0, 0, min_zoom_level)
var _cache: Dictionary = {}
var _queue: Dictionary = {}
var _dragging: bool = false
var _drag_pos: Vector2 = Vector2.ZERO
var _last_cache_check: float = 0
var _rollover: bool = false
var _marks: Dictionary = {}
var _req_count: = 0
var image: = Image.new()
var image_load_proc: Callable
var now: = 0
var visible_marks_count: = 0
var mouse_wheel_count: = 0
var map_server_available: = false
## Вызывается после того, как загрузка плиток завершена
signal load_completed()
## Вызывается при запуске загрузки плиток
signal load_started()
## Вызывается при получении ответа от сервера карт
signal map_server_online()
## Вызывается при отсутствии ответа от сервера карт
signal map_server_offline()
const image_load_proc_names = {
'png': 'load_png_from_buffer',
'jpg': 'load_jpg_from_buffer',
'tga': 'load_tga_from_buffer',
'webp': 'load_webp_from_buffer' }
@export_enum('png', 'jpg', 'jpeg', 'tga', 'webp') var image_type: String = 'png':
set(v): image_load_proc = Callable(image, image_load_proc_names[v])
func _ready():
$canvas.connect('mouse_entered', func(): _rollover = true)
$canvas.connect('mouse_exited', func(): _rollover = false)
_on_resize()
image_type = 'png'
func _on_resize() -> void:
var center = $canvas.size / 2.0
$canvas/spr_mask.position = center
$canvas/spr_mask.scale = $canvas.size / default_size
## Создаёт новую отметку из координат экрана
func add_mark_from_screen_pos(id: int, x: float, y: float, mark: Node):
if _marks.has(id):
return false
var wrld_pos = screen_to_world(_xyz, x, y)
var p = world_to_tile(wrld_pos.x, wrld_pos.y, _xyz.z)
var tile_index = xyz_to_idx(p.x, p.y, _xyz.z)
var dot = { 'position': wrld_pos, 'tile_index': tile_index, 'mark': mark }
_marks[id] = dot
add_child(mark)
fix_mark_position(dot)
return true
func get_marks_count(): return _marks.size()
func delete_mark(id: int):
if not _marks.has(id):
return false
_marks[id].mark.queue_free()
_marks.erase(id)
queue_redraw()
## Создаёт новую отметку из долготы и широты
func add_mark_from_lon_lat(id: int, lon: float, lat: float, mark: Node, gap_size: float = 32):
if _marks.has(id):
return false
var wrld_pos = lonlat_to_world(lon, lat)
var scr_pos = world_to_screen(wrld_pos.x, wrld_pos.y)
mark.set_global_position(scr_pos)
_marks[id] = {'position': wrld_pos, 'mark': mark, 'gap_size': gap_size }
add_child(mark)
queue_redraw()
return true
## Возвращает новую позицию для точки, если она выходит из поля видимости
func fix_mark_position(dot):
if is_mark_visible(dot):
return
var v = 2 * tile_width * pow(2, _xyz.z)
if dot.position.x < _xyz.x - $canvas.size.x / v:
dot.position.x = _xyz.x - $canvas.size.x / v
elif dot.position.x > _xyz.x + $canvas.size.x / v:
dot.position.x = _xyz.x + $canvas.size.x / v
func is_mark_visible(dot):
var screen_pos = world_to_screen(dot.position.x, dot.position.y)
return screen_pos.x > 0 and screen_pos.x < $canvas.size.x
func _input(e):
var lmp = get_local_mouse_position()
if not _rollover:
return
if e is InputEventMouseButton:
if e.pressed:
if e.button_index == MOUSE_BUTTON_WHEEL_DOWN:
mouse_wheel_count -= 1
queue_redraw()
elif e.button_index == MOUSE_BUTTON_WHEEL_UP:
mouse_wheel_count += 1
queue_redraw()
if e.button_index == MOUSE_BUTTON_LEFT:
_dragging = e.pressed
_drag_pos = screen_to_world(_xyz, lmp.x, lmp.y)
elif e is InputEventMouseMotion:
if _dragging:
var wp = screen_to_world(_xyz, lmp.x, lmp.y)
var diff = _drag_pos - wp
_xyz.x += diff.x
_xyz.y += diff.y
queue_redraw()
## Возвращает true, если видна область за краем карты
func is_border_visible():
var z = min(max_zoom_level, _xyz.z)
return screen_to_tile(0, 0, z).y != 0
func multiply_zoom(multiplier: float, pivot: Vector2):
var p1: = screen_to_world(_xyz, pivot.x, pivot.y)
_xyz.z = clampf(_xyz.z * multiplier, min_zoom_level, max_zoom_level)
var p2: = screen_to_world(_xyz, pivot.x, pivot.y)
_xyz.x -= p2.x - p1.x
_xyz.y -= p2.y - p1.y
func set_zoom(zoom: float, pivot=null):
if pivot == null:
pivot = $canvas.size / 2.0
var p1: = screen_to_world(_xyz, pivot.x, pivot.y)
_xyz.z = clampf(zoom, max_zoom_level, min_zoom_level)
var p2: = screen_to_world(_xyz, pivot.x, pivot.y)
_xyz.x -= p2.x - p1.x
_xyz.y -= p2.y - p1.y
func _draw():
var lmp = get_local_mouse_position()
if mouse_wheel_count > 0:
multiply_zoom(1.0 + zoom_step, lmp)
mouse_wheel_count -= 1
elif mouse_wheel_count < 0:
multiply_zoom(1.0 - zoom_step, lmp)
mouse_wheel_count += 1
var z = min(max_zoom_level, _xyz.z)
var t1 = screen_to_tile(0, 0, z)
var t2 = screen_to_tile($canvas.size.x, $canvas.size.y, z)
for tx in range(t1.x, t2.x + 1):
for ty in range(t1.y, t2.y + 1):
_draw_tile(tx, ty, z)
var n = pow(2, z)
var dx = n * tile_width
var c = $canvas.size / 2.0
var r = min($canvas.size.x, $canvas.size.y) / 2.0
visible_marks_count = 0
for dot in _marks.values():
var pos = world_to_screen(dot.position.x, dot.position.y)
if pos.x >= 0:
pos.x = fmod(pos.x, dx)
else:
pos.x = -pos.x - 1
pos.x = int(dx - 1) - fmod(pos.x, dx)
dot.mark.position = pos
dot.mark.visible = c.distance_to(pos) < (r - dot.gap_size)
visible_marks_count += int(dot.mark.visible)
func _clean_all():
_queue.clear()
_cache.clear()
_marks.clear()
_req_count = 0
for c in get_children().filter(func (item): return not item.is_in_group('gui_items')):
remove_child(c)
c.queue_free()
# get the tile from queue with the newest timestamp
func get_next_in_queue():
var tile = null
for idx in _queue:
var t = _queue.get(idx)
if not tile or t.t > tile.t:
tile = t
return tile
# delete oldest tiles from cache
func _clean_cache():
var overflow = _cache.size() - max_cached_tiles
if overflow <= 0:
return
var vals = _cache.values()
vals.sort_custom(func(t1, t2): return t1.t < t2.t)
for i in overflow:
_cache.erase(vals[i].i)
func _process(delta):
now += delta
# if cache is overflowing - clean it
if not _last_cache_check or now - _last_cache_check > 5:
_last_cache_check = now
_clean_cache()
while not _queue.is_empty() and (_req_count < max_concurrent_requests):
var tile = get_next_in_queue()
if not tile:
break
var req = HTTPRequest.new()
_req_count += 1
req.set_meta('tile', tile)
add_child(req)
req.name = NodePath(str(tile.i))
_queue.erase(tile.i)
req.request_completed.connect(_response.bind(req, tile))
if req.request(tile.url) != OK:
tile.t = now
remove_child(req)
req.queue_free()
_req_count -= 1
push_error('ошибка: запрос плитки \"%s\" не отправлен' % tile.url)
if _queue.is_empty():
emit_signal('load_completed')
# result, response_code, headers, body
func _response(result, code, _headers: Array, body: PackedByteArray, req, tile: Tile):
_req_count -= 1
remove_child(req)
req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS:
if map_server_available:
map_server_available = false
emit_signal('map_server_offline')
_queue[tile.i] = tile
return
if not map_server_available:
map_server_available = true
emit_signal('map_server_online')
if code == 404:
push_error('ошибка: на сервере нет \"%s\"' % tile.url)
return
if Error.OK != image_load_proc.call(body):
push_error('ошибка: не удалось получить изображение (неверный формат?) из \"%s\"' % tile.url)
return
# create texture from image and add it to the cache
tile.texture = ImageTexture.create_from_image(image)
_cache[tile.i] = tile
# redraw the map
queue_redraw()
func _draw_subtile(tx: int, ty: int, tz: int, origx: int, origy: int, origz: float) -> bool:
var subtile = get_tile(tx, ty, tz)
if not subtile:
return false
var p1 = tile_to_screen(origx, origy, origz)
var p2 = tile_to_screen(origx + 1, origy + 1, origz)
var x1 = tile_to_screen(tx, ty, tz)
var x2 = tile_to_screen(tx + 1, ty + 1, tz)
var xdiff = x2.x - x1.x
var xrat1 = (p1.x - x1.x) / xdiff
var xrat2 = (p2.x - x1.x) / xdiff
var xwidth = xrat2 - xrat1
var ydiff = x2.y - x1.y
var yrat1 = (p1.y - x1.y) / ydiff
var yrat2 = (p2.y - x1.y) / ydiff
var yheight = yrat2 - yrat1
var rect = Rect2(xrat1 * tile_width, yrat1 * tile_height, xwidth * tile_width, yheight * tile_height)
if subtile.texture:
draw_texture_rect_region(subtile.texture, Rect2(p1, p2 - p1), rect)
return true
return false
func _draw_tile(tx: int, ty: int, z: float):
var tz = floor(z)
var p1 = tile_to_screen(tx, ty, z)
var p2 = tile_to_screen(tx + 1, ty + 1, z)
var tile = get_tile(tx, ty, tz)
if tile:
if tile.texture:
draw_texture_rect(tile.texture, Rect2(p1, p2 - p1), false, Color.WHITE, false)
else:
var zzz = tz
var txx = tx
var tyy = ty
while zzz > 1:
zzz -= 1
txx = floor(txx / 2.0)
tyy = floor(tyy / 2.0)
if _draw_subtile(txx, tyy, zzz, tx, ty,z):
break
## convert lon/lat to world coords
func lonlat_to_world(lon: float, lat: float) -> Vector2:
var x = lon / 180.0
var latsin = sin(deg_to_rad(lat) * sign(lat))
var y = (sign(lat) * (log((1.0 + latsin) / (1.0 - latsin)) / 2.0)) / PI
return Vector2(x, y)
## convert lon/lat to screen coords
func lonlat_to_screen(lon: float, lat: float) -> Vector2:
var w = lonlat_to_world(lon, lat)
return world_to_screen(w.x, w.y)
## convert lon/lat to tile coords
func lonlat_to_tile(lon: float, lat: float, z: float) -> Vector2:
var w = lonlat_to_world(lon,lat)
return world_to_tile(w.x, w.y, z)
## convert world coords to lon/lat
func world_to_lonlat(wx: float, wy: float) -> Vector2:
var lon = wx * 180.0
var lat = rad_to_deg(atan(sinh(wy * PI)))
return Vector2(lon, lat)
## convert screen coords to lon/lat
func screen_to_lonlat(sx: float, sy: float) -> Vector2:
var w = screen_to_world(_xyz, sx, sy)
return world_to_lonlat(w.x, w.y)
## convert tile coords to lon/lat
func tile_to_lonlat(tx: float, ty: float, tz: float) -> Vector2:
var w = tile_to_world(tx, ty, tz)
return world_to_lonlat(w.x, w.y)
## convert screen coords to world coords
func screen_to_world(xyz: Vector3, sx: float, sy: float) -> Vector2:
var n = pow(2.0, xyz.z)
var span_w = n * tile_width
var span_h = n * tile_height
var px = sx - $canvas.size.x / 2 + span_w / 2
var py = sy - $canvas.size.y / 2 + span_h / 2
var xr = px / span_w
var yr = py / span_h
var x = (xr * 2.0 - 1.0) + xyz.x
var y = ((-yr * 2.0) + 1.0) + xyz.y
return Vector2(x, y)
## convert screen coords to tile coords
func screen_to_tile(sx: float, sy: float, z: float) -> Vector2:
var world = screen_to_world(_xyz, sx, sy)
return world_to_tile(world.x, world.y, z)
## convert tile coords to screen coords
func tile_to_screen(tx: float, ty: float, tz: float) -> Vector2:
var w = tile_to_world(tx, ty, tz)
return world_to_screen(w.x, w.y)
## convert tile coords to world coords
func tile_to_world(tx: float, ty: float, tz: float) -> Vector2:
var n = pow(2.0, floor(tz))
var x = (tx / n) * 2.0 - 1.0
var y = -((ty / n) * 2.0 - 1.0)
return Vector2(x, y)
## convert world coords to tile coords
func world_to_tile(wx: float, wy: float, z: float) -> Vector2:
var n: = pow(2.0, floor(z))
var tx: = ((wx + 1.0) / 2.0) * n
var ty: = ((-wy + 1.0) / 2.0) * n
tx = floor(tx)
ty = floor(ty)
return Vector2(tx, ty)
## convert world coords to screen coords
func world_to_screen(wx: float, wy: float) -> Vector2:
var n: = pow(2.0, _xyz.z)
var w: = n * tile_width
var h: = n * tile_height
var xr: = ((wx - _xyz.x) + 1.0) / 2.0
var yr: = (-(wy - _xyz.y) + 1.0) / 2.0
var x = w * xr - w / 2.0 + $canvas.size.x / 2.0
var y = h * yr - h / 2.0 + $canvas.size.y / 2.0
return Vector2(x, y)
## get the tile index from xyz tile coords
func xyz_to_idx(x: float, y: float, z: float) -> int:
var i: = (pow(4.0, z) - 1.0) / 3.0
var n: = pow(2.0, z)
return int(i + (y * n + x))
func get_tile(x: int, y: int, z: int, create: bool = true) -> Tile:
var n = pow(2, z)
if x >= 0:
x %= int(n)
else:
x = -x - 1
x = int(n - 1) - (x % int(n))
# out of bounds
if z < 0 or y < 0 or y >= n:
return null
# get tile index
var idx = xyz_to_idx(x, y, z)
var tile = null
# retrieve from current requests...
var req = get_node_or_null(NodePath(str(idx)))
if req:
tile = req.get_meta('tile')
tile.t = now
return tile
# retrieve from cache
tile = _cache.get(idx)
if tile:
tile.t = now
return tile
# retrieve from queue
tile = _queue.get(idx)
if tile:
tile.t = now
return tile
# create a new tile - add it to queue
var queue_size: = _queue.size()
if create and (not $canvas/player.is_playing()):
tile = Tile.new(idx, x, y, z)
tile.url = base_url.replace('{x}', str(x)).replace('{y}', str(y)).replace('{z}', str(z))
tile.t = now
_queue[idx] = tile
if queue_size == 0 and _queue.size():
emit_signal('load_started')
return tile
func lonlat_to_dms(lon: float, lat: float) -> String:
var pf = 'N' if lat >= 0 else 'S'
lat = abs(lat)
var deg = floor(lat)
var minute = (lat - deg) * 60
var second = (minute - floor(minute)) * 60
var text = ('%02d' % deg) + ('%02d' % floor(minute)) + ('%02d' % floor(second)) + pf + ' '
pf = 'E' if lon >= 0 else 'W'
lon = abs(lon)
deg = floor(lon)
minute = (lon - deg) * 60
second = (minute - floor(minute)) * 60
text += ('%02d' % deg) + ('%02d' % floor(minute)) + ('%02d' % floor(second)) + pf
return text
class Tile:
var i: int = 0
var x: int = 0
var y: int = 0
var z: int = 0
var t: int = 0
var url: String = ''
var texture: Texture2D = null
func _init(i0: int, x0: int, y0: int, z0: int):
self.i = i0
self.x = x0
self.y = y0
self.z = z0
func haversine_m(lat1, lon1, lat2, lon2):
# Радиус Земли в метрах
var R: = 6372795.0
# Преобразование градусов в радианы
var rlat1: = deg_to_rad(lat1)
var rlon1: = deg_to_rad(lon1)
var rlat2: = deg_to_rad(lat2)
var rlon2: = deg_to_rad(lon2)
var cl1: = cos(rlat1)
var cl2: = cos(rlat2)
var sl1: = sin(rlat1)
var sl2: = sin(rlat2)
var delta: = rlon2 - rlon1
var cdelta: = cos(delta)
var sdelta: = sin(delta)
# вычисления длины большого круга
var y: = sqrt(pow(cl2 * sdelta, 2) + pow(cl1 * sl2 - sl1 * cl2 * cdelta, 2))
var x: = sl1 * sl2 + cl1 * cl2 * cdelta
var ad: = atan2(y, x)
var dist: = ad * R
return dist
func remove_closest_dot(screen_pos):
var closest_dot = find_at_screen_position(screen_pos)
if not closest_dot:
return
remove_child(closest_dot.mark)
closest_dot.mark.queue_free()
_marks.erase(closest_dot)
func find_at_screen_position(screen_pos: Vector2):
var closest_dot = null
var closest_distance = INF
for mark in _marks.values():
var dot_screen_pos = world_to_screen(mark.position.x, mark.position.y)
var distance = dot_screen_pos.distance_to(screen_pos)
if distance < closest_distance:
closest_distance = distance
closest_dot = mark
return closest_dot
func clear() -> void:
_clean_all()
queue_redraw()
func set_coordinates(lon: float, lat: float):
var anim: Animation = $canvas/player.get_animation('goto')
var wrld_pos = lonlat_to_world(lon, lat)
anim.track_insert_key(0, 0, _xyz.x)
anim.track_insert_key(1, 0, _xyz.y)
anim.track_insert_key(0, animation_time, wrld_pos.x)
anim.track_insert_key(1, animation_time, wrld_pos.y)
anim.length = animation_time
$canvas/player.play('goto')
func blend_mask(val: bool) -> void:
$canvas/spr_mask.visible = val
queue_redraw()