From 7b83d0d2619bf31f9eeeec1a45755af482b4e8a1 Mon Sep 17 00:00:00 2001
From: Maryshca <kuzmichova.maria@ya.ru>
Date: Fri, 11 Oct 2024 17:25:06 +0300
Subject: [PATCH] major update

---
 README.md     |   2 +-
 app.py        |   2 +-
 callbacks.py  | 268 +++++++++++++++++++++++++++++++++++++-------------
 config.py     |   2 +-
 constants.py  |  27 ++++-
 dynamic_ui.py | 119 ++++++++++++++++++++++
 generator.py  |   4 +-
 handlers.py   |  53 +---------
 map_edit.py   | 175 ++++++++++++++++++++++++++++++++
 remote_run.py | 143 +++++++++++++++++++--------
 static_ui.py  | 101 ++++++++++++++++---
 utils.py      | 136 ++++++++++++-------------
 12 files changed, 784 insertions(+), 248 deletions(-)
 create mode 100644 map_edit.py

diff --git a/README.md b/README.md
index 08342a5..9df03c3 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ## Как запустить?
 
-`pip install dearpygui numpy paramiko scp python-dotenv`
+`pip install dearpygui numpy paramiko scp python-dotenv tifffile`
 
 `git clone http://tesla.parallel.ru/kuzmichovamary/gui.git`
 
diff --git a/app.py b/app.py
index 5c4fc7c..6efa891 100644
--- a/app.py
+++ b/app.py
@@ -28,4 +28,4 @@ if __name__ == '__main__':
     
     dpg.set_primary_window('main', True)
     dpg.start_dearpygui()
-    dpg.destroy_context()
\ No newline at end of file
+    dpg.destroy_context()
diff --git a/callbacks.py b/callbacks.py
index 34417fc..ad52205 100644
--- a/callbacks.py
+++ b/callbacks.py
@@ -1,58 +1,93 @@
 import dearpygui.dearpygui as dpg
+import tifffile
 
 from utils import *
-from remote_run import start_remote_execution
+from constants import SPACE, TREE_COLOR, ROAD_COLOR, BUILDING_COLOR
+from remote_run import start_remote_execution, start_model_build, get_server_output_structure
+from map_edit import dump_height_map, load_height_map, update_texture
 from styles import get_accent_color, apply_theme
 from generator import LCZ
 from config import load_config, dump_config
-from dynamic_ui import construct_config_ui
+from dynamic_ui import construct_config_ui, construct_model_output_structure_ui
 
 def map_window_on_close_clb(s, a, u):
-    dpg.set_item_user_data('map_window', {'height_map': None, 'height_map_file_path': None, 'drawing': False, 'prev_coords': None, 'current_action': None})
+    delete_item_and_clear_alias(f'map_buildings_texture')
+    delete_item_and_clear_alias(f'map_trees_texture')
+    delete_item_and_clear_alias(f'map_roads_texture')
+    delete_item_children_and_clear_aliases(f'map_drawlist')
+    user_data = {
+        'buildings_map': None,
+        'trees_map': None,
+        'roads_map': None,
+        'height_map_file_path': None,
+        'drawing': False,
+        'prev_coords': None,
+        'current_action': None,
+        'max_height': 0,
+        'output_dirs': None,
+    }
+    dpg.set_item_user_data('map_window', user_data)
+
+def set_file_path_clb(s, a, u):
+    file_path = a['file_path_name']
+    dpg.set_value(u, file_path)
 
 def construct_config_ui_clb(s, a, u):
     if not get_config_file_path():
         return
-    delete_item_children_and_clear_aliases()
+    delete_config_items_and_clear_aliases()
     update_config_values()
     construct_config_ui(get_config_file_path(), get_config())
 
-def map_open_clb(s, a, u):
-    height_map_file_path = a['file_path_name']
-    height_map = load_height_map(height_map_file_path)
-
-    set_map_window_value('height_map', height_map)
-    set_map_window_value('height_map_file_path', height_map_file_path)
-
-    update_texture(height_map)
-    dpg.show_item('map_window')
 
-def generate_map_clb(s, a, u):
-    height_map = LCZ(config_path=f'configs/{s}.json').to_height_map()
+################################################################################
+#
+#       Config
+#
+################################################################################
 
-    set_map_window_value('height_map', height_map)
-    set_map_window_value('height_map_file_path', 'lcz.txt')
 
-    update_texture(height_map)
-    dpg.show_item('map_window')
+def update_config_values():
+    config = get_config()
+    for path in config:
+        if dpg.does_item_exist(f'{get_config_file_path()}.{path}'):
+            config[path].value = dpg.get_value(f'{get_config_file_path()}.{path}')
+    set_main_window_value('config', config)
 
-def map_save_clb(s, a, u):
-    dump_map(get_height_map(), get_height_map_file_path())
+def config_save_as_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), a['file_path_name'])
     dpg.show_item('message_popup')
 
-def map_save_as_clb(s, a, u):
-    dump_map(get_height_map(), a['file_path_name'])
+def config_open_clb(s, a, u):
+    delete_config_items_and_clear_aliases()
+    config_file_path = a['file_path_name']
+    config = load_config(config_file_path)
+    set_main_window_value('config', config)
+    set_main_window_value('config_file_path', config_file_path)
+    construct_config_ui(a['file_path_name'], config)
+
+def config_save_clb(s, a, u):
+    update_config_values()
+    dump_config(get_config(), get_config_file_path())
     dpg.show_item('message_popup')
 
-def open_map_save_as_dialog_clb(s, a, u):
-    dpg.show_item('map_save_as')
+def open_config_open_dialog_clb(s, a, u):
+    dpg.show_item('config_open')
 
-def open_map_open_dialog_clb(s, a, u):
-    dpg.show_item('map_open')
+def show_config_search_window_clb(s, a, u):
+    dpg.show_item('search_popup')
 
-def open_config_open_dialog_clb(s, a, u):
+def open_config_save_as_dialog_clb(s, a, u):
     dpg.show_item('config_open')
 
+################################################################################
+#
+#       Config Search
+#
+################################################################################
+
+
 def search_clb(s, data, u):
     if not get_config_file_path():
         return
@@ -70,59 +105,160 @@ def search_clb(s, data, u):
                     dpg.set_value(dpg.get_item_parent(dpg.get_item_parent(tag)), dpg.get_item_parent(tag))
             change_color_temporarily('.'.join([get_config_file_path(), path, 'text']), color=get_accent_color())
 
-def remote_run_clb(s, a, u):
-    if not get_config_file_path():
-        show_status_text('no config.txt specified. cannot run.')
-        return
 
-    update_config_values()
-    dump_config(get_config(), 'tmp.txt')
+################################################################################
+#
+#       Map edit
+#
+################################################################################
 
-    dpg.show_item('login_modal')
+def construct_map_layers(w, h):
+    dpg.set_item_width('map_child_window', w)
+    dpg.set_item_height('map_child_window', h)
 
-def apply_theme_clb(s, a, u):
-    apply_theme(s)
+    # dpg.add_texture_registry(tag='textures')
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag=f'map_buildings_texture', parent='textures')
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag=f'map_trees_texture', parent='textures')
+    dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag=f'map_roads_texture', parent='textures')
 
