From 94206beb47dd3fa60b3cb4a4ed88e98b688d5974 Mon Sep 17 00:00:00 2001 From: sasha80 Date: Thu, 2 Nov 2023 10:42:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B5.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=80=D1=82=D0=B0=D0=BC=D0=B8=20=D0=9C=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=B0.=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9?= =?UTF-8?q?=20=D1=88=D0=B5=D0=B9=D0=B4=D0=B5=D1=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- project.godot | 3 + scenes/tilemap.gd | 62 ++++ scenes/tilemap.tscn | 15 + scenes/настройки.gd | 14 +- scenes/настройки.tscn | 17 + scenes/работа.gd | 1 + scenes/работа.tscn | 20 +- scripts/mercator.gd | 664 ++++++++++++++++++++++++++++++++++++++ scripts/modbus.gd | 11 + scripts/repnetwork.gd | 5 +- shaders/tilemap.gdshader | 13 + shaders/tools.gdshaderinc | 6 + table/table.gd | 3 +- 13 files changed, 818 insertions(+), 16 deletions(-) create mode 100644 scenes/tilemap.gd create mode 100644 scenes/tilemap.tscn create mode 100644 scripts/mercator.gd create mode 100644 scripts/modbus.gd create mode 100644 shaders/tilemap.gdshader create mode 100644 shaders/tools.gdshaderinc diff --git a/project.godot b/project.godot index 338e4eb..5f21f7d 100644 --- a/project.godot +++ b/project.godot @@ -19,6 +19,8 @@ boot_splash/show_image=false boot_splash/fullsize=false boot_splash/use_filter=false config/icon="res://data/icon.png" +config/map_addr="https://a.tile.openstreetmap.org" +config/map_bcs=Vector3(0.5, 1.8, 0.3) [autoload] @@ -36,6 +38,7 @@ yemsboards="*res://scripts/yemsboards.gd" yemsconsts="*res://scripts/yems-consts.gd" interfer="*res://scripts/interfer.gd" hotkeys="*res://scripts/hotkeys.gd" +mercantile="*res://scripts/mercator.gd" [debug] diff --git a/scenes/tilemap.gd b/scenes/tilemap.gd new file mode 100644 index 0000000..03f36bd --- /dev/null +++ b/scenes/tilemap.gd @@ -0,0 +1,62 @@ +extends Sprite2D + +## Состояния КА карты +enum TILE_STATE { + IDLE, ## Покой + WAIT, ## Ожидание + DONE, ## Выполнено + ERROR ## Ошибка +} + +var curent_map_tile: Vector3i ## Текущая запрошенная плитка +var curent_state: TILE_STATE = TILE_STATE.IDLE ## Текущее состояние КА карты +var image: Image ## Изображение с картой +var map_bcs: Vector3 ## Яркость, контраст насыщенность + + +func _ready(): + # Create an HTTP request node and connect its completion signal. + var tile = mercantile.Tile.new() + print_debug('Плитка: %s %s' % [tile, tile.is_valid()]) + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(self._http_request_completed) + curent_map_tile = Vector3i(0, 0, 0) + + image = Image.create(512, 512, true, Image.FORMAT_RGB8) + map_bcs = ProjectSettings.get_setting('application/config/map_bcs', Vector3(1.0, 1.0, 1.0)) + + var error = http_request.request('https://a.tile.openstreetmap.org/0/0/0.png') + if error == OK: + curent_state = TILE_STATE.WAIT + else: + push_error('Ошибка: Не возможно сформировать запрос HTTP') + curent_state = TILE_STATE.ERROR + + +func _process(delta: float): + pass + + +func _http_request_completed(result, response_code, headers, data): + if result != HTTPRequest.RESULT_SUCCESS: + push_error('Ошибка: запрос не обработан: %s' % result) + curent_state = TILE_STATE.ERROR + return + + var image0 = Image.new() + var error = image0.load_png_from_buffer(data) + if error != OK: + push_error('Ошибка: Полученые данные невозможно использовать для формироания изображения') + curent_state = TILE_STATE.ERROR + return + + curent_state = TILE_STATE.DONE + var tile_size = image0.get_size() + + image.blit_rect(image0, Rect2i(0, 0, tile_size.x, tile_size.y), Vector2i( 0, 0)) + image.blit_rect(image0, Rect2i(0, 0, tile_size.x, tile_size.y), Vector2i(tile_size.x, 0)) + image.blit_rect(image0, Rect2i(0, 0, tile_size.x, tile_size.y), Vector2i( 0, tile_size.y)) + image.blit_rect(image0, Rect2i(0, 0, tile_size.x, tile_size.y), Vector2i(tile_size.y, tile_size.y)) + image.adjust_bcs(map_bcs.x, map_bcs.y, map_bcs.z) + texture = ImageTexture.create_from_image(image) diff --git a/scenes/tilemap.tscn b/scenes/tilemap.tscn new file mode 100644 index 0000000..7dda1a1 --- /dev/null +++ b/scenes/tilemap.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=4 format=3 uid="uid://bbq6nklh36yij"] + +[ext_resource type="Script" path="res://scenes/tilemap.gd" id="1_mkfgj"] +[ext_resource type="Shader" path="res://shaders/tilemap.gdshader" id="1_tsdui"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_pmqvj"] +resource_local_to_scene = true +shader = ExtResource("1_tsdui") +shader_parameter/center = Vector2(0.5, 0.5) +shader_parameter/radius = 0.4 + +[node name="tilemap" type="Sprite2D"] +material = SubResource("ShaderMaterial_pmqvj") +position = Vector2(560, 559) +script = ExtResource("1_mkfgj") diff --git a/scenes/настройки.gd b/scenes/настройки.gd index 7a2fbeb..446ab49 100644 --- a/scenes/настройки.gd +++ b/scenes/настройки.gd @@ -3,13 +3,7 @@ extends Panel # Called when the node enters the scene tree for the first time. func _ready() -> void: - if OS.get_cmdline_args().has('++ограниченный'): - $Label2.text = 'Текущий режим доступа: ограниченный' - for node in $GridContainer.get_children(): - if node is Button: - node.disabled = true - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - pass + if not OS.get_cmdline_args().has('++ограниченный'): return + $Label2.text = 'Текущий режим доступа: ограниченный' + for node in $GridContainer.get_children().filter(func(b): return b is Button): + node.disabled = true diff --git a/scenes/настройки.tscn b/scenes/настройки.tscn index 281d9a1..bb9f105 100644 --- a/scenes/настройки.tscn +++ b/scenes/настройки.tscn @@ -238,3 +238,20 @@ offset_top = 27.0 offset_right = 445.0 offset_bottom = 49.0 text = "Текущий режим доступа: полный" + +[node name="edt_mapaddr" type="LineEdit" parent="."] +layout_mode = 0 +offset_left = 536.0 +offset_top = 60.0 +offset_right = 908.0 +offset_bottom = 87.0 +text = "https://a.tile.openstreetmap.org" + +[node name="Label" type="Label" parent="edt_mapaddr"] +layout_mode = 0 +offset_top = -30.0 +offset_right = 372.0 +offset_bottom = -8.0 +text = "Адрес сервера карт" +horizontal_alignment = 1 +vertical_alignment = 1 diff --git a/scenes/работа.gd b/scenes/работа.gd index 24a0cad..b534f3a 100644 --- a/scenes/работа.gd +++ b/scenes/работа.gd @@ -52,6 +52,7 @@ func on_threats_resized(threats: Dictionary): $count_all_pad/count_all.text = '% ## Производит инициализацию func _ready(): + $tilemap.position = $canvas.get_strob_center() $btn_strob.button_connect('pressed', Callable(self, 'on_button_strobe_pressed')) $btn_view.button_connect('toggled', Callable(self, 'on_button_view_toggled')) $grd_btns/btn_off.set_pressed(true) diff --git a/scenes/работа.tscn b/scenes/работа.tscn index 4530f07..6a9f4fa 100644 --- a/scenes/работа.tscn +++ b/scenes/работа.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=10 format=3 uid="uid://b276iygic5itk"] +[gd_scene load_steps=13 format=3 uid="uid://b276iygic5itk"] [ext_resource type="Script" path="res://scenes/работа.gd" id="1_vmjjl"] [ext_resource type="PackedScene" uid="uid://nl1vklubr5kr" path="res://scenes/bip.tscn" id="2_ew6j4"] @@ -9,6 +9,14 @@ [ext_resource type="PackedScene" uid="uid://b5kjdyxuwsot5" path="res://scenes/canvas.tscn" id="7_pvt8f"] [ext_resource type="PackedScene" uid="uid://da7w3vkhadfwe" path="res://scenes/button-flat.tscn" id="8_r2k1p"] [ext_resource type="PackedScene" uid="uid://0uqi1ghf7sma" path="res://scenes/btn_scale.tscn" id="8_xddop"] +[ext_resource type="PackedScene" uid="uid://bbq6nklh36yij" path="res://scenes/tilemap.tscn" id="10_wpi4s"] +[ext_resource type="Shader" path="res://shaders/tilemap.gdshader" id="11_7ohqv"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_x7kiv"] +resource_local_to_scene = true +shader = ExtResource("11_7ohqv") +shader_parameter/center = Vector2(0.5, 0.5) +shader_parameter/radius = 0.4 [node name="Работа" type="Panel"] script = ExtResource("1_vmjjl") @@ -18,10 +26,10 @@ col_red = Color(1, 0.568627, 0.431373, 0.239216) drag_scale_band = 0.05 drag_scale_width = 0.5 freq_high = 6000.0 -freq_low = 200.0 +freq_low = 400.0 radius_center = Vector2(560, 560) radius_internal = 200.0 -radius_outter = 550.0 +radius_outter = 400.0 strob_max_band = 15.538 strob_max_width = 90.0 strob_min_band = 0.1 @@ -35,6 +43,7 @@ offset_left = 1114.0 offset_top = 7.0 offset_right = 1594.0 offset_bottom = 1170.0 +metadata/_edit_lock_ = true [node name="canvas" parent="." instance=ExtResource("7_pvt8f")] metadata/_edit_lock_ = true @@ -45,6 +54,7 @@ offset_left = 10.0 offset_top = 1116.0 offset_right = 1106.0 offset_bottom = 1170.0 +metadata/_edit_lock_ = true [node name="count_all_pad" parent="." instance=ExtResource("6_l7wyl")] layout_mode = 0 @@ -123,6 +133,10 @@ offset_top = 1038.0 offset_right = 1104.0 offset_bottom = 1056.0 +[node name="tilemap" parent="." instance=ExtResource("10_wpi4s")] +material = SubResource("ShaderMaterial_x7kiv") +position = Vector2(548, 552) + [connection signal="drag_begin" from="." to="." method="_on_drag_begin"] [connection signal="drag_continue" from="." to="." method="_on_drag_continue"] [connection signal="pressed" from="btn_scale" to="." method="_on_btn_scale_pressed"] diff --git a/scripts/mercator.gd b/scripts/mercator.gd new file mode 100644 index 0000000..163a6b5 --- /dev/null +++ b/scripts/mercator.gd @@ -0,0 +1,664 @@ +extends Node +## Web mercator XYZ tile utilities + +const R2D: = 180.0 / PI +const RE: = 6378137.0 +const CE: = 2.0 * PI * RE +const EPSILON: = 1e-14 +const LL_EPSILON: = 1e-11 +const MAX_ZOOM: = 28 + + +## An XYZ web mercator tile +## x, y, z : int +## x and y indexes of the tile and zoom level z. +class Tile: + var x: int + var y: int + var z: int + + func as_array(): return [self.x, self.y, self.z] + + func _to_string() -> String: return '[%d, %d, %d]' % [self.x, self.y, self.z] + + func _init(x0: int = 0, y0: int = 0, z0: int = 0): + self.x = x0 + self.y = y0 + self.z = z0 + + func is_valid() -> bool: + if (self.z >= 0) and (self.z < MAX_ZOOM): + var mz: int = 2 ** self.z + return (self.x >= 0) and (self.x < mz) \ + and (self.y >= 0) and (self.y < mz) + return false + + +## Пара долготы и широты. +class LngLat: + var lng: float = 0.0 ## Долгота в десятичных градусах восточной долготы. + var lat: float = 0.0 ## Широта в десятичных градусах северной широты. + func _init(lng0, lat0): + self.lng = lng0 + self.lat = lat0 + + +## Географическое ограничивающее поле. +## запад, юг, восток, север : float +## Значения границ в десятичных градусах. +class LngLatBbox: + var west: float + var south: float + var east: float + var north: float + func as_array(): return [self.west, self.south, self.east, self.north] + func _init(w: float = 0.0, s: float = 0.0, e: float = 0.0, n: float = 0.0): + self.west = w + self.south = s + self.east = e + self.north = n + + +## Граничная область веб-меркатора. +## Атрибуты: left, bottom, right, top : float. +## Значения границ в метрах. +class Bbox: + var left: float + var bottom: float + var right: float + var top: float + func as_array(): return [self.left, self.bottom, self.right, self.top] + func _init(l: float = 0.0, b: float = 0.0, r: float = 0.0, t: float = 0.0): + self.left = l + self.bottom = b + self.right = r + self.top = t + + +func degrees(v: float): return (v * 180.0) / PI + + +func radians(v: float): return (v * PI) / 180.0 + + +func rshift(val: int, n: int): return (val % 0x100000000) >> n + + +## Возвращает плитки и занимаемый ими размер области в пикселях: +## ширина, высота, левая граница, верхняя граница, плитки +## [param west] [float] Западная граница области +## [param east] [float] Восточная граница области +## [param south] [float] Южная граница области +## [param north] [float] Северная граница области +## [param size] [float] Размер плитки в пикселях +## [param zoom] [float] Детализация +func get_tiles(west, south, east, north, zoom, size): + var tiles = [create_tiles(west, south, east, north, zoom)] + var mm = [] + for t in tiles: mm.append(t.x) + var min_x = mm.min() + var max_x = mm.max() + + mm.clear() + for t in tiles: mm.append(t.y) + var min_y = mm.min() + var max_y = mm.max() + + var w = size * (max_x - min_x + 1) + var h = size * (max_y - min_y + 1) + return [w, h, min_x, min_y, tiles, max_x - min_x + 1] + + +## Возвращает размер области в метрах +## [param west] [float] - Западная граница области +## [param param] [float] - Восточная граница области +## [param south] [float] - Южная граница области +## [param north] [float] - Северная граница области +## [return] [Array] +func get_size_from_degrees(west, south, east, north) -> Array[float]: + var xy0: = xy(west, north, true) + var xy1: = xy(east, south, true) + return [xy0[0] - xy1[0], xy0[1] - xy1[1]] + + +## Возвращает границы области в градусах +## [param m] [float] - Размер карты в метрах. +## [param lng] [float] - Долгота центра области в градусах (-180...180). +## [param lat] [float] - Широта центра области в градусах (-90...90). +## [return] [float, float, float, float] +func get_degrees_bounds(m: float, lng: float, lat: float): + m /= 2.0 + var x_y = xy(lng, lat, true) + var ws = lnglat(x_y[0] - m, x_y[1] - m) + var en = lnglat(x_y[0] + m, x_y[1] + m) + return [ws[0], ws[1], en[0], en[1]] + + +func get_tiles_bounds(tiles): + var mm = [] + for t in tiles: mm.append(bounds(t).west) + var mmw = mm.min(); mm.clear() + + for t in tiles: mm.append(bounds(t).south) + var mms = mm.min(); mm.clear() + + for t in tiles: mm.append(bounds(t).east) + var mme = mm.max(); mm.clear() + + for t in tiles: mm.append(bounds(t).north) + var mmn = mm.max(); mm.clear() + + return [mmw, mms, mme, mmn] + + +## Возвращает левую верхнюю долготу и широту плитки. +## [param tile] Плитка или последовательность int. Может быть либо экземпляр +## Tile, либо 3 ints, X, Y, Z. +## [return] LngLat +## [codeblock] +## >>> ul(Tile(x=0, y=0, z=1)) +## LngLat(lng=-180.0, lat=85.0511287798066) +## >>> mercantile.ul(1, 1, 1) +## LngLat(lng=0.0, lat=0.0) +## [/codeblock] +func ul(tile: Tile, zoom: int): + var Z2 = pow(2, zoom) + var lon_deg = tile.x / Z2 * 360.0 - 180.0 + var lat_rad = atan(sinh(PI * (1 - 2 * tile.y / Z2))) + var lat_deg = degrees(lat_rad) + return LngLat.new(lon_deg, lat_deg) + + +## Возвращает левую верхнюю долготу и широту плитки. +## [param tile] [Плитка] Может быть либо экземпляром Tile, либо 3 интами, X, Y, Z. +## [return] [LngLat] +## [codeblock] +## >>> ul(Tile(x=0, y=0, z=1)) +## LngLat(lng=-180.0, lat=85.0511287798066) +## >>> mercantile.ul(1, 1, 1) +## LngLat(lng=0.0, lat=0.0) +## [/codeblock] +func ul2(tile: Tile) -> Array[float]: + var z2 = pow(2, tile.z) + var lon_deg = tile.x / z2 * 360.0 - 180.0 + var lat_rad = atan(sinh(PI * (1 - 2 * tile.y / z2))) + var lat_deg = degrees(lat_rad) + return [lat_deg, lon_deg] + + +## Возвращает граничную область плитки. +## [param tile] [Tile] или [Array]. Может быть либо экземпляром [Tile], либо 3 +## целыми числами (X, Y, Z). +## [return] [LngLatBbox] +func bounds(tile: Tile) -> LngLatBbox: + var Z2: = pow(2, tile.z) + + var ul_lon_deg = tile.x / Z2 * 360.0 - 180.0 + var ul_lat_rad = atan(sinh(PI * (1 - 2 * tile.y / Z2))) + var ul_lat_deg = degrees(ul_lat_rad) + + var lr_lon_deg = (tile.x + 1) / Z2 * 360.0 - 180.0 + var lr_lat_rad = atan(sinh(PI * (1 - 2 * (tile.y + 1) / Z2))) + var lr_lat_deg = degrees(lr_lat_rad) + + return LngLatBbox.new(ul_lon_deg, lr_lat_deg, lr_lon_deg, ul_lat_deg) + + +func truncate_lnglat(lng: float, lat: float) -> Array: + if lng > 180.0: lng = 180.0 + elif lng < -180.0: lng = -180.0 + if lat > 90.0: lat = 90.0 + elif lat < -90.0: lat = -90.0 + return [lng, lat] + + +## Преобразование долготы и широты в веб-меркатор x, y. +## [param lng] [param lat]: float +## Долгота и широта в десятичных градусах [param truncate] [bool] -Нужно ли +## усекать или обрезать входные данные до пределов веб-меркатора. +## [return] [x: float, y: float]. [param y] будет [b]inf[/b] на северном полюсе. +## ([param lat] >= 90) и [b]-inf[/b] на южном полюсе ([param lat] <= -90). +func xy(lng: float, lat: float, truncate: bool = false) -> Array[float]: + var ll = truncate_lnglat(lng, lat) if truncate else [0.0, 0.0] + var x: float = RE * radians(lng) + var y: float = 0.0 + if ll[1] <= -90.0: y = float('-inf') + elif ll[1] >= 90.0: y = float('inf') + else: y = RE * log(tan((PI * 0.25) + (0.5 * radians(lat)))) + return [x, y] + + +## Преобразование координат веб-меркатора x, y в долготу и широту. +## [param x] [float], [param y] [float] - Координаты веб-меркатора в метрах. +## [param truncate] [bool] - Нужно ли усекать или обрезать входные данные до границ веб-меркатора. +## [return] [LngLat] +func lnglat(x, y, truncate=false): + var lng = x * R2D / RE + var lat = ((PI * 0.5) - 2.0 * atan(exp(-y / RE))) * R2D + var ll = truncate_lnglat(lng, lat) if truncate else [0.0, 0.0] + return LngLat.new(ll[0], ll[1]) + + +## Соседи плитки. [func neighbors] возвращает до восьми +## соседних плиток, при этом плитки будут опущены, если они не являются действительными, например. +## Tile(-1, -1, z). [param tile]: Плитка или последовательность int +## Может быть либо экземпляр Tile, либо 3 ints, X, Y, Z. +## [return] Массив +## [codeblock] +## >>> neighbors(Tile(486, 332, 10)) +## [Tile(x=485, y=331, z=10), Tile(x=485, y=332, z=10), Tile(x=485, y=333, z=10), Tile(x=486, y=331, z=10), Tile(x=486, y=333, z=10), Tile(x=487, y=331, z=10), Tile(x=487, y=332, z=10), Tile(x=487, y=333, z=10)] +## [/codeblock] +func neighbors(tile: Tile) -> Array[Tile]: + var tiles: = Array() + var lo_hi = [0, 2 ** tile.z - 1] + for i in [-1, 0, 1]: + for j in [-1, 0, 1]: + if i == 0 and j == 0: continue + elif tile.x + i < 0 or tile.y + j < 0: continue + elif tile.x + i > lo_hi[1] or tile.y + j > lo_hi[i]: continue + var t = Tile.new(tile.x + i, tile.y + j, tile.z) + if t.is_valid(): + tiles.append(t) + return tiles + + +## Получение меркаторской граничной рамки веб-плитки +## [param tile] - Плитка или последовательность int. Может быть либо экземпляр +## [Tile], либо 3 ints, X, Y, Z. +## [return] [Bbox] +## Примечания. Эпсилон вычитается из правой границы и прибавляется к нижней. +func xy_bounds(tile: Tile, zoom: int) -> Bbox: + var tile_size = CE / pow(2, zoom) + var left = tile.x * tile_size - CE / 2 + var right = left + tile_size + var top = CE / 2 - tile.y * tile_size + var bottom = top - tile_size + return Bbox.new(left, bottom, right, top) + + +func _xy(lng, lat, truncate=false) -> Array: + var ll: = [lng, lat] if not truncate else truncate_lnglat(lng, lat) + var x = ll[0] / 360.0 + 0.5 + var sinlat: = sin(radians(ll[1])) + var y: = 0.5 - 0.25 * log((1.0 + sinlat) / (1.0 - sinlat)) / PI + return [x, y] + + +## Получение плитки, содержащей долготу и широту +## [param lng], [param lat] [float] - Пара долготы и широты в десятичных градусах. +## [param zoom] [int] - Уровень масштабирования веб-меркатора. +## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора. +## [return] [Tile] +func create_tile(lng: float, lat: float, zoom: int, truncate: bool=false) -> Tile: + var xy0: = _xy(lng, lat, truncate) + var Z2 = pow(2, zoom) + + # Чтобы устранить потерю точности при переходе от одной плитки к другой + # и lng/lat, точки в пределах EPSILON от правой стороны тайла + # считаются в следующей за ней плитке. + var tile: = Tile.new() + if xy0[0] <= 0: tile.x = 0 + elif xy0[0] >= 1: tile.x = int(Z2 - 1) + else: tile.x = int(floor((xy0[0] + EPSILON) * Z2)) + + if xy0[1] <= 0: tile.y = 0 + elif xy0[1] >= 1: tile.y = int(Z2 - 1) + else: tile.y = int(floor((xy0[1] + EPSILON) * Z2)) + tile.z = zoom + return tile + + +## Получение четверного ключа плитки +## [param tile] [Tile] или последовательность [int]. Может быть либо экземпляр +## [Tile] или 3 целых, X, Y, Z. +## [return] [String] +func quadkey(tile: Tile, zoom) -> String: + var rs: = String() + for z in range(zoom, 0, -1): + var digit = 0 + var mask = 1 << (z - 1) + if tile.x & mask: + digit += 1 + if tile.y & mask: + digit += 2 + rs += str(digit) + return rs + + +## Получение плитки, соответствующей четверному ключу. +## [param qk] [String] - Строка четверного ключа. +## [return] [Tile] +func quadkey_to_tile(qk: Array): + var tile: = Tile.new() + if len(qk) == 0: return tile + var i = 0 + qk.reverse() + for digit in qk: + var mask = 1 << i + i += 1 + if digit == '1': + tile.x |= mask + elif digit == '2': + tile.y |= mask + elif digit == '3': + tile.x |= mask + tile.y |= mask + elif digit == '0': + continue + else: + push_error('Не ожидаемый четырёхзначный знак: \"%s\"' % digit) + tile.z = i + return tile + + +## Получение плиток, перекрываемых географической рамкой +## [param west], [param south], [param east], [param north]: последовательность [float] +## - Значения границ в десятичных градусах. +## [param zooms] [int] или [Array[int]]. - Один или несколько уровней масштабирования. +## [param truncate] [bool] - Нужно ли усекать вводимые данные до границ веб-меркатора. +## [return] Tile +## Примечания. В параметрах south и east используется небольшой эпсилон, чтобы +## функция выдавала ровно одну плитку при задании границ этой же плитки. +func create_tiles(west: float, south: float, east: float, north: float, zooms, truncate: bool=false) -> Array[Tile]: + var west_south = [west, south] if truncate else truncate_lnglat(west, south) + var east_north = [east, north] if truncate else truncate_lnglat(east, north) + var bboxes = [[west, south, east, north]] if west <= east else [[-180.0, south, east, north], [west, south, 180.0, north]] + var tiles = [] + if not is_instance_of(zooms, TYPE_ARRAY): + zooms = [zooms] + for box in bboxes: + var w = box[0] + var s = box[1] + var e = box[2] + var n = box[3] + # Зажим граничных значений. + w = max(-180.0, w) + s = max(-85.051129, s) + e = min(180.0, e) + n = min(85.051129, n) + for z in zooms: + var ul_tile = create_tile(w, n, z) + var lr_tile = create_tile(e - LL_EPSILON, s + LL_EPSILON, z) + for i in range(ul_tile.x, lr_tile.x + 1): + for j in range(ul_tile.y, lr_tile.y + 1): + tiles.append(Tile.new(i, j, z)) + return tiles + + +## Получение родителя плитки. Родительская плитка - это плитка на один уровень масштабирования ниже, которая +## содержит данную "дочернюю" плитку. +## [param tile] [Tile] или последовательность int - Может быть либо экземпляром Tile. +## или 3 ints, X, Y, Z. +## [param zoom] [int] - Определяет уровень *масштабирования* возвращаемой родительской плитки. +## По умолчанию это значение на один ниже, чем у плитки (непосредственного родителя). +## [return] [Tile] +## [codeblock] +## >>> parent(Tile(0, 0, 2)) +## Tile(x=0, y=0, z=1) +## >>> parent(Tile(0, 0, 2), zoom=0) +## Tile(x=0, y=0, z=0) +## [/codeblock] +func parent(tile: Tile, zoom=null): + if tile.z == 0: return null + + assert((zoom != null) and (tile.z > zoom or zoom == int(zoom)), 'Масштаб (zoom) должен быть целым числом и меньше масштаба входной плитки (tile).') + assert(tile.x == int(tile.x) and tile.y == int(tile.y) and tile.z == int(tile.z), 'Родитель нецелочисленной плитки (tile) является неуточнённым') + + var target_zoom = tile.z - 1 if zoom == null else zoom + + # Алгоритм, в значительной степени вдохновленный https://github.com/mapbox/tilebelt + var return_tile = Tile.new(tile.x, tile.y, tile.z) + while return_tile.z > target_zoom: + tile.x = return_tile.x + tile.y = return_tile.y + tile.z = return_tile.z + if tile.x % 2 == 0 and tile.y % 2 == 0: + return_tile = Tile.new(tile.x / 2, tile.y / 2, tile.z - 1) + elif tile.x % 2 == 0: + return_tile = Tile.new(tile.x / 2, (tile.y - 1) / 2, tile.z - 1) + elif not tile.x % 2 == 0 and tile.y % 2 == 0: + return_tile = Tile.new((tile.x - 1) / 2, tile.y / 2, tile.z - 1) + else: + return_tile = Tile.new((tile.x - 1) / 2, (tile.y - 1) / 2, tile.z - 1) + return return_tile + + +## Получение дочерних элементов плитки. Дочерние элементы упорядочены: +## верхний-левый, верхний-правый, bottom-right, bottom-left. +## [tile] [Tile] или последовательность int. Может быть либо экземпляром Tile, +## либо 3 интами, X, Y, Z. +## [param zoom] [int] Возвращает все дочерние элементы с масштабом *zoom*, в порядке убывания глубины. +## в порядке намотки по часовой стрелке. Если не указано, возвращаются ближайшие (т.е. zoom + 1) +## дочерние элементы плитки. +## [return] [Array] +## [codeblock] +## >>> children(Tile(0, 0, 0)) +## [Tile(x=0, y=0, z=1), Tile(x=0, y=1, z=1), Tile(x=1, y=0, z=1), Tile(x=1, y=1, z=1)] +## >>> children(Tile(0, 0, 0), zoom=2) +## [Tile(x=0, y=0, z=2), Tile(x=0, y=1, z=2), Tile(x=0, y=2, z=2), Tile(x=0, y=3, z=2), ...] +## [/codeblock] +func children(tile, zoom) -> Array: + assert((tile.z <= zoom) and zoom == int(zoom), 'Масштаб (zoom) должен быть целым числом и превышать масштаб входной плитки (tile)') + var target_zoom = zoom if zoom != null else tile.z + 1 + var tiles: Array[Tile] = [tile] + while tiles[0].z >= target_zoom: + tile = tiles.pop_at(0) + tiles += [ + Tile.new(tile.x * 2, tile.y * 2, tile.z + 1), + Tile.new(tile.x * 2 + 1, tile.y * 2, tile.z + 1), + Tile.new(tile.x * 2 + 1, tile.y * 2 + 1, tile.z + 1), + Tile.new(tile.x * 2, tile.y * 2 + 1, tile.z + 1) ] + return tiles + + +## Проверяется, есть ли в [param merge_set] 4 плитки, которые могут быть объединены. +## Если есть, то происходит их слияние. Возвращает список плиток, а также булево +## число, указывающее, были ли они объединены. При многократном применяя +## [method merge], можно упростить набор плиток. +func merge(merge_set): + var upwards_merge: = {} + for tile in merge_set.keys(): + var tile_parent = parent(tile) + if not upwards_merge.has(tile_parent): + upwards_merge[tile_parent] = {} + upwards_merge[tile_parent].update({tile: false}) + var current_tileset: = [] + var changed: = false + for supertile in upwards_merge.keys(): + var tile_children = upwards_merge[supertile] + if len(tile_children) == 4: + current_tileset += [supertile] + changed = true + else: + current_tileset += [tile_children] + return [current_tileset, changed] + + +## Максимально уменьшает размер набора плиток путем слияния листьев с родителями. +## [param tiles] - Последовательность плиток для слияния. +## [return] [Array] +func simplify(tiles: Array): + # Проверьте, существует ли уже плитка и ее родитель. + # Убедитесь, что плитки отсортированы по масштабу, поэтому родительские + # плитки встречаются первыми. Если да, отбросьте дочернюю плитку + # (она покрыта родительской). + var root_set = {} + tiles.sort_custom(func(a, b): return a[2] < b[2]) + for tile in tiles: + var is_new_tile: bool = true + var supertiles = [] + for i in tile.z: + supertiles.append(parent(tile, i)) + for supertile in supertiles: + if supertile in root_set: + is_new_tile = false + continue + if is_new_tile: + root_set.update({tile: false}) + + # Повторять слияние до тех пор, пока дальнейшее упрощение перестанет быть невозможным. + var is_merging: bool = true + while is_merging: + var root_set_is_merging = merge(root_set) + root_set = root_set_is_merging[0] + is_merging = root_set_is_merging[1] + return root_set + + +## Получение наименьшей плитки, содержащей географическую границу. +## Если bbox охватывает линии lng 0 или lat 0, ограничивающая плитка будет иметь вид +## Tile(x=0, y=0, z=0). +## [param bbox] последовательность [float] значений границ запада, юга, востока, +## севера в десятичных градусах. +## [return] [Tile] +func bounding_tile(bbox, truncate: bool = false): + if len(bbox) == 2: + bbox += bbox + + var w = bbox[0] + var s = bbox[1] + var e = bbox[2] + var n = bbox[3] + + if truncate: + var w_s = truncate_lnglat(w, s) + w = w_s[0] + s = w_s[1] + var e_n = truncate_lnglat(e, n) + n = e_n[0] + e = e_n[1] + + e -= LL_EPSILON + s += LL_EPSILON + + var tmin = create_tile(w, n, 32) + var tmax = create_tile(e, s, 32) + assert(tmin.is_valid()) + assert(tmax.is_valid()) + + var cell = [tmin.x, tmin.y, tmax.x, tmax.y] + var z = _getBboxZoom(cell) + + if z == 0: + return Tile.new() + + var x = rshift(cell[0], 32 - z) + var y = rshift(cell[1], 32 - z) + + return Tile.new(x, y, z) + + +func _getBboxZoom(bbox: Array): + for z in MAX_ZOOM: + var mask: int = 1 << (32 - (z + 1)) + if (bbox[0] & mask) != (bbox[2] & mask) or (bbox[1] & mask) != (bbox[3] & mask): + return z + return MAX_ZOOM + + +## Получение GeoJSON-функции, соответствующей плитке +## [param tile] [Плитка] или последовательность int - Может быть либо экземпляром [Плитка], либо 3 int, X, Y, Z. +## [Tile] или 3 целых, X, Y, Z. +## [param fid] [String] - Идентификатор объекта. +## [param props] [Dictionary] - Необязательные дополнительные свойства признака. +## [param projected] [String] - Можно создать нестандартный веб-меркатор GeoJSON. +## передав 'mercator'. +## [param buffer] [float] - Необязательное буферное расстояние для полигона GeoJSON. +## [param precision] [int] - Координаты GeoJSON будут усечены до данного +## количества знаков после запятой. +## [return] [Dictionary] +func feature(tile: Tile, fid=null, props=null, projected='geographic', buffer=null): + var west_south_east_north = bounds(tile).as_array() + var west = west_south_east_north[0] + var south = west_south_east_north[1] + var east = west_south_east_north[2] + var north = west_south_east_north[3] + if projected == 'mercator': + var west_south = xy(west, south, false) + west = west_south[0] + south = west_south[1] + var east_north = xy(east, north, false) + east = east_north[0] + north = east_north[1] + + if buffer: + west -= buffer + south -= buffer + east += buffer + north += buffer + + west = round(west) + south = round(south) + east = round(east) + north = round(north) + + var bbox: = [min(west, east), min(south, north), max(west, east), max(south, north)] + var geom: = {'type': 'Polygon', 'coordinates': [[[west, south], [west, north], [east, north], [east, south], [west, south]]] } + var xyz: = str(tile) + var feat: = { + 'type': 'Feature', + 'bbox': bbox, + 'id': xyz, + 'geometry': geom, + 'properties': {'title': 'XYZ tile %s' % xyz} } + + if props: + feat['properties'].update(props) + + if fid != null: + feat['id'] = fid + + return feat + + +func isinstance(obj, types: Array): + for type in types: + if is_instance_of(obj, type): + return true + return false + + +## Все кортежи координат из геометрии, объекта или коллекции +## [param lng] [float] - Долгота +## [param lat] [float] - широта +## [return] [Array] +func _coords(obj) -> Array: + var coordinates = [] + if is_instance_of(obj, TYPE_ARRAY): + coordinates = obj + elif 'features' in obj: + for feat in obj['features']: + coordinates = feat['geometry']['coordinates'] + elif 'geometry' in obj: + coordinates = obj['geometry']['coordinates'] + else: + coordinates = obj.get('coordinates', obj) + + var v: = [] + for e in coordinates: + if isinstance(e, [TYPE_FLOAT, TYPE_INT]): + v.append(coordinates) + break + else: + for f in _coords(e): + v.append([f[0], f[1]]) + return v + + +func func1(bbox: Array, coords: Array): + var w = bbox[0] + var s = bbox[1] + var e = bbox[2] + var n = bbox[3] + var lng = coords[0] + var lat = coords[1] + return [min(w, lng), min(s, lat), max(e, lng), max(n, lat)] + + +## Возвращает граничную область объекта GeoJSON. +## [param obj] - Отображение, геометрия GeoJSON, объект или коллекция объектов. +## [returns] [LngLatBbox] +func geojson_bounds(obj): + var coords: Array = _coords(obj) + var wsen = coords.reduce(func1, [180.0, 90.0, -180.0, -90.0]) + return LngLatBbox.new(wsen[0], wsen[1], wsen[2], wsen[3]) diff --git a/scripts/modbus.gd b/scripts/modbus.gd new file mode 100644 index 0000000..89731fa --- /dev/null +++ b/scripts/modbus.gd @@ -0,0 +1,11 @@ +extends Node + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + pass diff --git a/scripts/repnetwork.gd b/scripts/repnetwork.gd index 3e49396..52e6413 100644 --- a/scripts/repnetwork.gd +++ b/scripts/repnetwork.gd @@ -46,8 +46,9 @@ func _ready() -> void: var unit = PROTOCOL_TYPES[proto].new(unit_name) var unit_key = repsettings.get_unit_key(unit_name) units[unit_key] = unit - unit.connect('line_changed', Callable(logger_page, 'on_line_changed').bind(unit_key)) - unit.connect('command_fail', Callable(logger_page, 'on_command_fail').bind(unit_key)) + if logger_page: + unit.connect('line_changed', Callable(logger_page, 'on_line_changed').bind(unit_key)) + unit.connect('command_fail', Callable(logger_page, 'on_command_fail').bind(unit_key)) for key in units: log.info('%s %s:%d' % [units[key].name, key[0], key[1]]) diff --git a/shaders/tilemap.gdshader b/shaders/tilemap.gdshader new file mode 100644 index 0000000..f1403dc --- /dev/null +++ b/shaders/tilemap.gdshader @@ -0,0 +1,13 @@ +shader_type canvas_item; + +#include "tools.gdshaderinc" + +uniform vec2 center = vec2(0.5, 0.5); +uniform float radius: hint_range(0.0, 1.0) = 0.4; /* Радиус */ + + +void fragment() +{ + COLOR = texture(TEXTURE, UV) * smooth_px(length(UV - center), radius, DS); +} + diff --git a/shaders/tools.gdshaderinc b/shaders/tools.gdshaderinc new file mode 100644 index 0000000..bb0b6d3 --- /dev/null +++ b/shaders/tools.gdshaderinc @@ -0,0 +1,6 @@ +const float DS = 0.004; /* Относительное расстояние на котором выполняется сглаживание краёв */ + +float smooth_px(float r, float R, float ds) +{ + return 1.0 - smoothstep(R - ds, R + ds, r); +} diff --git a/table/table.gd b/table/table.gd index 1705343..b00ad44 100644 --- a/table/table.gd +++ b/table/table.gd @@ -45,7 +45,8 @@ func set_node_user_data(i_col: int, i_row: int, val) -> void: set_node_meta(i_co func defaultselector_color_proc(_i_row: int): return selector_color -## Закрепляет заголовок таблицы. [param scrl] - Прокрутка родитель таблицы. +## Закрепляет заголовок таблицы. +## [param scrl] - Прокрутка родитель таблицы. ## [param prnt] - Родитель прокрутки. func pin_header(scrl: ScrollContainer, prnt: Control): scrl.remove_child(header)