Доработка. Улучшено отображение карт
This commit is contained in:
@@ -1,173 +1,670 @@
|
||||
class_name main_tilemap extends Sprite2D
|
||||
@tool
|
||||
extends CanvasGroup
|
||||
|
||||
## Возможные состояния конечного автомата заданий.
|
||||
enum TASK_STATE {
|
||||
DONE, ## Выполнено.
|
||||
WAIT, ## Ожидание плитки.
|
||||
RUN, ## Запуск выполнения.
|
||||
CHECK, ## Проверка пустого места.
|
||||
ERROR ## Ошибка.
|
||||
}
|
||||
## Отображение карт 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
|
||||
|
||||
|
||||
## Координаты для проверок [lng, lat].
|
||||
const TEST_DATA: = {
|
||||
'Калиниград': [ 20.51095, 54.70649 ],
|
||||
'Москва': [ 37.6156, 55.7522 ],
|
||||
'Нью-Йорк': [ -74.006, 40.7143 ],
|
||||
'Сидней': [ 151.20732, -33.8678500 ],
|
||||
'Таганрог': [ 38.8969, 47.2362 ],
|
||||
'Хельсинки': [ 24.9354, 60.1695 ],
|
||||
'Азорские о-ва': [ 28.3996582, 38.5397568 ],
|
||||
'Мехико': [ -99.12766, 19.42847 ],
|
||||
'Белое море': [ 68.136606, 42.045546 ],
|
||||
'море Беллинсгаузена': [ -72.406587, -74.857997 ],
|
||||
'Фиджи': [ 178.065032, -17.713371 ],
|
||||
'Ноль': [ 0.0, 0.0 ],
|
||||
'Точка1': [ 170.0, 0.0 ],
|
||||
'Точка2': [-170.0, 0.0 ],
|
||||
'Море' : [ 19.4, 54.91 ] }
|
||||
#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
|
||||
|
||||
|
||||
const MAP_MODES: = {
|
||||
0: 'application/config/map_image_size_rto',
|
||||
1: 'application/config/map_image_size_rls' }
|
||||
@export var size: Vector2i = Vector2i(400, 400):
|
||||
set(v):
|
||||
$canvas.size = v
|
||||
_on_resize()
|
||||
queue_redraw()
|
||||
get:
|
||||
return $canvas.size
|
||||
|
||||
|
||||
const MAP_RELATIVE_RADIUSES: = {
|
||||
0: 0.39,
|
||||
1: 0.466
|
||||
}
|
||||
@export var ax: float = 0.0:
|
||||
set(v):
|
||||
_xyz.x = v
|
||||
queue_redraw()
|
||||
get:
|
||||
return _xyz.x
|
||||
|
||||
|
||||
var map_image: Image ## Изображение с картой.
|
||||
var map_tile_state: TASK_STATE ## Текущее состояние конечного автомата плитки.
|
||||
var map_bcs: Vector3 ## Яркость, контраст насыщенность.
|
||||
var map_server_addr: String ## Адрес сервера изображений карт в проекции Меркатора.
|
||||
var map_tile_format: String ##
|
||||
var map_tile_stack: Array ## Стек для хранения плиток карты.
|
||||
var map_request: HTTPRequest ## Обработчик запросов http.
|
||||
var map_zoom: int ## Уровень детализации карты.
|
||||
var map_tile: Rect2i ## Область одной плитки: [x-координата области, y-координата области, x-индекс плитки, y-индекс плитки].
|
||||
var map_mode: int ## Режим карты: 0 - приоритет РТО, 1 - приоритет РЛС.
|
||||
var prev_tiles_dic: Dictionary ##
|
||||
var lnglat: Array ## Текущие координаты.
|
||||
var root_rect: Rect2i
|
||||
@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():
|
||||
lnglat = TEST_DATA['Таганрог']
|
||||
map_tile_state = TASK_STATE.DONE
|
||||
map_request = HTTPRequest.new()
|
||||
add_child(map_request)
|
||||
map_request.request_completed.connect(self._map_request_completed)
|
||||
map_bcs = ProjectSettings.get_setting('application/config/map_bcs', Vector3(1.0, 1.0, 1.0))
|
||||
map_server_addr = ProjectSettings.get_setting('application/config/map_server_addr')
|
||||
map_tile_format = ProjectSettings.get_setting('application/config/map_tile_format', '%s/%d/%d/%d.png')
|
||||
var map_image_size = ProjectSettings.get_setting(MAP_MODES[0], Vector2i(512, 512))
|
||||
var map_tile_size = ProjectSettings.get_setting('application/config/map_tile_size', 256)
|
||||
map_image = Image.create(map_image_size.x, map_image_size.y, true, Image.FORMAT_RGB8)
|
||||
map_tile = Rect2i(0, 0, map_tile_size, map_tile_size)
|
||||
signaller.connect('zoom_changed', Callable(self, '_on_zoom_changed'))
|
||||
signaller.connect('map_mode_changed', Callable(self, '_on_map_mode_changed'))
|
||||
log.info('Адрес сервера поставщика карт: \"%s\"' % map_server_addr)
|
||||
log.info('Модуль географической карты готов')
|
||||
$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_map_mode_changed(mode: int) -> void:
|
||||
var map_image_size = ProjectSettings.get_setting(MAP_MODES[mode])
|
||||
material.set('shader_parameter/radius', MAP_RELATIVE_RADIUSES[mode])
|
||||
map_image.fill(Color(0, 0, 0, 0))
|
||||
map_image.resize(map_image_size.x, map_image_size.y)
|
||||
map_tile_state = TASK_STATE.DONE
|
||||
map_mode = mode
|
||||
var tile: = mercator.create_tile(lnglat[0], lnglat[1], map_zoom)
|
||||
map_tile_stack.clear()
|
||||
map_tile_stack.append(tile)
|
||||
prev_tiles_dic.clear()
|
||||
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 mark_image_center(image: Image, color: Color = Color.RED):
|
||||
var c = image.get_size() / 2
|
||||
map_image.set_pixel(c.x - 1, c.y - 1, color)
|
||||
map_image.set_pixel(c.x - 1, c.y, color)
|
||||
map_image.set_pixel(c.x, c.y - 1, color)
|
||||
map_image.set_pixel(c.x, c.y, color)
|
||||
## Создаёт новую отметку из координат экрана
|
||||
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 _on_zoom_changed(zoom: int):
|
||||
if map_tile_stack.size() != 0:
|
||||
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
|
||||
map_zoom = zoom
|
||||
var tile: = mercator.create_tile(lnglat[0], lnglat[1], zoom)
|
||||
map_tile_stack.clear()
|
||||
map_tile_stack.append(tile)
|
||||
prev_tiles_dic.clear()
|
||||
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 img_tile_to_map_image(image: Image, dst_pos: Vector2i):
|
||||
image.adjust_bcs(map_bcs.x, map_bcs.y, map_bcs.z)
|
||||
map_image.blit_rect(image, map_tile, dst_pos)
|
||||
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 _process(_delta: float):
|
||||
if (map_tile_state == TASK_STATE.DONE) and map_tile_stack.size():
|
||||
var tile: mercator.Tile = map_tile_stack[-1]
|
||||
var tile_addr: = map_tile_format % [map_server_addr, tile.z, tile.x , tile.y]
|
||||
var rc = map_request.request(tile_addr)
|
||||
if rc == OK:
|
||||
map_tile_state = TASK_STATE.WAIT
|
||||
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:
|
||||
log.error('%s: Невозможно сформировать запрос HTTP' % self)
|
||||
map_tile_state = TASK_STATE.ERROR
|
||||
elif map_tile_state == TASK_STATE.CHECK:
|
||||
var root_tile: = mercator.Tile.new(root_rect.size.x, root_rect.size.y, map_zoom)
|
||||
var neighbors_tiles = mercator.get_perimetr_tiles(root_tile, (map_image.get_size().x / map_tile.size.x) / 2)
|
||||
var neighbors_tiles_dic = {}
|
||||
for tile in neighbors_tiles:
|
||||
neighbors_tiles_dic[[tile.x, tile.y]] = false
|
||||
if neighbors_tiles_dic != prev_tiles_dic:
|
||||
for tile in neighbors_tiles:
|
||||
tile.is_root = false
|
||||
map_tile_stack.push_front(tile)
|
||||
prev_tiles_dic[[tile.x, tile.y]] = false
|
||||
else:
|
||||
if map_tile_stack.size() == 0:
|
||||
signaller.emit_signal('zoom_complete', map_zoom)
|
||||
map_tile_state = TASK_STATE.DONE
|
||||
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 _map_request_completed(result, response_code, headers, data):
|
||||
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:
|
||||
log.error('%s: Запрос не обработан. result: \"%s\", response_code: \"%s\", headers: \"%s\"' % [self, result, response_code, headers])
|
||||
map_tile_state = TASK_STATE.ERROR
|
||||
if map_server_available:
|
||||
map_server_available = false
|
||||
emit_signal('map_server_offline')
|
||||
_queue[tile.i] = tile
|
||||
return
|
||||
|
||||
var img_tile = Image.new()
|
||||
var error = img_tile.load_png_from_buffer(data)
|
||||
if error != OK:
|
||||
log.error('%s: Полученые данные невозможно использовать для формироания изображения. result: \"%s\", response_code: \"%s\", headers: \"%s\"' % [self, result, response_code, headers])
|
||||
map_tile_state = TASK_STATE.ERROR
|
||||
if not map_server_available:
|
||||
map_server_available = true
|
||||
emit_signal('map_server_online')
|
||||
|
||||
if code == 404:
|
||||
push_error('ошибка: на сервере нет \"%s\"' % tile.url)
|
||||
return
|
||||
|
||||
var tile = map_tile_stack.pop_back()
|
||||
map_tile_state = TASK_STATE.CHECK
|
||||
if Error.OK != image_load_proc.call(body):
|
||||
push_error('ошибка: не удалось получить изображение (неверный формат?) из \"%s\"' % tile.url)
|
||||
return
|
||||
|
||||
if tile.is_root:
|
||||
# Отображение корневой плитки
|
||||
map_image.fill(Color(0, 0, 0, 0))
|
||||
var bounds: = mercator.bounds(tile)
|
||||
var lng_c = (bounds.east + bounds.west) / 2.0
|
||||
var lat_c = (bounds.north + bounds.south) / 2.0
|
||||
var sx: float = float(map_tile.size.x) / (bounds.east - bounds.west) * (lng_c - tile.lng) + map_tile.size.x / 2.0 # Cмещение в пискселях центра плитки от заданной точки по горизонтали.
|
||||
var sy: float = float(map_tile.size.y) / (bounds.north - bounds.south) * (lat_c - tile.lat) - map_tile.size.y / 2.0 # Cмещение в пискселях центра плитки от заданной точки по вертикали.
|
||||
var dst_pos: Vector2i = Vector2i(int(sx), int(-sy)) + map_tile.size * map_mode
|
||||
img_tile_to_map_image(img_tile, dst_pos)
|
||||
root_rect = Rect2i(dst_pos.x, dst_pos.y, tile.x, tile.y)
|
||||
# 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:
|
||||
# Отображение соседних плиток
|
||||
var delta: = Vector2i(tile.x - root_rect.size.x, tile.y - root_rect.size.y)
|
||||
var dst_pos: Vector2i = root_rect.position + delta * map_tile.size
|
||||
img_tile_to_map_image(img_tile, dst_pos)
|
||||
mark_image_center(map_image)
|
||||
texture = ImageTexture.create_from_image(map_image)
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user