-def set_file_path_clb(s, a, u):
-    file_path = a['file_path_name']
-    dpg.set_value(u, file_path)
+    dpg.bind_item_theme('map_child_window', 'map_image_theme')
+    dpg.add_drawlist(w, h, tag=f'map_drawlist', parent='map_child_window', pos=[0, 0])
 
-def show_config_search_window_clb(s, a, u):
-    dpg.show_item('search_popup')
+    dpg.draw_image(f'map_buildings_texture', color=BUILDING_COLOR, tag=f'map_buildings_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+    dpg.draw_image(f'map_trees_texture', color=TREE_COLOR, tag=f'map_trees_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
+    dpg.draw_image(f'map_roads_texture', color=ROAD_COLOR, tag=f'map_roads_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist')
 
-def start_remote_execution_clb(s, a, u):
-    start_remote_execution(dpg.get_value('server_username'), dpg.get_value('server_password'), dpg.get_value('gitlab_username'), dpg.get_value('gitlab_password'))
+def map_open_clb(s, a, u):
+    height_map_file_path = a['file_path_name']
 
-def update_config_values():
-    config = get_config()
-    for path in config:
-        if dpg.does_item_exist(f'{get_config_file_path()}.{path}'):
-            config[path].value = dpg.get_value(f'{get_config_file_path()}.{path}')
-    set_main_window_value('config', config)
+    if height_map_file_path.endswith('txt'):
+        height_map = load_height_map(height_map_file_path)
+        max_height = np.max(height_map)
+        height_map /= max_height
+    else:
+        height_map = tifffile.imread(height_map_file_path)
+        max_height = 1
 
-def config_save_as_clb(s, a, u):
-    update_config_values()
-    dump_config(get_config(), a['file_path_name'])
-    dpg.show_item('message_popup')
+    h, w = height_map.shape
 
-def config_open_clb(s, a, u):
-    delete_item_children_and_clear_aliases()
-    config_file_path = a['file_path_name']
-    config = load_config(config_file_path)
-    set_main_window_value('config', config)
-    set_main_window_value('config_file_path', config_file_path)
-    construct_config_ui(a['file_path_name'], config)
+    print(dpg.get_item_user_data('map_window'))
 
-def config_save_clb(s, a, u):
-    update_config_values()
-    dump_config(get_config(), get_config_file_path())
+    set_map_window_value('buildings_map', height_map)
+    set_map_window_value('roads_map', np.zeros((h, w)))
+    set_map_window_value('trees_map', np.zeros((h, w)))
+    set_map_window_value('height_map_file_path', height_map_file_path)
+    set_map_window_value('max_height', max_height)
+
+    construct_map_layers(w, h)
+    
+    update_texture()
+
+    dpg.show_item('map_window')
+
+def generate_map_clb(s, a, u):
+    height_map = LCZ(config_path=f'configs/{s}.json').to_height_map(dtype=np.float64)
+
+    max_height = np.max(height_map)
+    height_map /= max_height
+
+    h, w = height_map.shape
+
+    set_map_window_value('buildings_map', height_map)
+    set_map_window_value('roads_map', np.zeros((h, w)))
+    set_map_window_value('trees_map', np.zeros((h, w)))
+    set_map_window_value('height_map_file_path', f'{dpg.generate_uuid()}.txt')
+    set_map_window_value('max_height', max_height)
+
+    construct_map_layers(w, h)
+
+    update_texture()
+
+    dpg.show_item('map_window')
+
+def save_map(file_path):
+    if file_path.endswith('.tif') or file_path.endswith('.tiff'):
+        with TiffWriter(file_path) as tif:
+            tif.write(get_map_window_value('buildings_map'))
+            tif.write(get_map_window_value('roads_map'))
+            tif.write(get_map_window_value('trees_map'))
+    else:
+        dump_height_map(get_map_window_value('buildings_map'), file_path)
+
+def map_save_clb(s, a, u):
+    file_path = get_height_map_file_path()
+    save_map(file_path)
+    dpg.show_item('message_popup')
+
+def map_save_as_clb(s, a, u):
+    file_path = a['file_path_name']
+    save_map(file_path)
     dpg.show_item('message_popup')
 
+def open_map_save_as_dialog_clb(s, a, u):
+    dpg.show_item('map_save_as')
+
+def open_map_open_dialog_clb(s, a, u):
+    dpg.show_item('map_open')
+
 def set_action_clb(s, a, u):
     set_map_window_value('current_action', s)
 
 def set_action_none_clb(s, a, u):
     set_map_window_value('current_action', None)
 
-def open_config_save_as_dialog_clb(s, a, u):
-    dpg.show_item('config_open')
\ No newline at end of file
+
+################################################################################
+#
+#       Styles
+#
+################################################################################
+
+
+def apply_theme_clb(s, a, u):
+    apply_theme(s)
+
+
+################################################################################
+#
+#       Remote execution
+#
+################################################################################
+
+
+def start_remote_execution_clb(s, a, u):
+    dpg.configure_item('login_modal', show=False)
+    u = dpg.get_item_user_data('login_modal')
+    if u == 'build_model':
+        start_model_build(dpg.get_value('server_username'), dpg.get_value('server_password'), dpg.get_value('gitlab_username'), dpg.get_value('gitlab_password'))
+    elif u == 'run_on_lab_server':
+        start_remote_execution(dpg.get_value('server_username'), dpg.get_value('server_password'))
+    elif u == 'download_output':
+        s = get_server_output_structure(dpg.get_value('server_username'), dpg.get_value('server_password'))
+        folders, filepaths = parse_dirs(s)
+        delete_item_children_and_clear_aliases('output')
+        construct_model_output_structure_ui(filepaths, folders)
+        dpg.show_item('model_output_window')
+
+def build_model_on_lab_server_clb(s, a, u):
+    pass
+
+def show_get_credentials_modal_clb(s, a, u):
+    dpg.set_item_user_data('login_modal', u)
+
+    if u == 'run_on_lab_server':
+        if not get_config_file_path():
+            show_status_text('no config.txt specified. cannot run.')
+            return
+
+        update_config_values()
+    
+        dump_config(get_config(), 'tmp.txt')
+
+        dpg.hide_item('gitlab_password') 
+        dpg.hide_item('gitlab_username')
+
+    elif u == 'download_output':
+        dpg.hide_item('gitlab_password') 
+        dpg.hide_item('gitlab_username')
+
+    dpg.show_item('login_modal')
\ No newline at end of file
diff --git a/config.py b/config.py
index e910835..0096a2b 100644
--- a/config.py
+++ b/config.py
@@ -12,7 +12,7 @@ class CfgVar:
         self.comment = comment
 
     def __repr__(self):
-        return f"CfgVar(key={self.key}, value={self.value}, comment={self.comment})"
+        return f"CfgVar(key='{self.key}', value={self.value}, value_type='{self.value_type}', comment='{self.comment}')"
 
 
 TokenType = Enum('TokenType', ['BRACE_OPEN', 'BRACE_CLOSE', 'VARIABLE', 'NAMESPACE'])
diff --git a/constants.py b/constants.py
index dbeadab..ba44caf 100644
--- a/constants.py
+++ b/constants.py
@@ -1,11 +1,32 @@
 import os
 
+from config import CfgVar
+
+ROAD_COLOR = (221, 161, 94, 255)
+TREE_COLOR = (96, 108, 56, 255)
+BUILDING_COLOR = (188, 108, 37, 255)
+EMISSION_POINT_COLOR = (255, 0, 0, 255)
+
 TABS_UI = 'tabs_ui'
 COLLAPSING_HEADERS_UI = 'collapsing_headers_ui'
 WIDGET_WIDTH = 300
 XOFFSET = 300
 LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build']
 
+MAX_MAP_WIDTH = 2000
+MAX_MAP_HEIGHT = 2000
+
+EMISSION_POINT_CONFIG_VARIABLES = {
+    'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''),
+	'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'),
+	'xpos': CfgVar(key='xpos', value=200.0, value_type='DOUBLE', comment='[m]'),
+	'ypos': CfgVar(key='ypos', value=200.0, value_type='DOUBLE', comment='[m]'),
+	'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'),
+	'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'),
+	'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
+	'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
+}
+
 UIS = [TABS_UI, COLLAPSING_HEADERS_UI]
 
 FONT_PATH = os.path.join('fonts', 'Montserrat-Medium.ttf')
