@tool extends CanvasGroup ## Отображение карт WEB Mercator const MIN_ZOOM: float = 2.0 const MAX_ZOOM: float = 19.0 const TEST_COORDS: = [ Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1) ] var test_coords_i: = 3 #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 = 21 @export_color_no_alpha() var back_color: = Color.BLACK @export var animation_time: float = 1.0 @export var size: Vector2i = Vector2i(400, 400): 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) 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 spr_mask: = Sprite2D.new() 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' spr_mask.name = StringName('spr_mask') spr_mask.material = CanvasItemMaterial.new() spr_mask.material.blend_mode = CanvasItemMaterial.BLEND_MODE_SUB spr_mask.add_to_group('gui_items') $canvas.add_child(spr_mask) func _on_resize() -> void: var img_mask: = Image.create($canvas.size.x, $canvas.size.y, false, Image.FORMAT_RGBA8) var c = $canvas.size / 2.0 var r = min($canvas.size.x, $canvas.size.y) / 2.0 var color = Color.WHITE var sz = img_mask.get_size() for x in sz.x: for y in sz.y: var dist = c.distance_to(Vector2(x, y)) color.a = smoothstep(r - 2.0, r + 2.0, dist) img_mask.set_pixel(x, y, color) spr_mask.texture = ImageTexture.create_from_image(img_mask) spr_mask.position = $canvas.position + $canvas.size / 2.0 ## Создаёт новую отметку из координат экрана 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, MAX_ZOOM) 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, MIN_ZOOM) 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_l = 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: spr_mask.visible = val queue_redraw()