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

839 lines
25 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@tool
extends CanvasGroup
class_name MercatorTileMap
## Отображение карт WEB Mercator
#http://10.1.1.220/osm_tiles/0/0/0.png
#http://169.254.27.71:8001/osm_tiles/{z}/{x}/{y}.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()
const TILE_REQUEST_NAME: = 'tile_request_%d'
@export var zoom_step: float = 0.1
@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 = 1024
@export_range(0, 20, 1) var max_zoom_level: int = 20
@export_range(0, 20, 1) var min_zoom_level: int = 0
@export_range(0, 20, 1) var max_zoom_level_server: int = 20
@export_color_no_alpha() var back_color: = Color.BLACK
@export var show_ship_radius: bool = true
@export var ship_radius_km: float = 8.0
@onready var scale_label: String
@onready var coordinates_label: String
@onready var threat_mark: PackedScene = load("res://scenes/tilemap/threat.tscn")
@onready var ship_mark: PackedScene = load("res://scenes/tilemap/ship-mark.tscn")
var ship_id := 9999
var ship_instance: Node2D = null
const default_size: = 400.0
var is_centered: = false
var is_out_border1: = true
var is_out_border2: = true
var threat_markers := {}
var zoom_timer: Timer
var zoom_action: String
@export var animation_time: float = 0.1:
set(val):
animation_time = val
@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, 0)
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 border1: ImageTexture
var border2: ImageTexture
## Вызывается после того, как загрузка плиток завершена
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 on_map_centered(toggle_centered: bool): is_centered = toggle_centered
func get_marks_count(): return _marks.size()
func _ready() -> void:
var img: = Image.create(1, 2, false, Image.FORMAT_RGBA8)
img.set_pixel(0, 0, Color.CORAL)
img.set_pixel(0, 1, Color(1, 1, 1, 0))
border1 = ImageTexture.create_from_image(img)
img.set_pixel(0, 0, Color(1, 1, 1, 0))
img.set_pixel(0, 1, Color.CORAL)
border2 = ImageTexture.create_from_image(img)
zoom_timer = Timer.new()
add_child(zoom_timer)
zoom_timer.one_shot = false
zoom_timer.wait_time = 0.1
zoom_timer.timeout.connect(_on_zoom_timeout)
_xyz.z = min_zoom_level
image_type = 'png'
$canvas.connect('mouse_entered', func(): _rollover = true)
$canvas.connect('mouse_exited', func(): _rollover = false)
_on_resize()
signaller.connect('map_centered', on_map_centered)
if not Engine.is_editor_hint():
var unit_nav = network.get_unit_instance('навигация')
unit_nav.connect('data_received', on_data_received)
ProjectSettings.connect('settings_changed', _settings_changed)
threats.connect('threat_new', Callable(self, '_on_threat_new'))
threats.connect('threat_lost', Callable(self, '_on_threat_lost'))
threats.connect('threat_update', Callable(self, '_on_threat_update'))
func _settings_changed():
base_url = ProjectSettings.get_setting('application/config/Шаблон адреса сервера карт', base_url)
func on_data_received(coords: Dictionary) -> void:
if not is_centered:
return
if coords.has_all(['latitude', 'longitude']):
set_coordinates(coords['longitude'], coords['latitude'], coords['k'])
func _on_resize() -> void:
var center: Vector2 = $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, scr_xy: Vector2, mark: Node) -> bool:
if _marks.has(id):
return false
var wrld_pos: Vector2 = screen_to_world(_xyz, scr_xy)
_marks[id] = { 'position': wrld_pos, 'mark': mark, 'gap_size': 0.0 }
add_child(mark)
return true
func delete_mark(id: int) -> bool:
if not _marks.has(id):
return false
_marks[id].mark.queue_free()
_marks.erase(id)
queue_redraw()
return true
## Создаёт новую отметку из долготы и широты
func add_mark_from_lon_lat(id: int, lon: float, lat: float, mark: Node, gap_size: float = 32) -> bool:
if _marks.has(id):
return false
var wrld_pos: Vector2 = lonlat_to_world(lon, lat)
_marks[id] = {'position': wrld_pos, 'mark': mark, 'gap_size': gap_size}
add_child(mark)
queue_redraw()
return true
## Возвращает новую позицию для точки, если она выходит из поля видимости
func fix_mark_position(dot) -> void:
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) -> bool:
var screen_pos = world_to_screen(dot.position)
return screen_pos.x > 0 and screen_pos.x < $canvas.size.x
func _input(e) -> void:
var lmp = get_local_mouse_position()
if not _rollover:
_dragging = false
return
if e is InputEventMouseButton:
if e.pressed:
if e.button_index == MOUSE_BUTTON_WHEEL_DOWN:
if (not is_out_border1) and (not is_out_border2):
increment_zoom(-zoom_step, lmp)
queue_redraw()
elif e.button_index == MOUSE_BUTTON_WHEEL_UP:
increment_zoom( zoom_step, lmp)
queue_redraw()
if e.button_index == MOUSE_BUTTON_LEFT:
_dragging = e.pressed
_drag_pos = screen_to_world(_xyz, lmp)
elif e is InputEventMouseMotion:
if _dragging:
var wp = screen_to_world(_xyz, lmp)
var diff = _drag_pos - wp
_xyz.x += diff.x
if (not is_out_border1) and diff.y >= 0:
_xyz.y += diff.y
if is_out_border1 and diff.y <= 0:
_xyz.y += diff.y
if is_out_border2 and diff.y >= 0:
_xyz.y += diff.y
if (not is_out_border2) and diff.y <= 0:
_xyz.y += diff.y
queue_redraw()
if diff.length() > 0:
signaller.emmit(signaller.map_user_panning)
func multiply_zoom(multiplier: float, pivot: Vector2) -> void:
var p1: = screen_to_world(_xyz, pivot)
_xyz.z = clampf(_xyz.z * multiplier, min_zoom_level, max_zoom_level)
var p2: = screen_to_world(_xyz, pivot)
_xyz.x -= p2.x - p1.x
_xyz.y -= p2.y - p1.y
update_scale_label()
func increment_zoom(increment: float, pivot: Vector2) -> void:
var p1: = screen_to_world(_xyz, pivot)
_xyz.z = clampf(_xyz.z + increment, min_zoom_level, max_zoom_level)
var p2: = screen_to_world(_xyz, pivot)
_xyz.x -= p2.x - p1.x
_xyz.y -= p2.y - p1.y
update_scale_label()
func set_zoom(zoom: float, pivot: Vector2) -> void:
var p1: = screen_to_world(_xyz, pivot)
_xyz.z = clampf(zoom, max_zoom_level, min_zoom_level)
var p2: = screen_to_world(_xyz, pivot)
_xyz.x -= p2.x - p1.x
_xyz.y -= p2.y - p1.y
update_scale_label()
func _draw() -> void:
# Рисование плиток
var z: float = min(max_zoom_level, _xyz.z)
var t1: = screen_to_tile(Vector2(0, 0), z)
var t2: = screen_to_tile($canvas.size, z) + Vector2(1, 1)
is_out_border1 = true
is_out_border2 = true
for tx in range(t1.x, t2.x):
for ty in range(t1.y, t2.y):
_draw_tile(tx, ty, z)
# Рисование отметок
var n: = pow(2, z)
var dx: = n * tile_width
var c: Vector2 = $canvas.size / 2.0
var r: float = c.length()
visible_marks_count = 0
for dot in _marks.values():
var scr_pos: Vector2 = world_to_screen(dot.position)
scr_pos.x = posmod(int(scr_pos.x), int(dx))
dot.mark.position = scr_pos
dot.mark.visible = is_mark_visible(dot)
dot.mark.visible = c.distance_to(scr_pos) < (r - dot.gap_size)
visible_marks_count += int(dot.mark.visible)
# Рисование границ
var p1: Vector2 = $canvas.position
var p2: Vector2 = $canvas.size / Vector2(1, 20)
if is_out_border1 and _dragging:
draw_texture_rect(border1, Rect2(p1, p2), false, Color.WHITE, false)
p1.y = p1.y + $canvas.size.y - p2.y
if is_out_border2 and _dragging:
draw_texture_rect(border2, Rect2(p1, p2), false, Color.WHITE, false)
if should_draw_ship_radius():
draw_ship_radius()
func _clean_all() -> void:
_queue.clear()
_cache.clear()
_req_count = 0
# get the tile from queue with the newest timestamp
func get_next_in_queue() -> Tile:
var tile = null
for idx in _queue:
var t = _queue.get(idx)
if not tile or t.t > tile.t:
tile = t
return tile
func _clean_cache() -> void:
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) -> void:
now += delta
# Если кэш переполнен - очистить его.
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(TILE_REQUEST_NAME % 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) #TODO: For Deugs
if _queue.is_empty():
emit_signal('load_completed')
update_scale_label()
func process_online(result):
# ERROR: res://scenes/tilemap/tilemap.gd:396 - Invalid access to property or key 'online' on a base object of type 'Nil'.
var unit_instance: = network.get_unit_instance('уарэп-карта')
if not unit_instance: return
if result != HTTPRequest.RESULT_SUCCESS:
if unit_instance.online:
unit_instance.online = false
unit_instance.emit_signal('line_changed', unit_instance)
return false
unit_instance.rx_tick = Time.get_ticks_msec()
if not unit_instance.online:
unit_instance.online = true
unit_instance.emit_signal('line_changed', unit_instance)
return true
func _response(result, code, _headers: Array, body: PackedByteArray, req, tile: Tile) -> void:
_req_count -= 1
remove_child(req)
req.queue_free()
if process_online(result):
_queue[tile.i] = tile
else:
return
if code == 404:
push_error('на сервере нет \"%s\"' % tile.url)
return
if Error.OK != image_load_proc.call(body):
push_error('не удалось получить изображение (неверный формат?) из \"%s\"' % tile.url)
return
tile.texture = ImageTexture.create_from_image(image)
_cache[tile.i] = tile
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:
check_borders(p1, p2, $canvas.size.y)
draw_texture_rect_region(subtile.texture, Rect2(p1, p2 - p1), rect)
return true
return false
func check_borders(p1, p2, h):
if p1.y < 0:
is_out_border1 = false
if p1.y + (p2 - p1).y > h:
is_out_border2 = false
func _draw_tile(tx: int, ty: int, z: float) -> void:
var tz: = int(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:
check_borders(p1, p2, $canvas.size.y)
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: float = (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: Vector2 = lonlat_to_world(lon, lat)
return world_to_screen(w)
## convert lon/lat to tile coords
func lonlat_to_tile(lon: float, lat: float, z: float) -> Vector2:
var w: Vector2 = 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: float = wx * 180.0
var lat: float = rad_to_deg(atan(sinh(wy * PI)))
return Vector2(lon, lat)
## convert screen coords to lon/lat
func screen_to_lonlat(scr_xy: Vector2) -> Vector2:
var w: Vector2 = screen_to_world(_xyz, scr_xy)
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: Vector2 = 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, s: Vector2) -> Vector2:
var n: float = pow(2.0, xyz.z)
var span_w: float = n * tile_width
var span_h: float = n * tile_height
var px: float = s.x - $canvas.size.x / 2 + span_w / 2
var py: float = s.y - $canvas.size.y / 2 + span_h / 2
var xr: float = px / span_w
var yr: float = py / span_h
var x: float = (xr * 2.0 - 1.0) + xyz.x
var y: float = ((-yr * 2.0) + 1.0) + xyz.y
return Vector2(x, y)
## convert screen coords to tile coords
func screen_to_tile(scr_xy: Vector2, z: float) -> Vector2:
var world: Vector2 = screen_to_world(_xyz, scr_xy)
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: Vector2 = tile_to_world(tx, ty, tz)
return world_to_screen(w)
## convert tile coords to world coords
func tile_to_world(tx: float, ty: float, tz: float) -> Vector2:
var n: float = pow(2.0, floor(tz))
var x: float = (tx / n) * 2.0 - 1.0
var y: float = -((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: float = pow(2.0, floor(z))
var tx: float = floor(((wx + 1.0) / 2.0) * n)
var ty: float = floor(((-wy + 1.0) / 2.0) * n)
return Vector2(tx, ty)
## convert world coords to screen coords
func world_to_screen(pos: Vector2) -> Vector2:
var n: = pow(2.0, _xyz.z)
var wh: = Vector2(n * tile_width, n * tile_height)
var rxy: = Vector2(((pos.x - _xyz.x) + 1.0) / 2.0, (-(pos.y - _xyz.y) + 1.0) / 2.0)
return wh * rxy + ($canvas.size - wh) / 2.0
## 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) -> Tile:
var n: float = pow(2, z)
if x >= 0:
x %= int(n)
else:
x = -x - 1
x = int(n - 1) - (x % int(n))
if z < 0 or y < 0 or y >= n:
return null
var idx: int = xyz_to_idx(x, y, z)
var tile = null
var req: = get_node_or_null(NodePath(TILE_REQUEST_NAME % idx))
if req:
tile = req.get_meta('tile')
if tile != null:
tile.t = now
return tile
tile = _cache.get(idx)
if tile:
tile.t = now
return tile
tile = _queue.get(idx)
if tile:
tile.t = now
return tile
var queue_size: = _queue.size()
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
if z <= max_zoom_level_server:
_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: String = 'N' if lat >= 0 else 'S'
lat = abs(lat)
var deg: float = floor(lat)
var minute: float = (lat - deg) * 60
var second: float = (minute - floor(minute)) * 60
var text: String = ('%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: float, lon1: float, lat2: float, lon2: float) -> float:
# Радиус Земли в метрах
const 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.0) + pow(cl1 * sl2 - sl1 * cl2 * cdelta, 2.0))
var x: = sl1 * sl2 + cl1 * cl2 * cdelta
var ad: = atan2(y, x)
var dist: = ad * R
return dist
func remove_closest_dot(screen_pos: Vector2) -> bool:
var closest_dot = find_at_screen_position(screen_pos)
if not closest_dot:
return false
remove_child(closest_dot.mark)
closest_dot.mark.queue_free()
_marks.erase(closest_dot)
return true
func find_at_screen_position(screen_pos: Vector2) -> Node:
var closest_dot = null
var closest_distance = INF
for mark in _marks.values():
var dot_screen_pos = world_to_screen(mark.position)
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, course: float) -> void:
var anim: Animation = $player.get_animation('goto')
var new_pos: Vector2 = 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, new_pos.x)
anim.track_insert_key(1, animation_time, new_pos.y)
$player.play('goto')
update_coordinates_label(lon, lat)
if not _marks.has(ship_id):
ship_instance = ship_mark.instantiate()
add_mark_from_lon_lat(ship_id, lon, lat, ship_instance, 0.0)
else:
_marks[ship_id].position = lonlat_to_world(lon, lat)
ship_instance.rotation_degrees = fmod(course, 360.0)
queue_redraw()
func blend_mask(val: bool) -> void:
$canvas/spr_mask.visible = val
queue_redraw()
func update_coordinates_label(lon: float, lat: float) -> void:
coordinates_label = 'Координаты: NS:%f, EW:%f' % [lat, lon]
signaller.emit_signal('update_coordinates_map', coordinates_label)
func update_scale_label() -> void:
scale_label = 'Масштаб: ' + str(round(_xyz.z))
signaller.emit_signal('update_scale_map', scale_label)
func _on_threat_new(th) -> void:
var bip_instance = threat_mark.instantiate()
if bip_instance:
bip_instance.name = str(th.id)
bip_instance.position = lonlat_to_world(th.lon, th.lat)
add_mark_from_lon_lat(th.id, th.lon, th.lat, bip_instance, 32)
func _on_threat_update(th) -> void:
var target = get_parent().get_node(str(th.id))
if not target:
return
var new_position = lonlat_to_world(th.lon, th.lat)
if _marks.has(th.id):
_marks[th.id].position = new_position
if new_position != target.position:
target.position = new_position
func _on_threat_lost(th) -> void:
var target = get_parent().get_node(str(th.id))
if target:
target.queue_free()
func zoom_in() -> void:
if _xyz.z >= max_zoom_level:
return
var center: Vector2 = $canvas.size / 2.0
increment_zoom(zoom_step, center)
queue_redraw()
func zoom_out() -> void:
if _xyz.z <= min_zoom_level:
return
var center: Vector2 = $canvas.size / 2.0
increment_zoom(-zoom_step, center)
queue_redraw()
func _on_zoom_plus_button_down() -> void:
zoom_action = "in"
perform_zoom()
zoom_timer.start(0.3)
func _on_zoom_minus_button_down() -> void:
zoom_action = "out"
perform_zoom()
zoom_timer.start(0.3)
func _on_zoom_button_up() -> void:
zoom_action = ""
zoom_timer.stop()
func _on_zoom_timeout():
perform_zoom()
zoom_timer.wait_time = 0.1
func perform_zoom():
if zoom_action != "":
if zoom_action == "in":
zoom_in()
else:
zoom_out()
update_scale_label()
# Вычисление для радиуса в мировых координатах
func get_ship_radius_world() -> float:
if not _marks.has(ship_id):
return 0.0
var ship_world_pos = _marks[ship_id].position
var ship_lonlat = world_to_lonlat(ship_world_pos.x, ship_world_pos.y)
var earth_radius_km = 6371.0
var distance_deg = (ship_radius_km / earth_radius_km) * (180.0 / PI) / cos(deg_to_rad(ship_lonlat.y))
var east_lon = ship_lonlat.x + distance_deg
var east_point_world = lonlat_to_world(east_lon, ship_lonlat.y)
return abs(east_point_world.x - ship_world_pos.x)
func should_draw_ship_radius() -> bool:
if not show_ship_radius or not _marks.has(ship_id):
return false
return _xyz.z >= 11
func draw_ship_radius() -> void:
var ship_world_pos = _marks[ship_id].position
var ship_screen_pos = world_to_screen(ship_world_pos)
var radius_world = get_ship_radius_world()
var radius_point_world = Vector2(ship_world_pos.x + radius_world, ship_world_pos.y)
var radius_point_screen = world_to_screen(radius_point_world)
var radius_screen = ship_screen_pos.distance_to(radius_point_screen)
#draw_arc(ship_screen_pos, radius_screen, 0, TAU, 64, Color.ORANGE, 2.0) # отрисовка контура
draw_circle(ship_screen_pos, radius_screen, Color.ORANGE_RED * Color(1, 1, 1, 0.1)) # отрисовка заливки