@@ -22,7 +43,11 @@ ACCENT = (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11,
 PRIMARY = (2, 3, 39)
 SECONDARY = (36, 33, 21, 24, 4, 7, 8, 9, 13, 14)
 APPLY_ALPHA = (1, 22, 25, 31, 34, 16)
-COLOR_TYPE_TO_DPG_ITEM = {'TEXT': (0, 1), 'PRIMARY': (2, 3, 39), 'SECONDARY': (36, 33, 21, 24, 4, 7, 8, 9, 13, 14), 'ACCENT': (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)}
+COLOR_TYPE_TO_DPG_ITEM = {
+    'TEXT': (0, 1),
+	'PRIMARY': (2, 3, 39),
+	'SECONDARY': (36, 33, 21, 24, 4, 7, 8, 9, 13, 14),
+	'ACCENT': (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50)}
 COLOR_TO_COLOR_TYPE = {0: 'TEXT', 1: 'TEXT', 2: 'PRIMARY', 3: 'PRIMARY', 39: 'PRIMARY', 36: 'SECONDARY', 21: 'SECONDARY', 24: 'SECONDARY', 4: 'SECONDARY', 7: 'SECONDARY', 8: 'SECONDARY', 9: 'SECONDARY', 13: 'SECONDARY', 33: 'SECONDARY', 14: 'SECONDARY', 35: 'ACCENT', 20: 'ACCENT', 37: 'ACCENT', 22: 'ACCENT', 23: 'ACCENT', 5: 'ACCENT', 25: 'ACCENT', 26: 'ACCENT', 27: 'ACCENT', 28: 'ACCENT', 29: 'ACCENT', 10: 'ACCENT', 30: 'ACCENT', 11: 'ACCENT', 31: 'ACCENT', 12: 'ACCENT', 32: 'ACCENT', 49: 'ACCENT', 34: 'ACCENT', 15: 'ACCENT', 50: 'ACCENT', 16: 'ACCENT', 18: 'ACCENT', 19: 'ACCENT', 1: 'TEXT'}
 
 BLUE = {
diff --git a/dynamic_ui.py b/dynamic_ui.py
index 3b6ab2e..1b1df18 100644
--- a/dynamic_ui.py
+++ b/dynamic_ui.py
@@ -3,6 +3,22 @@ import dearpygui.dearpygui as dpg
 from utils import *
 from constants import *
 
+def check_every_child_checkbox(s, a):
+    parent = dpg.get_item_parent(s)
+
+    for child in dpg.get_item_children(parent, 1):
+        print(s, dpg.get_item_alias(child), child, dpg.get_item_type(child))
+        t = dpg.get_item_type(child)
+        if t == 'mvAppItemType::mvGroup':
+            dpg.set_value(dpg.get_item_children(child, 1)[1], a)
+        if t == 'mvAppItemType::mvCollapsingHeader':
+            checkbox = dpg.get_item_children(child, 1)[0]
+            dpg.set_value(checkbox, a)
+            check_every_child_checkbox(checkbox, a)
+
+def folder_checkbox_clb(s, a, u):
+    check_every_child_checkbox(s, a)
+
 def open_folder_dialog_clb(s, a, u):
     dpg.show_item('set_folder_dialog')
     dpg.set_item_user_data('set_folder_dialog', u)
@@ -16,6 +32,109 @@ def construct_config_ui(config_file_path, config):
     else:
         construct_config_ui_collapsing_headers(config_file_path, config)
 
+def add_item_to_config_ui(full_path):
+    config_file_path = get_config_file_path()
+    config = get_config()
+    if get_current_ui_type() == TABS_UI:
+        namespaces = full_path.split('.')
+        parent = config_file_path
+        for level in range(len(namespaces) - 1):
+            path = config_file_path + '.' + '.'.join(namespaces[:level+1])
+
+            tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist(parent)
+
+            if not tab_bar_exists:
+                tab_bar_uuid = dpg.add_tab_bar(parent=parent, callback=change_tab_bar_height_clb)
+                dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 4 * SPACE + FONT_SIZE)
+
+            if not dpg.does_item_exist(path):
+                tab_uuid = dpg.add_tab(label=namespaces[level], parent=tab_bar_uuid)
+                dpg.add_child_window(parent=tab_uuid, tag=path, border=True, autosize_y=False, autosize_x=True)
+                dpg.set_item_user_data(path, 2 * SPACE)
+
+            parent = path
+    else:
+        namespaces = full_path.split('.')
+        parent = config_file_path
+        for level in range(len(namespaces) - 1):
+            path = config_file_path + '.' + '.'.join(namespaces[:level+1])
+
+            if not dpg.does_item_exist(path):
+                dpg.add_collapsing_header(label=namespaces[level], parent=parent, tag=path, indent=INDENT)
+
+            parent = path
+
+
+    if not dpg.does_item_exist(config_file_path + '.' + full_path):
+        create_widget(config_file_path, config, full_path)
+
+def add_emission_point_and_update_ui(x, y):
+    emission_point = {
+        'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''),
+        'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'),
+        'xpos': CfgVar(key='xpos', value=x, value_type='DOUBLE', comment='[m]'),
+        'ypos': CfgVar(key='ypos', value=y, value_type='DOUBLE', comment='[m]'),
+        'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'),
+        'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'),
+        'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
+        'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
+    }
+    prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission'
+
+    config = get_config()
+
+    for key in emission_point:
+        full_path = '.'.join([prefix, key])
+        config[full_path] = emission_point[key]
+        add_item_to_config_ui(full_path)
+
+    set_main_window_value('config', config)
+
+def delete_emission_point_and_update_ui(x, y):
+    emission_point = {
+        'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''),
+        'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'),
+        'xpos': CfgVar(key='xpos', value=x, value_type='DOUBLE', comment='[m]'),
+        'ypos': CfgVar(key='ypos', value=y, value_type='DOUBLE', comment='[m]'),
+        'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'),
+        'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'),
+        'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'),
+        'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]')
+    }
+    prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission'
+
+    config = get_config()
+
+    for key in emission_point:
+        full_path = '.'.join([prefix, key])
+        config[full_path] = emission_point[key]
+        add_item_to_config_ui(full_path)
+
+    set_main_window_value('config', config)
+
+def construct_model_output_structure_ui(filepaths, folders):
+    dpg.add_collapsing_header(label='output', parent='model_output_window', tag='output_header', indent=0, default_open=True)
+    dpg.add_checkbox(label='select all', default_value=False, parent='output_header', tag='output', callback=folder_checkbox_clb)
+
+    for path in folders:
+        folders = path.split('/')
+        parent = 'output_header'
+        for folder in folders:
+            if not dpg.does_item_exist(f'{folder}_header'):
+                with dpg.collapsing_header(label=folder, parent=parent, tag=f'{folder}_header', indent=INDENT):
+                    dpg.add_checkbox(label='select all', default_value=False, tag=folder, callback=folder_checkbox_clb)
+
+            parent = f'{folder}_header'
+
+    for filepath in filepaths:
+        if '/' not in filepath:
+            parent = 'output_header'
+        else:
+            parent = filepath.split('/')[-2] + '_header'
+        with dpg.group(horizontal=True, parent=parent, indent=INDENT, xoffset=XOFFSET):
+            dpg.add_text(filepath.split('/')[-1], tag=f'{filepath}.text')
+            dpg.add_checkbox(default_value=False, tag=filepath)
+
 def construct_config_ui_collapsing_headers(config_file_path, config):
     dpg.add_collapsing_header(label=config_file_path, parent='main', tag=config_file_path, indent=0, default_open=True)
 
