839 lines
25 KiB
GDScript
839 lines
25 KiB
GDScript
@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)) # отрисовка заливки
|