diff --git a/generator.py b/generator.py
index df871be..bc10c5b 100644
--- a/generator.py
+++ b/generator.py
@@ -241,8 +241,8 @@ class LCZ:
             file.write(f"{self.height} {self.width}\n")
             file.write("\n".join([" ".join(list(map(str, row))) for row in lcz]))
 
-    def to_height_map(self, building=True, tree=False):
-        lcz = np.zeros((self.height, self.width), dtype=int)
+    def to_height_map(self, dtype=np.int64, building=True, tree=False):
+        lcz = np.zeros((self.height, self.width), dtype=dtype)
         if building:
             self.put_buildings(lcz)
         if tree:
diff --git a/handlers.py b/handlers.py
index f77165e..77be6bd 100644
--- a/handlers.py
+++ b/handlers.py
@@ -2,50 +2,7 @@ import dearpygui.dearpygui as dpg
 
 from utils import *
 from callbacks import *
-
-def mouse_down_callback():
-    if not get_height_map_path():
-        return
-    if not is_drawing():
-        set_map_window_value('prev_coords', dpg.get_mouse_pos())
-        set_map_window_value('drawing', True)
-    else:
-        x, y = dpg.get_mouse_pos()
-        px, py = get_prev_coords()
-        y += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
-        py += dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window')
-        x += dpg.get_x_scroll('map_window')
-        px += dpg.get_x_scroll('map_window')
-        w, h = abs(x - px), abs(y - py)
-        x, y = min(x, px), min(y, py)
-
-        if w > 0 and h > 0 and check_coords(x, y, w, h):
-            dpg.set_item_pos('drawing_frame', [min(x, px), min(y, py)])
-            dpg.set_item_width('drawing_frame', w)
-            dpg.set_item_height('drawing_frame', h)
-            dpg.show_item('drawing_frame')
-
-def mouse_release_callback():
-    if not get_height_map_path():
-        return
-    coords = dpg.get_mouse_pos()
-
-    if get_current_action() == 'erase':
-        draw_rectangle(coords, 0)
-    elif get_current_action() == 'draw_rect':
-        height = dpg.get_value('height_input')
-        draw_rectangle(coords, np.max(get_height_map()) * height)
-
-    update_texture(get_height_map())
-
-    set_map_window_value('drawing', False)
-    dpg.hide_item('drawing_frame')
-
-def draw_rectangle(coords, height):
-    y1, y2, x1, x2 = get_rect(get_prev_coords(), coords)
-    height_map = get_height_map()
-    height_map[y1:y2, x1:x2] = height
-    set_map_window_value('height_map', height_map)
+from map_edit import mouse_down_callback, mouse_release_callback, mouse_click_callback
 
 def on_ctrl_o(sender, app_data):
     if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_O):
@@ -79,11 +36,7 @@ def setup_handlers():
     dpg.bind_item_handler_registry('main', 'main_handler')
     dpg.set_viewport_resize_callback(change_height_)
 
-    with dpg.item_handler_registry(tag='map_handler'):
-        dpg.add_item_resize_handler(callback=set_action_none_clb)
-
-    dpg.bind_item_handler_registry('map_window', 'map_handler')
-
     with dpg.handler_registry():
         dpg.add_mouse_down_handler(callback=mouse_down_callback)
-        dpg.add_mouse_release_handler(callback=mouse_release_callback)
\ No newline at end of file
+        dpg.add_mouse_release_handler(callback=mouse_release_callback)
+        dpg.add_mouse_click_handler(button=1, callback=mouse_click_callback)
\ No newline at end of file
diff --git a/map_edit.py b/map_edit.py
new file mode 100644
index 0000000..4b2a861
--- /dev/null
+++ b/map_edit.py
@@ -0,0 +1,175 @@
+import dearpygui.dearpygui as dpg
+
+from utils import *
+from callbacks import *
+from constants import MAX_MAP_WIDTH, MAX_MAP_HEIGHT, EMISSION_POINT_COLOR
+from dynamic_ui import construct_config_ui, add_emission_point_and_update_ui
+
+def load_height_map(file_path):
+    with open(file_path, "r") as file:
+        lines = file.readlines()[1:]
+        height_map = [list(map(int, line.strip().split())) for line in lines]
+    return np.array(height_map, dtype=np.float64)
+
+def dump_height_map(height_map, file_path):
+    with open(file_path, "w", encoding="utf-8") as file:
+        w, h = height_map.shape
+        height_map *= get_map_window_value('max_height')
+        file.write(f"{w} {h}\n")
+        for row in height_map:
+            file.write(" ".join(list(map(str, map(int, row)))) + "\n")
+
+def update_texture():
+    if get_config_file_path():
+        emission_points = get_emission_points()
+        for n in emission_points:
+            x = int(emission_points[n]['xpos'])
+            y = int(emission_points[n]['ypos'])
+            coords = get_triangle_coords(x, y)
+            dpg.draw_polygon(coords, color=EMISSION_POINT_COLOR, fill=(255, 0, 0, 255), parent=f'{get_height_map_file_path()}map_drawlist', thickness=0)
+
+    texture_map = np.repeat(get_map_window_value(f'{dpg.get_value('layer')}_map'), 4)
+
+    print(texture_map.shape, np.all(texture_map == 0), f'map_{dpg.get_value('layer')}_texture', np.all(get_map_window_value(f'{dpg.get_value('layer')}_map') == 0))
+
+    texture_map[3::4] = (texture_map[::4] != 0)
+
+    dpg.set_value(f'map_{dpg.get_value('layer')}_texture', texture_map)
+
+def mouse_pos_to_height_map_coords(coords):
+    return map(int, coords)
+
+def check_coords(x, y, w, h):
+    return 0 <= x <= x + w <= dpg.get_item_width('map_child_window') and 0 <= y <= y + h <= dpg.get_item_height('map_child_window')
+
+def get_rect(cur_pos, end_pos):
+    x1, y1 = mouse_pos_to_height_map_coords(cur_pos)
+    x2, y2 = mouse_pos_to_height_map_coords(end_pos)
+
+    if y2 < y1:
+        y1, y2 = y2, y1
+
+    if x2 < x1:
+        x1, x2 = x2, x1
+
+    return y1, y2, x1, x2
+
+def mouse_down_callback():
+    if not get_height_map_file_path():
+        return
+    if not dpg.is_item_focused('map_child_window'):
+        return
+    if not is_drawing():
+        set_map_window_value('prev_coords', dpg.get_drawing_mouse_pos())
+        set_map_window_value('drawing', True)
+    else:
+        if get_current_action() == 'erase' or get_current_action() == 'draw_rect':
+            x, y = dpg.get_drawing_mouse_pos()
+            px, py = get_prev_coords()
+            w, h = abs(x - px), abs(y - py)
+            x, y = min(x, px), min(y, py)
+            if w > 0 and h > 0 and check_coords(x, y, w, h):
+                dpg.set_item_pos('drawing_frame', [min(x, px), min(y, py)])
+                dpg.set_item_width('drawing_frame', w)
+                dpg.set_item_height('drawing_frame', h)
+                dpg.show_item('drawing_frame')
+
+def mouse_release_callback():
+    if not get_height_map_file_path():
+        return
+    if not dpg.is_item_focused('map_child_window'):
+        return
+
+    dpg.hide_item('emission_values')
+
+    coords = dpg.get_drawing_mouse_pos()
+
+    if get_current_action() == 'erase':
+        draw_rectangle(coords, 0)
+    elif get_current_action() == 'draw_rect':
+        layer = dpg.get_value('layer')
+        if layer == 'roads':
+            height = 1.
+        else:
+            height = dpg.get_value('height_input')
+        draw_rectangle(coords, height)
+
+    update_texture()
+
+    set_map_window_value('drawing', False)
+
+    dpg.hide_item('drawing_frame')
+
+def mouse_click_callback():
+    if not get_height_map_file_path():
+        return
+    if not get_config_file_path():
+        return
+
+    emission_points = get_emission_points()
+
+    x, y = dpg.get_drawing_mouse_pos()
+
+    is_pos_emission_point, n = is_mouse_pos_emission_point(x, y, emission_points)
+
+    if is_pos_emission_point:
+        dpg.set_item_width('emission_values', 300)
+        dpg.set_item_height('emission_values', 300)
+        dpg.set_item_pos('emission_values', dpg.get_drawing_mouse_pos())
+        dpg.show_item('emission_values')
+        dpg.set_value('emission_settings', f'value: {emission_points[n]['value']}\nbegin: {emission_points[n]['begin']}')
+    else:
+        if 0 <= x < dpg.get_item_width('map_child_window') and 0 <= y < dpg.get_item_height('map_child_window'):
+            add_emission_point_and_update_ui(x, y)
+            update_texture()
+
+def draw_rectangle(coords, height):
+    y1, y2, x1, x2 = get_rect(get_prev_coords(), coords)
+    map_type = f'{dpg.get_value('layer')}_map'
+    height_map = get_map_window_value(map_type)
+    height_map[y1:y2, x1:x2] = height
+    set_map_window_value(map_type, height_map)
+
+def is_point_in_triangle(x1, y1, x2, y2, x3, y3, x, y):
+    denominator = ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3))
+    a = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / denominator
+    b = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / denominator
+    c = 1 - a - b
+    return 0 <= a <= 1 and 0 <= b <= 1 and 0 <= c <= 1
+
+def is_mouse_pos_emission_point(posx, posy, emission_points, side=30):
+    h = (3 ** 0.5 / 2) * side
+    for n in emission_points:
+        x = emission_points[n]['xpos']
+        y = emission_points[n]['ypos']
+        x1, y1 = x, y - (2 / 3) * h
+        x2, y2 = x + side / 2, y + (1 / 3) * h
+        x3, y3 = x - side / 2, y + (1 / 3) * h
+
+        if is_point_in_triangle(x1, y1, x2, y2, x3, y3, posx, posy):
+            return True, n
+    return False, 0
+
+def draw_triangle(array, x, y, side=30, color=1.):
+    
+    h = (3 ** 0.5 / 2) * side
+
+    x1, y1 = x, y - (2 / 3) * h
+    x2, y2 = x + side / 2, y + (1 / 3) * h
+    x3, y3 = x - side / 2, y + (1 / 3) * h
+
+    for i in range(y - side, y + side):
+        for j in range(x - side, x + side):
+            if is_point_in_triangle(x1, y1, x2, y2, x3, y3, j, i):
+                array[i, j] = color
+
+    return array
+
+def get_triangle_coords(x, y, side=30):
+    h = (3 ** 0.5 / 2) * side
+
+    x1, y1 = x, y - (2 / 3) * h
+    x2, y2 = x + side / 2, y + (1 / 3) * h
+    x3, y3 = x - side / 2, y + (1 / 3) * h
+
+    return (x1, y1), (x2, y2), (x3, y3)
\ No newline at end of file
diff --git a/remote_run.py b/remote_run.py
index 721b75e..6e49ed3 100644
--- a/remote_run.py
+++ b/remote_run.py
@@ -10,39 +10,85 @@ import paramiko
 from utils import show_status_text
 
 
-HOST = "geophyslab.srcc.msu.ru"
-REPOS = (
-    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git",
-    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git",
-    "git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git"
+HOST = 'geophyslab.srcc.msu.ru'
+URBAN_LES_REPOS = (
+    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git',
+    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git',
+    'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git'
 )
 
-def run_on_remote_server(username, password, gitlab_username, gitlab_password):
+def build_model(server_username, server_password, gitlab_username, gitlab_password, repos=URBAN_LES_REPOS):
     try:
         ssh = paramiko.SSHClient()
         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        ssh.connect(HOST, username=username, password=password)
-        
-        scp = SCPClient(ssh.get_transport())
-        
-        scp.put("tmp.txt", "./tmp.txt")
+        ssh.connect(HOST, username=server_username, password=server_password)
 
         commands = [
-            "mkdir run",
-            "cd ./run"
+            'mkdir run',
+            'cd ./run'
         ]
 
-        # for repo in REPOS:
-        #     commands.append(repo.format(username=gitlab_username, password=gitlab_password))
+        for repo in repos:
+            commands.append(repo.format(username=gitlab_username, password=gitlab_password))
         
         commands += [
-            "cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/",
-            #"chmod +x cpall.sh",
-            #"./cpall.sh ../../code",
-            "cd ../../code",
-            #"make -B MACHINE=local COMPILER=gnu",
-            "cp ../../tmp.txt ./config.txt",
-            "./nsenx"
+            'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/',
+            'chmod +x cpall.sh',
+            './cpall.sh ../../code',
+            'cd ../../code',
+            'make -B MACHINE=local COMPILER=gnu -j 23',
+        ]
+
+        commands = '; '.join(commands)
+
+        show_status_text(f'building model')
+        dpg.show_item('loading')
+        x, y = dpg.get_item_pos('status_text')
+        dpg.set_item_pos('loading', (x - 40, y))
+
+        stdin, stdout, stderr = ssh.exec_command(commands)
+        
+        with open('log.txt', 'w') as file:
+            file.write(stdout.read().decode())
+
+        ssh.close()
+
+        show_status_text('')
+        dpg.hide_item('loading')
+        
+        show_status_text('model builded:) log written to log.txt.')
+    
+    except Exception as e:
+        show_status_text(f'error: {str(e)}')
+
+def make_run_command():
+    command = f'mpirun -np {int(dpg.get_value('-np'))} ./nsenx'
+    if dpg.get_value('-arch') != 'cpu':
+        command += f' -arch {dpg.get_value('-arch')}'
+    if dpg.get_value('use-udump'):
+        command += f' -udump {dpg.get_value('-udump')}'
+    if dpg.get_value('use-model-output'):
+        command += f' -model-output {dpg.get_value('-model-output')}'
+    return command
+
+def run_on_lab_server(username, password):
+    try:
+        show_status_text('model running...')
+        dpg.show_item('progress_bar')
+        ssh = paramiko.SSHClient()
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        ssh.connect(HOST, username=username, password=password)
+        
+        scp = SCPClient(ssh.get_transport())
+        
+        scp.put('tmp.txt', 'run/code/config.txt')
+
+        command = make_run_command()
+
+        commands = [
+            'cd ./run',
+            'cd ./code',
+            command
         ]
 
         commands = '; '.join(commands)
@@ -56,38 +102,57 @@ def run_on_remote_server(username, password, gitlab_username, gitlab_password):
                 output = stdout.channel.recv(512).decode()
                 update_progress(output)
                 log += '\n\n' + output
-            time.sleep(1)
+            # time.sleep(1)
 
         output = stdout.read().decode()
-        dpg.set_value("progress_bar", 1.)
-        dpg.hide_item('loading')
+        dpg.set_value('progress_bar', 1.)
 
         log += '\n\n' + output
         
-        with open("log.txt", "w") as res_file:
-            res_file.write(output)
+        with open('log.txt', 'w') as file:
+            file.write(log)
         
         scp.close()
         ssh.close()
         
-        show_status_text("success:) log written to log.txt.")
+        dpg.hide_item('progress_bar')
+        show_status_text('success:) log written to log.txt.')
     
     except Exception as e:
-        show_status_text(f"error: {str(e)}")
+        show_status_text(f'error: {str(e)}')
+
+def update_progress_building_model():
+    show_status_text(f'building model')
+    dpg.show_item('loading')
+    x, y = dpg.get_item_pos('status_text')
+    dpg.set_item_pos('loading', (x - 40, y))
 
 def update_progress(output):
     if not re.findall(r'([0-9]+)%', output):
         progress = 0.0
-        show_status_text(f"building model")
-        dpg.show_item('loading')
-        x, y = dpg.get_item_pos('status_text')
-        dpg.set_item_pos('loading', (x - 40, y))
     else:
         progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100
-        show_status_text('')
-        dpg.hide_item('loading')
-    dpg.set_value("progress_bar", progress)
+    dpg.set_value('progress_bar', progress)
+
+def start_remote_execution(server_username, server_password):
+    threading.Thread(target=run_on_lab_server, args=(server_username, server_password)).start()
 
-def start_remote_execution(server_username, server_password, gitlab_username, gitlab_password):
-    dpg.configure_item("login_modal", show=False)
-    threading.Thread(target=run_on_remote_server, args=(server_username, server_password, gitlab_username, gitlab_password)).start()
+def start_model_build(server_username, server_password, gitlab_username, gitlab_password):
+    threading.Thread(target=build_model, args=(server_username, server_password, gitlab_username, gitlab_password)).start()
+
+def get_server_output_structure(username, password):
+    try:
+        ssh = paramiko.SSHClient()
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        ssh.connect(HOST, username=username, password=password)
+
+        stdin, stdout, stderr = ssh.exec_command('cd ./run/code/output; find . -type f')
+
+        output = stdout.read().decode()
+        
+        ssh.close()
+
+        return output
+    
+    except Exception as e:
+        show_status_text(f'error: {str(e)}')
\ No newline at end of file
diff --git a/static_ui.py b/static_ui.py
index 051a130..8704c2f 100644
--- a/static_ui.py
+++ b/static_ui.py
@@ -8,6 +8,10 @@ from callbacks import *
 
 
 def construct_main_window_ui():
+    with dpg.theme() as progress_bar_theme:
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (255, 255, 255, 100), category=dpg.mvThemeCat_Core)
+
     with dpg.window(label='config file editor', tag='main', user_data={'config': None, 'config_file_path': None}):
         with dpg.menu_bar():
             with dpg.menu(label='config'):
@@ -23,7 +27,10 @@ def construct_main_window_ui():
                         dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(generate_map_clb), tag=lcz_type)
 
             with dpg.menu(label='run'):
-                dpg.add_menu_item(label='run on lab server', callback=remote_run_clb, tag='remote_run')
+                dpg.add_menu_item(label='edit remote run default values', callback=lambda: dpg.show_item('remote_run_settings_window'))
+                dpg.add_menu_item(label='build model on lab server', callback=show_get_credentials_modal_clb, user_data='build_model')
+                dpg.add_menu_item(label='run on lab server', callback=show_get_credentials_modal_clb, user_data='run_on_lab_server')
+                dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, user_data='download_output')
 
             with dpg.menu(label='view'):
                 with dpg.menu(label='ui', tag='ui'):
@@ -36,12 +43,40 @@ def construct_main_window_ui():
                         dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(apply_theme_clb), tag=theme)
                     dpg.set_value('orange', True)
     
-            dpg.add_progress_bar(label='progress', default_value=0.0, tag='progress_bar', height=FONT_SIZE, width=200, pos=[dpg.get_viewport_width() // 2 - 100 + SPACE, SPACE])
+            dpg.add_progress_bar(label='progress', show=False, default_value=0.0, tag='progress_bar', height=FONT_SIZE, width=200, pos=[dpg.get_viewport_width() // 2 - 100 + SPACE, SPACE])
             dpg.add_text('', tag='status_text', show=True)
             dpg.add_loading_indicator(style=1, color=(255, 255, 255, 255), thickness=2, radius=2, tag='loading', show=False)
+    dpg.bind_item_theme('progress_bar', progress_bar_theme)
+
+def construct_remote_run_settings_window():
+    xoffset = 400
+    width = 300
+    with dpg.window(label='remote run settings', show=False, tag='remote_run_settings_window', autosize=True, pos=[50, 50]):
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('number of processes for mpi run')
+            dpg.add_input_int(default_value=8, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-np')
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('arch')
+            dpg.add_radio_button(items=('gpu', 'cpu', 'mix'), label='arch', default_value='cpu', tag='-arch', horizontal=True)
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('use dump')
+            dpg.add_checkbox(default_value=False, tag='use-udump', callback=lambda s, a, u: dpg.show_item('-udump-group') if a else dpg.hide_item('-udump-group'))
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='-udump-group', show=False):
+            dpg.add_text('dump from this control points')
+            dpg.add_input_int(default_value=1, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-udump', enabled=False)
+        with dpg.group(horizontal=True, xoffset=xoffset):
+            dpg.add_text('write model stdout to file')
+            dpg.add_checkbox(default_value=False, tag='use-model-stdout', callback=lambda s, a, u: dpg.show_item('-model-stdout-group') if a else dpg.hide_item('-model-stdout-group'))
+        with dpg.group(horizontal=True, xoffset=xoffset, tag='-model-stdout-group', show=False):
+            dpg.add_text('filename')
+            dpg.add_input_text(default_value='log.txt', tag='-model-stdout', width=width, enabled=False)
+
+def construct_model_output_window():
+    with dpg.window(label='select model output to download', show=False, tag='model_output_window', width=600, height=600, pos=[50, 50]):
+        dpg.add_button(label='download selected files')
 
 def construct_crendentials_window():
-    with dpg.window(label='credentials', show=False, tag='login_modal', width=550, height=350):
+    with dpg.window(label='credentials', show=False, tag='login_modal', autosize=True, pos=[50, 50]):
         dpg.add_input_text(label='server username', tag='server_username', password=False, default_value=os.getenv('SERVER_LOGIN', ''), width=300)
         dpg.add_input_text(label='server password', tag='server_password', password=True, default_value=os.getenv('SERVER_PASS', ''), width=300)
         dpg.add_input_text(label='gitlab username', tag='gitlab_username', password=False, default_value=os.getenv('GITLAB_LOGIN', ''), width=300)
@@ -61,11 +96,15 @@ def construct_config_save_as_dialog():
 def construct_map_open_dialog():
     with dpg.file_dialog(directory_selector=False, show=False, callback=map_open_clb, tag='map_open', width=600, height=400):
         dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.tif', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.tiff', color=(0, 255, 0, 255))
         dpg.add_file_extension('.*', color=(255, 255, 255, 255))
 
 def construct_map_save_as_dialog():
     with dpg.file_dialog(directory_selector=False, show=False, callback=map_save_as_clb, tag='map_save_as', width=600, height=400):
         dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.tif', color=(0, 255, 0, 255))
+        dpg.add_file_extension('.tiff', color=(0, 255, 0, 255))
         dpg.add_file_extension('.*', color=(255, 255, 255, 255))
 
 def construct_set_file_path_dialog():
@@ -81,18 +120,47 @@ def construct_search_window():
         dpg.add_input_text(hint='search for...', on_enter=True, callback=search_clb, tag='search')
 
 def construct_map_edit_window():
-    with dpg.texture_registry():
-        dpg.add_dynamic_texture(800, 800, np.zeros((800, 800, 4), dtype=np.uint8), tag='heatmap_texture')
+    user_data = {
+        'buildings_map': None,
+        'trees_map': None,
+        'roads_map': None,
+        'height_map_file_path': None,
+        'drawing': False,
+        'prev_coords': None,
+        'current_action': None,
+        'max_height': 0,
+        'output_dirs': None,
+    }
+
+    dpg.add_texture_registry(tag='textures')
 
     with dpg.theme() as item_theme:
         with dpg.theme_component(dpg.mvAll):
             dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 0, 0, 0), category=dpg.mvThemeCat_Core)
             dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 0, category=dpg.mvThemeCat_Core)
 
-    dpg.add_colormap_registry(label='colormap registry', tag='colormap_registry')
-    dpg.add_colormap(list(map(apply_tint, [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='colormap', parent='colormap_registry')
+    with dpg.theme(tag='map_image_theme'):
+        with dpg.theme_component(dpg.mvAll):
+            dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 0, 0, category=dpg.mvThemeCat_Core)
+            dpg.add_theme_style(dpg.mvStyleVar_ItemInnerSpacing, 0, 0, category=dpg.mvThemeCat_Core)
 
-    with dpg.window(label='edit map', tag='map_window', user_data={'height_map': None, 'height_map_file_path': None, 'drawing': False, 'prev_coords': None, 'current_action': None}, horizontal_scrollbar=True, show=False, width=600, height=400, pos=[20, 60], on_close=map_window_on_close_clb):
+    dpg.add_colormap_registry(label='colormap registry', tag='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(BUILDING_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='buildings_colormap', parent='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(TREE_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='trees_colormap', parent='colormap_registry')
+    dpg.add_colormap(list(map(apply_tint_func(ROAD_COLOR), [[255, 255, 255], [255, 255, 255], [255, 255, 255]])), False, tag='roads_colormap', parent='colormap_registry')
+
+    with dpg.window(
+                label='edit map',
+                tag='map_window',
+                user_data=user_data,
+                horizontal_scrollbar=True,
+                show=False,
+                width=600,
+                height=400,
+                pos=[20, 60],
+                on_close=map_window_on_close_clb):
         with dpg.menu_bar():
             with dpg.menu(label='file'):
                 dpg.add_menu_item(label='save', callback=map_save_clb, tag='map_save')
@@ -100,15 +168,21 @@ def construct_map_edit_window():
 
         with dpg.group(horizontal=True):
             with dpg.group(width=200, tag='tools'):
+                dpg.add_radio_button(items=('buildings', 'trees', 'roads'), tag='layer', default_value='buildings', callback=lambda s, a, u: dpg.bind_colormap('height_input', f'{a}_colormap'))
                 dpg.add_button(label='erase', callback=set_action_clb, tag='erase')
-                dpg.add_button(label='draw rectangle', callback=set_action_clb, tag='draw_rect')
+                dpg.add_button(label='add rectangle', callback=set_action_clb, tag='draw_rect')
                 dpg.add_colormap_slider(tag='height_input')
-                dpg.bind_colormap(dpg.last_item(), 'colormap')
-            dpg.add_image('heatmap_texture', tint_color=(0, 119, 200, 255), tag='map')
+                dpg.bind_colormap(dpg.last_item(), 'buildings_colormap')
+
+            with dpg.child_window(no_scrollbar=True, no_scroll_with_mouse=True, menubar=False, border=True, tag='map_child_window'):
+                dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
 
-        dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
+                with dpg.child_window(tag='emission_values', show=False, width=100, height=200):
+                    dpg.add_button(label='delete', tag='delete_emission_point')#, callback=delete_emission_point_clb)
+                    dpg.add_text(tag='emission_settings')
 
     dpg.bind_item_theme('drawing_frame', item_theme)
+    dpg.bind_item_theme('map_child_window', 'map_image_theme')
 
 
 def construct_ui():
@@ -119,7 +193,10 @@ def construct_ui():
     construct_map_open_dialog()
     construct_map_save_as_dialog()
     construct_map_edit_window()
+
     construct_crendentials_window()
+    construct_remote_run_settings_window()
+    construct_model_output_window()
 
     construct_config_save_as_dialog()
     construct_config_open_dialog()
diff --git a/utils.py b/utils.py
index bb18a0a..baf43fa 100644
--- a/utils.py
+++ b/utils.py
@@ -1,10 +1,22 @@
-import dearpygui.dearpygui as dpg
+import re
+import threading
 
 import numpy as np
-import threading
+import dearpygui.dearpygui as dpg
 
 from constants import *
 
+def parse_dirs(s):
+    filepaths = set(map(lambda x: x[2:], s.strip('\n').split('\n')[1:]))
+    dirs = set()
+    for path in filepaths:
+        lst = path.split('/')
+        dir_ = '/'.join(lst[:-1])
+        if dir_:
+            dirs.add(dir_)
+
+    return dirs, filepaths
+
 def get_current_ui_type():
     for child in dpg.get_item_children('ui', 1):
         if dpg.get_value(child):
@@ -26,11 +38,11 @@ def set_main_window_value(key, value):
     data[key] = value
     dpg.set_item_user_data('main', data)
 
-def get_height_map_path():
-    return dpg.get_item_user_data('map_window')['height_map_file_path']
+def get_main_window_value(key):
+    return dpg.get_item_user_data('main').get(key, None)
 
-def get_height_map():
-    return dpg.get_item_user_data('map_window')['height_map']
+def get_height_map_file_path():
+    return dpg.get_item_user_data('map_window')['height_map_file_path']
 
 def is_drawing():
     return dpg.get_item_user_data('map_window')['drawing']
@@ -46,32 +58,66 @@ def set_map_window_value(key, value):
     data[key] = value
     dpg.set_item_user_data('map_window', data)
 
+def get_map_window_value(key):
+    return dpg.get_item_user_data('map_window').get(key, None)
+
 def show_status_text(message):
     dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(message)[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1]))
     dpg.set_value('status_text', message)
 
+def get_emission_points():
+    points = dict()
+    config = get_config()
+    for key in config:
+        if 'point_emission' in key:
+            point_n = re.findall(r'tracer_([0-9]+)', key)[-1]
+            if point_n not in points:
+                points[point_n] = dict()
+            points[point_n][config[key].key] = config[key].value
+    return points
+
+def get_max_n_emission_point():
+    config = get_config()
+    n = 0
+    for key in config:
+        if 'point_emission' in key:
+            n = max(int(re.findall(r'tracer_([0-9]+)', key)[-1]), n)
+    return n
+
+def apply_tint_func(tint):
+    tint = np.array(tint[:-1]) / 255
+    def func(color):
+        color = np.array(color) / 255
+        color *= tint
+        return list(map(int, (color * 255)))
+    return func
+
+def apply_tint(color, tint=(0, 119, 200)):
+    tint = np.array(tint) / 255
+    color = np.array(color) / 255
+    color *= tint
+    return list(map(int, (color * 255)))
+
 def path_iter(path, prefix=""):
     namespaces = path.split('.')
     for i in range(len(namespaces)):
         yield prefix + '.' + '.'.join(namespaces[:i+1])
 
 def delete_item_and_clear_alias(item):
-    alias = dpg.get_item_alias(item)
-    if alias:
-        dpg.remove_alias(alias)
     if dpg.does_item_exist(item):
         dpg.delete_item(item)
 
-def delete_item_children_and_clear_aliases_(item):
+def delete_item_children_and_clear_aliases(item):
     if dpg.does_item_exist(item):
         for child in dpg.get_item_children(item, 1):
-            delete_item_children_and_clear_aliases_(child)
-            delete_item_and_clear_alias(child)
+            delete_item_children_and_clear_aliases(child)
+        for child in dpg.get_item_children(item, 2):
+            delete_item_children_and_clear_aliases(child)
+        delete_item_and_clear_alias(item)
 
-def delete_item_children_and_clear_aliases():
+def delete_config_items_and_clear_aliases():
     if len(dpg.get_item_children('main', 1)) > 1:
-        delete_item_children_and_clear_aliases_(dpg.get_item_children('main', 1)[1])
-        delete_item_and_clear_alias(dpg.get_item_children('main', 1)[1])
+        delete_item_children_and_clear_aliases(dpg.get_item_children('main', 1)[1])
 
 ####################################################################################
 #
@@ -87,66 +133,6 @@ def change_color_temporarily(text_tag, color, duration=1):
 def revert_color(text_tag):
     dpg.configure_item(text_tag, color=(255, 255, 255, 255))
 
-####################################################################################
-#
-#    edit map utils
-#
-####################################################################################
-
-def update_texture(height_map):
-    max_height = np.max(height_map)
-    normalized_map = height_map / max_height
-
-    texture_data = normalized_map.flatten()
-    texture_data = np.tile(texture_data, (4, 1)).T.flatten()
-
-    texture_data.resize(800 * 800 * 4)
-
-    width, height = height_map.shape
-
-    dpg.set_value('heatmap_texture', texture_data)
-
-def check_coords(x, y, w, h):
-    mx, my = dpg.get_item_pos('map')
-    mw, mh = dpg.get_item_width('map'), dpg.get_item_height('map')
-
-    return mx <= x <= x + w <= mx + mw and my <= y <= y + h <= my + mh
-
-def apply_tint(color, tint=(0, 119, 200)):
-    tint = np.array(tint) / 255
-    color = np.array(color) / 255
-    color *= tint
-    return list(map(int, (color * 255)))
-
-def adjust_coords(coords):
-    x, y = coords
-    return int(x - dpg.get_item_pos('map')[0] + dpg.get_x_scroll('map_window')), int(y - dpg.get_item_pos('map')[1] + dpg.get_text_size('edit map')[1] + 2 * SPACE + dpg.get_y_scroll('map_window'))
-
-def get_rect(cur_pos, end_pos):
-    x1, y1 = adjust_coords(cur_pos)
-    x2, y2 = adjust_coords(end_pos)
-
-    if y2 < y1:
-        y1, y2 = y2, y1
-
-    if x2 < x1:
-        x1, x2 = x2, x1
-
-    return y1, y2, x1, x2
-
-def load_height_map(file_path):
-    with open(file_path, "r") as file:
-        lines = file.readlines()[1:]
-        height_map = [list(map(int, line.strip().split())) for line in lines]
-    return np.array(height_map)
-
-def dump_map(height_map, file_path):
-    with open(file_path, "w", encoding="utf-8") as file:
-        w, h = height_map.shape
-        file.write(f"{w} {h}\n")
-        for row in height_map:
-            file.write(" ".join(list(map(str, row))) + "\n")
-
 ####################################################################################
 #
 #    main menu utils
-- 
GitLab