From f6d0233a252a9a190d51aca431fddaa6108f1f85 Mon Sep 17 00:00:00 2001
From: Maryshca <kuzmichova.maria@ya.ru>
Date: Fri, 13 Sep 2024 15:09:03 +0300
Subject: [PATCH] added maps edit

---
 app.py                            | 387 +++++++++++++++++++++---------
 configs/compact_high_rise.json    |  14 ++
 configs/compact_low_rise.json     |  14 ++
 configs/compact_mid_rise.json     |  14 ++
 configs/heavy_industry.json       |  14 ++
 configs/large_low_rise.json       |  14 ++
 configs/lightweight_low_rise.json |  14 ++
 configs/open_high_rise.json       |  14 ++
 configs/open_low_rise.json        |  14 ++
 configs/open_mid_rise.json        |  14 ++
 configs/sparsley_build.json       |  14 ++
 generator.py                      | 316 ++++++++++++++++++++++++
 prerun.py                         |   2 +-
 styles.py                         | 136 +++++++----
 utils.py                          |  79 ++++++
 15 files changed, 891 insertions(+), 169 deletions(-)
 create mode 100644 configs/compact_high_rise.json
 create mode 100644 configs/compact_low_rise.json
 create mode 100644 configs/compact_mid_rise.json
 create mode 100644 configs/heavy_industry.json
 create mode 100644 configs/large_low_rise.json
 create mode 100644 configs/lightweight_low_rise.json
 create mode 100644 configs/open_high_rise.json
 create mode 100644 configs/open_low_rise.json
 create mode 100644 configs/open_mid_rise.json
 create mode 100644 configs/sparsley_build.json
 create mode 100644 generator.py
 create mode 100644 utils.py

diff --git a/app.py b/app.py
index d5a523d..a76068a 100644
--- a/app.py
+++ b/app.py
@@ -3,68 +3,92 @@ import ctypes
 import sys
 
 import dearpygui.dearpygui as dpg
+import numpy as np
 
 from styles import GuiStyle, INDENT, SPACE, FONT_SIZE, ICON_PATH
 from config import load_config, dump_config
+from generator import LCZ
+from utils import *
 
 ctypes.windll.shcore.SetProcessDpiAwareness(1)
 
-def get_absolute_y(item):
-    y = dpg.get_item_pos(item)[1]
-    parent = dpg.get_item_parent(item)
-    while parent:
-        if dpg.get_item_type(parent) == 'mvAppItemType::mvChildWindow':
-            y += dpg.get_item_pos(parent)[1]
-        parent = dpg.get_item_parent(parent)
-    return y
-
-def get_item_level(item):
-    level = 0
-    parent = dpg.get_item_parent(item)
-    while parent:
-        if dpg.get_item_type(parent) == 'mvAppItemType::mvTabBar':
-            level += 1
-        parent = dpg.get_item_parent(parent)
-    return level
-
-def config_iter(config, prefix=""):
-    for path in config:
-        namespaces = path.split('.')
-        for i in range(len(namespaces)):
-            yield prefix + '.' + '.'.join(namespaces[:i+1])
+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']
+
+UIS = [TABS_UI, COLLAPSING_HEADERS_UI]
 
 class ConfigEditor:
     def __init__(self):
-        dpg.create_context()
-        self.styles = GuiStyle()
         self.config = dict()
         self.config_file_path = str()
 
-    def delete_config_window_and_clear_aliases(self, file_path):
-        if dpg.does_item_exist(file_path):
+        self.height_map = None
+        self.height_map_path = str()
+
+        self.current_action = None
+        self.prev_coords = None
+        self.drawing = False
+
+        dpg.create_context()
+        dpg.create_viewport(
+            title='config file editor',
+            small_icon=ICON_PATH,
+            large_icon=ICON_PATH
+        )
+        dpg.setup_dearpygui()
+        dpg.show_viewport()
+
+        self.styles = GuiStyle()
 
-            if dpg.get_item_type(file_path) == "mvAppItemType::mvChildWindow":
-                uuid = dpg.get_item_parent(dpg.get_item_parent(file_path))
+        self.construct_main_window_ui()
+        if self.config_file_path:
+            self.construct_config_ui()
+
+    def update_texture(self):
+        if not self.height_map_path:
+            return
+        max_height = np.max(self.height_map)
+        normalized_map = self.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 = self.height_map.shape
+
+        dpg.set_value('heatmap_texture', texture_data)
+
+    def map_edit_window_on_close(self):
+        self.height_map = None
+        self.height_map_path = str()
+
+        self.current_action = None
+        self.prev_coords = None
+        self.drawing = False
+
+    def delete_config_ui_and_clear_aliases(self):
+        if dpg.does_item_exist(self.config_file_path):
+            if dpg.get_item_type(self.config_file_path) == 'mvAppItemType::mvChildWindow':
+                uuid = dpg.get_item_parent(dpg.get_item_parent(self.config_file_path))
                 dpg.delete_item(uuid)
             else:
-                dpg.delete_item(file_path)
+                dpg.delete_item(self.config_file_path)
 
-        if dpg.does_alias_exist(file_path):
-            dpg.remove_alias(file_path)
-        for path in config_iter(self.config, prefix=self.config_file_path):
-            if dpg.does_alias_exist(path):
-                dpg.remove_alias(path)
+        if dpg.does_alias_exist(self.config_file_path):
+            dpg.remove_alias(self.config_file_path)
+        for full_path in self.config:
+            for path in path_iter(full_path, prefix=self.config_file_path):
+                if dpg.does_alias_exist(path):
+                    dpg.remove_alias(path)
 
-    def update_config(self):
+    def update_config_values(self):
         for path in self.config:
-            if dpg.does_item_exist(f"{self.config_file_path}.{path}"):
-                self.config[path].value = dpg.get_value(f"{self.config_file_path}.{path}")
-
-    def check_tabview(self, root):
-        for uuid in dpg.get_item_children(root, 1):
-            if dpg.get_item_type(uuid) == "mvAppItemType::mvTabBar":
-                return True, uuid
-        return False, None
+            if dpg.does_item_exist(f'{self.config_file_path}.{path}'):
+                self.config[path].value = dpg.get_value(f'{self.config_file_path}.{path}')
 
     def manage_file(self, sender, sender_data, user_data):
         file_path = sender_data['file_path_name']
@@ -72,81 +96,198 @@ class ConfigEditor:
         if not file_path:
             return
 
-        if user_data == "open":
-            self.delete_config_window_and_clear_aliases(self.config_file_path)
+        if user_data == 'open_config':
+            self.delete_config_ui_and_clear_aliases()
             self.config_file_path = file_path
             self.config = load_config(file_path)
-            self.construct_config_ui()
-        elif user_data == "save_as" or user_data == "save":
-            self.update_config()
+            self.construct_config_ui(dpg.get_item_user_data('ui'))
+        elif user_data == 'open_map':
+            self.height_map_path = file_path
+            self.height_map = load_height_map(file_path)
+            dpg.show_item('map_window')
+            self.update_texture()
+        elif user_data == 'save_as' or user_data == 'save':
+            self.update_config_values()
             dump_config(self.config, file_path)
-            dpg.show_item("file_save_popup")
+            dpg.show_item('file_save_popup')
+        elif user_data == 'save_map_as' or user_data == 'save_map':
+            dump_map(self.height_map, file_path)
+            dpg.show_item('file_save_popup')
+
+    def generate_map(self, sender, sender_data, user_data):
+        self.height_map_path = 'lcz.txt'
+        self.height_map = LCZ(config_path=f'configs/{sender}.json').to_height_map()
+        dpg.show_item('map_window')
+        self.update_texture()
 
     def set_file_path(self, sender, sender_data, user_data):
         file_path = sender_data['file_path_name']
         dpg.set_value(user_data, file_path)
 
     def open_file_dialog(self, sender, sender_data, user_data):
-        dpg.show_item("file_dialog")
-        dpg.set_item_user_data("file_dialog", user_data)
+        dpg.show_item('file_dialog')
+        dpg.set_item_user_data('file_dialog', user_data)
 
     def open_folder_dialog(self, sender, sender_data, user_data):
-        dpg.show_item("folder_dialog")
-        dpg.set_item_user_data("folder_dialog", user_data)
+        dpg.show_item('folder_dialog')
+        dpg.set_item_user_data('folder_dialog', user_data)
+
+    def set_action(self, action):
+        self.current_action = action
+
+    def check_coords(self, 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 mouse_down_callback(self):
+        if not self.height_map_path:
+            return
+        if not self.drawing:
+            self.prev_coords = dpg.get_mouse_pos()
+            self.drawing = True
+        else:
+            x, y = dpg.get_mouse_pos()
+            px, py = self.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 self.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(self):
+        coords = dpg.get_mouse_pos()
+
+        if self.current_action == 'erase':
+            self.draw_rectangle(coords, 0)
+        elif self.current_action == 'draw_rect':
+            height = dpg.get_value('height_input')
+            self.draw_rectangle(coords, np.max(self.height_map) * height)
+
+        self.update_texture()
+
+        self.drawing = False
+        dpg.hide_item('drawing_frame')
+
+    def draw_rectangle(self, coords, height):
+        y1, y2, x1, x2 = get_rect(self.prev_coords, coords)
+        self.height_map[y1:y2, x1:x2] = height
+
+    def run_on_lab_server(self):
+        pass
+
+    def construct_config_ui(self, ui):
+        if not self.config_file_path:
+            return
 
-    def construct_config_ui(self):
-        self.update_config()
-        self.delete_config_window_and_clear_aliases(self.config_file_path)
-        if dpg.get_value("use_tabs_ui"):
+        self.update_config_values()
+        self.delete_config_ui_and_clear_aliases()
+        if ui == TABS_UI:
             self.construct_config_ui_tabs()
         else:
             self.construct_config_ui_collapsing_headers()
 
-    def construct_main_window(self):
+    def construct_main_window_ui(self):
         dpg.bind_font(self.styles.montserrat_font)
-        dpg.bind_theme(self.styles.main_theme)
+        self.styles.apply_theme('orange')
+
+        with dpg.window(label='config file editor', tag='main'):
+            with dpg.menu_bar():
+                with dpg.menu(label='config'):
+                    dpg.add_menu_item(label='open', callback=self.open_file_dialog, tag='open_btn', user_data='open_config')
+                    dpg.add_menu_item(label='save', callback=lambda: self.manage_file('', {'file_path_name': self.config_file_path}, 'save'))
+                    dpg.add_menu_item(label='save as', callback=self.open_file_dialog, tag='save_as_btn', user_data='save_as')
+
+                with dpg.menu(label='map'):
+                    dpg.add_menu_item(label='open map', callback=self.open_file_dialog, tag='open_map_btn', user_data='open_map')
+                    with dpg.menu(label='generate map'):
+                        for lcz_type in LCZS:
+                            dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(self.generate_map), tag=lcz_type)
+
+                with dpg.menu(label='run'):
+                    dpg.add_menu_item(label='run on lab server', callback=self.run_on_lab_server)
 
-        with dpg.window(label="config file editor", pos=(10, 10), width=dpg.get_viewport_client_width(), height=dpg.get_viewport_client_height(), tag="main", menubar=True):
-            with dpg.menu_bar(tag="main_menu_bar"):
-                with dpg.menu(label="file"):
-                    dpg.add_menu_item(label="open", callback=self.open_file_dialog, tag="open_btn", user_data="open")
-                    dpg.add_menu_item(label="save", callback=lambda: self.manage_file("", {'file_path_name': self.config_file_path}, "save"))
-                    dpg.add_menu_item(label="save as", callback=self.open_file_dialog, tag="save_as_btn", user_data="save_as")
+                with dpg.menu(label='view'):
+                    with dpg.menu(label='ui', tag='ui', user_data=COLLAPSING_HEADERS_UI):
+                        for ui in UIS:
+                            dpg.add_menu_item(label=ui, check=True, default_value=False, callback=combo_menu(lambda s, a, u: self.construct_config_ui(s)), tag=ui)
+                        dpg.set_value(UIS[1], True)
 
-                with dpg.menu(label="view"):
-                    dpg.add_menu_item(label="use default theme", check=True, callback=lambda _, v: dpg.bind_theme(None if v else self.styles.main_theme), default_value=False)
-                    dpg.add_menu_item(label="use default font", check=True, callback=lambda _, v: dpg.bind_font(None if v else self.styles.montserrat_font), default_value=False)
-                    dpg.add_menu_item(label="use tabs", check=True, callback=lambda: self.construct_config_ui(), default_value=False, tag="use_tabs_ui")
-                    dpg.add_menu_item(label="show theme editor", callback=dpg.show_style_editor)
+                    with dpg.menu(label='theme'):
+                        for theme in self.styles.themes:
+                            dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(lambda s, a, u: self.styles.apply_theme(s)), tag=theme)
+                        dpg.set_value('orange', True)
+                    
+                    dpg.add_menu_item(label='show theme editor', callback=dpg.show_style_editor)
 
-                with dpg.menu(label="settings"):
-                    dpg.add_menu_item(label="show mouse coords", check=True, callback=lambda _, v: dpg.show_item("mouse") if v else dpg.hide_item("mouse"), default_value=False)
+                with dpg.menu(label='settings'):
+                    dpg.add_menu_item(label='show mouse coords', check=True, callback=lambda _, v: dpg.show_item('mouse') if v else dpg.hide_item('mouse'), default_value=False)
 
-                dpg.add_text("", tag="mouse", show=False)
+                dpg.add_text('', tag='mouse', show=False)
 
-        with dpg.file_dialog(directory_selector=False, show=False, callback=self.manage_file, tag="file_dialog", width=600, height=400):
-            dpg.add_file_extension(".txt", color=(0, 255, 0, 255))
-            dpg.add_file_extension(".*", color=(255, 255, 255, 255))
+        with dpg.file_dialog(directory_selector=False, show=False, callback=self.manage_file, tag='file_dialog', width=600, height=400):
+            dpg.add_file_extension('.txt', color=(0, 255, 0, 255))
+            dpg.add_file_extension('.*', color=(255, 255, 255, 255))
 
-        with dpg.file_dialog(directory_selector=True, show=False, callback=self.set_file_path, tag="folder_dialog", width=500, height=400):
-            dpg.add_file_extension(".*", color=(255, 255, 255, 255))
+        with dpg.file_dialog(directory_selector=True, show=False, callback=self.set_file_path, tag='folder_dialog', width=500, height=400):
+            dpg.add_file_extension('.*', color=(255, 255, 255, 255))
 
-        with dpg.window(label="", autosize=True, no_resize=True, modal=True, tag="file_save_popup", show=False, pos=(40, 40)):
-            dpg.add_text("file saved successfully:)")
+        with dpg.window(label='', autosize=True, no_resize=True, modal=True, tag='file_save_popup', show=False, pos=(40, 40)):
+            dpg.add_text('file saved successfully:)')
 
-        with dpg.item_handler_registry(tag="main_handler") as handler:
+        with dpg.item_handler_registry(tag='main_handler'):
             dpg.add_item_resize_handler(callback=lambda: self.change_height(self.config_file_path))
 
-        dpg.bind_item_handler_registry("main", "main_handler")
+        dpg.bind_item_handler_registry('main', 'main_handler')
         dpg.set_viewport_resize_callback(lambda: self.change_height(self.config_file_path))
 
-        uuid = dpg.add_handler_registry()
+        with dpg.texture_registry():
+            dpg.add_dynamic_texture(800, 800, np.zeros((800, 800, 4), dtype=np.uint8), tag='heatmap_texture')
+
+        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.window(label='edit map', tag='map_window', horizontal_scrollbar=True, show=False, width=600, height=400, pos=[20, 60], on_close=self.map_edit_window_on_close):
+            with dpg.menu_bar():
+                with dpg.menu(label='file'):
+                    dpg.add_menu_item(label='save', callback=lambda: self.manage_file('', {'file_path_name': self.height_map_path}, 'save_map'))
+                    dpg.add_menu_item(label='save as', callback=self.open_file_dialog, tag='save_map_as_btn', user_data='save_map_as')
+
+            with dpg.group(horizontal=True):
+                with dpg.group(width=200, tag='tools'):
+                    dpg.add_button(label='erase', callback=lambda: self.set_action('erase'))
+                    dpg.add_button(label='draw rectangle', callback=lambda: self.set_action('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.add_child_window(tag='drawing_frame', show=False, width=0, height=0)
+
+        dpg.bind_item_theme('drawing_frame', item_theme)
+
         def clb(s, a, u):
-            dpg.set_value("mouse", str(a))
-        dpg.add_mouse_move_handler(parent=uuid, callback=clb)
+            dpg.set_value('mouse', str(a))
+
+        with dpg.handler_registry():
+            dpg.add_mouse_move_handler(callback=clb)
+            dpg.add_mouse_down_handler(callback=self.mouse_down_callback)
+            dpg.add_mouse_release_handler(callback=self.mouse_release_callback)
 
     def construct_config_ui_collapsing_headers(self):
-        dpg.add_collapsing_header(label=self.config_file_path, parent="main", tag=self.config_file_path, indent=0, default_open=True)
+        dpg.add_collapsing_header(label=self.config_file_path, parent='main', tag=self.config_file_path, indent=0, default_open=True)
 
         for full_path in self.config:
             namespaces = full_path.split('.')
@@ -162,9 +303,9 @@ class ConfigEditor:
             self.create_widget(full_path)
 
     def construct_config_ui_tabs(self):
-        tab_bar_exists, tab_bar_uuid = self.check_tabview("main")
+        tab_bar_exists, tab_bar_uuid = does_tab_bar_child_exist('main')
         if not tab_bar_exists:
-            tab_bar_uuid = dpg.add_tab_bar(parent="main")
+            tab_bar_uuid = dpg.add_tab_bar(parent='main')
         tab_uuid = dpg.add_tab(label=self.config_file_path, parent=tab_bar_uuid)
         dpg.add_child_window(parent=tab_uuid, tag=self.config_file_path, border=True, autosize_y=False, autosize_x=True)
         dpg.set_item_user_data(self.config_file_path, 2 * SPACE)
@@ -175,7 +316,7 @@ class ConfigEditor:
             for level in range(len(namespaces) - 1):
                 path = self.config_file_path + '.' + '.'.join(namespaces[:level+1])
 
-                tab_bar_exists, tab_bar_uuid = self.check_tabview(parent)
+                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=lambda s, a: self.change_height(a))
@@ -193,7 +334,7 @@ class ConfigEditor:
     def change_height(self, item):
         if not dpg.does_item_exist(item):
             return
-        if dpg.get_item_type(item) == "mvAppItemType::mvChildWindow":
+        if dpg.get_item_type(item) == 'mvAppItemType::mvChildWindow':
             uuid = dpg.get_item_parent(item)
             new_height = dpg.get_viewport_client_height() - get_absolute_y(uuid) - SPACE * get_item_level(item)
 
@@ -211,49 +352,55 @@ class ConfigEditor:
         key = self.config[full_path].key
         comment = self.config[full_path].comment
 
-        if dpg.get_value("use_tabs_ui") and parent != self.config_file_path and full_path != key:
+        if dpg.get_value('use_tabs_ui') and parent != self.config_file_path and full_path != key:
             dpg.set_item_user_data(parent, dpg.get_item_user_data(parent) + 2 * SPACE + FONT_SIZE + SPACE)
 
         indent = INDENT
-        if dpg.get_value("use_tabs_ui"):
+        if dpg.get_value('use_tabs_ui'):
             indent = 0
 
-        with dpg.group(horizontal=True, parent=parent, indent=indent):
-            text_uuid = dpg.add_text(key)
-            if value_type == 'BOOLEAN':
-                dpg.add_checkbox(default_value=value, tag=tag)
-            elif value_type == 'DOUBLE':
-                dpg.add_input_double(default_value=value, tag=tag, format="%.10f")
-            elif value_type == 'INT':
-                dpg.add_input_int(default_value=value, tag=tag)
-            elif "dir" in key.lower():
-                dpg.add_input_text(default_value=value, tag=tag)
-                dpg.add_button(label="...", callback=self.open_folder_dialog, user_data=tag)
+        with dpg.group(horizontal=True, parent=parent, indent=indent, xoffset=XOFFSET):
+            if not comment:
+                dpg.add_text(key)
+                if value_type == 'BOOLEAN':
+                    dpg.add_checkbox(default_value=value, tag=tag)
+                elif value_type == 'DOUBLE':
+                    dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
+                elif value_type == 'INT':
+                    dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                elif 'dir' in key.lower() or 'file' in key.lower():
+                    with dpg.group(horizontal=True):
+                        dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                        dpg.add_button(label='...', callback=self.open_folder_dialog, user_data=tag)
+                else:
+                    dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
             else:
-                dpg.add_input_text(default_value=value, tag=tag)
-            if comment:
-                dpg.add_text(comment, parent=dpg.add_tooltip(parent=text_uuid))
+                dpg.add_text(key)
+                with dpg.group(horizontal=True, xoffset=WIDGET_WIDTH + 100):
+                    if value_type == 'BOOLEAN':
+                        dpg.add_checkbox(default_value=value, tag=tag)
+                    elif value_type == 'DOUBLE':
+                        dpg.add_input_double(default_value=value, tag=tag, format='%.10f', width=WIDGET_WIDTH)
+                    elif value_type == 'INT':
+                        dpg.add_input_int(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                    elif 'dir' in key.lower() or 'file' in key.lower():
+                        with dpg.group(horizontal=True):
+                            dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH - 3 * SPACE - dpg.get_text_size('...')[0])
+                            dpg.add_button(label='...', callback=self.open_folder_dialog, user_data=tag)
+                    else:
+                        dpg.add_input_text(default_value=value, tag=tag, width=WIDGET_WIDTH)
+                    dpg.add_text(comment, color=(255, 255, 255, 150))
 
     def run(self):
-        dpg.create_viewport(
-            title='config file editor',
-            small_icon=ICON_PATH,
-            large_icon=ICON_PATH
-        )
-        dpg.setup_dearpygui()
-        dpg.show_viewport()
-        self.construct_main_window()
-        if self.config_file_path:
-            self.construct_config_ui()
-        dpg.set_primary_window("main", True)
+        dpg.set_primary_window('main', True)
         dpg.start_dearpygui()
         dpg.destroy_context()
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     app = ConfigEditor()
 
     if len(sys.argv) > 1:
         app.config_file_path = sys.argv[1]
         app.config = load_config(sys.argv[1])
 
-    app.run()
+    app.run()
\ No newline at end of file
diff --git a/configs/compact_high_rise.json b/configs/compact_high_rise.json
new file mode 100644
index 0000000..5377415
--- /dev/null
+++ b/configs/compact_high_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 5,
+    "building_surface_fraction": 0.5,
+    "mean_height_of_roughness_elements": 50,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 75,
+    "min_building_height": 25,
+    "sky_view_factor": 0.2,
+    "impervious_surface_fraction": 0.6,
+    "pervious_surface_fraction": 0.09,
+    "terrain_roughness_class": 8
+}
\ No newline at end of file
diff --git a/configs/compact_low_rise.json b/configs/compact_low_rise.json
new file mode 100644
index 0000000..85f5da1
--- /dev/null
+++ b/configs/compact_low_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 1,
+    "building_surface_fraction": 0.5,
+    "mean_height_of_roughness_elements": 6,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 10,
+    "min_building_height": 3,
+    "sky_view_factor": 0.3,
+    "impervious_surface_fraction": 0.3,
+    "pervious_surface_fraction": 0.2,
+    "terrain_roughness_class": 6
+}
\ No newline at end of file
diff --git a/configs/compact_mid_rise.json b/configs/compact_mid_rise.json
new file mode 100644
index 0000000..8c13e06
--- /dev/null
+++ b/configs/compact_mid_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 1.2,
+    "building_surface_fraction": 0.5,
+    "mean_height_of_roughness_elements": 18,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 25,
+    "min_building_height": 10,
+    "sky_view_factor": 0.3,
+    "impervious_surface_fraction": 0.6,
+    "pervious_surface_fraction": 0.1,
+    "terrain_roughness_class": 6
+}
\ No newline at end of file
diff --git a/configs/heavy_industry.json b/configs/heavy_industry.json
new file mode 100644
index 0000000..53ae904
--- /dev/null
+++ b/configs/heavy_industry.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 0.3,
+    "building_surface_fraction": 0.25,
+    "mean_height_of_roughness_elements": 10,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 15,
+    "min_building_height": 5,
+    "sky_view_factor": 0.7,
+    "impervious_surface_fraction": 0.3,
+    "pervious_surface_fraction": 0.45,
+    "terrain_roughness_class": 5
+}
\ No newline at end of file
diff --git a/configs/large_low_rise.json b/configs/large_low_rise.json
new file mode 100644
index 0000000..5cc56dd
--- /dev/null
+++ b/configs/large_low_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 0.2,
+    "building_surface_fraction": 0.4,
+    "mean_height_of_roughness_elements": 5,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 9,
+    "min_building_height": 3,
+    "sky_view_factor": 0.8,
+    "impervious_surface_fraction": 0.4,
+    "pervious_surface_fraction": 0.1,
+    "terrain_roughness_class": 5
+}
\ No newline at end of file
diff --git a/configs/lightweight_low_rise.json b/configs/lightweight_low_rise.json
new file mode 100644
index 0000000..40a8452
--- /dev/null
+++ b/configs/lightweight_low_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 1.2,
+    "building_surface_fraction": 0.8,
+    "mean_height_of_roughness_elements": 4,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 6,
+    "min_building_height": 2,
+    "sky_view_factor": 0.3,
+    "impervious_surface_fraction": 0.1,
+    "pervious_surface_fraction": 0.2,
+    "terrain_roughness_class": 4
+}
\ No newline at end of file
diff --git a/configs/open_high_rise.json b/configs/open_high_rise.json
new file mode 100644
index 0000000..2329717
--- /dev/null
+++ b/configs/open_high_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 0.9,
+    "building_surface_fraction": 0.3,
+    "mean_height_of_roughness_elements": 30,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 40,
+    "min_building_height": 20,
+    "sky_view_factor": 0.6,
+    "impervious_surface_fraction": 0.3,
+    "pervious_surface_fraction": 0.3,
+    "terrain_roughness_class": 7
+}
\ No newline at end of file
diff --git a/configs/open_low_rise.json b/configs/open_low_rise.json
new file mode 100644
index 0000000..fae24b5
--- /dev/null
+++ b/configs/open_low_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 0.6,
+    "building_surface_fraction": 0.3,
+    "mean_height_of_roughness_elements": 6,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 10,
+    "min_building_height": 3,
+    "sky_view_factor": 0.7,
+    "impervious_surface_fraction": 0.3,
+    "pervious_surface_fraction": 0.4,
+    "terrain_roughness_class": 5
+}
\ No newline at end of file
diff --git a/configs/open_mid_rise.json b/configs/open_mid_rise.json
new file mode 100644
index 0000000..21bb900
--- /dev/null
+++ b/configs/open_mid_rise.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 800,
+    "aspect_ratio": 0.5,
+    "building_surface_fraction": 0.3,
+    "mean_height_of_roughness_elements": 18,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 25,
+    "min_building_height": 10,
+    "sky_view_factor": 0.6,
+    "impervious_surface_fraction": 0.4,
+    "pervious_surface_fraction": 0.3,
+    "terrain_roughness_class": 5
+}
\ No newline at end of file
diff --git a/configs/sparsley_build.json b/configs/sparsley_build.json
new file mode 100644
index 0000000..e3bdd66
--- /dev/null
+++ b/configs/sparsley_build.json
@@ -0,0 +1,14 @@
+{
+    "width": 800,
+    "height": 600,
+    "aspect_ratio": 0.12,
+    "building_surface_fraction": 0.1,
+    "mean_height_of_roughness_elements": 6,
+    "standard_deviation_of_roughness_elements": 15.1,
+    "max_building_height": 10,
+    "min_building_height": 3,
+    "sky_view_factor": 0.9,
+    "impervious_surface_fraction": 0.1,
+    "pervious_surface_fraction": 0.7,
+    "terrain_roughness_class": 6
+}
\ No newline at end of file
diff --git a/generator.py b/generator.py
new file mode 100644
index 0000000..7cac3db
--- /dev/null
+++ b/generator.py
@@ -0,0 +1,316 @@
+import json
+import numpy as np
+from random import shuffle
+#from scipy.stats import truncnorm, beta, uniform
+import os
+import sys
+
+SURFACE = 0
+ROAD = -1
+TREE = -2
+
+ROAD_COLOR = (64, 61, 57)
+SURFACE_COLOR = (96, 108, 56)
+TREE_COLOR = (58, 90, 64)
+
+def uniform_(low, upper):
+    return uniform(low, upper - low)
+
+class Splitter:
+    def __init__(self, min_distance, min_block_width, rng):
+        self.min_distance = min_distance
+        self.min_block_width = min_block_width
+        self.rng = rng
+
+        self.split_distance = self.min_block_width * 2 + self.min_distance + 1
+
+    def gen(self, x1, y1, x2, y2):
+        self.blocks = []
+        self.gen_blocks(x1, y1, x2, y2)
+        return self.blocks
+
+    def gen_blocks(self, top_left_x, top_left_y, bottom_right_x, bottom_right_y):
+        width = bottom_right_x - top_left_x
+        height = bottom_right_y - top_left_y
+
+        if width > self.split_distance and height > self.split_distance:
+            split_x = self.rng.integers(top_left_x + self.min_block_width + 1, bottom_right_x - self.min_block_width - self.min_distance)
+            split_y = self.rng.integers(top_left_y + self.min_block_width + 1, bottom_right_y - self.min_block_width - self.min_distance)
+
+            self.gen_blocks(top_left_x, top_left_y, split_x, split_y)
+            self.gen_blocks(split_x + self.min_distance, top_left_y, bottom_right_x, split_y)
+            self.gen_blocks(top_left_x, split_y + self.min_distance, split_x, bottom_right_y)
+            self.gen_blocks(split_x + self.min_distance, split_y + self.min_distance, bottom_right_x, bottom_right_y)
+
+        elif width > self.split_distance and height >= self.min_block_width:
+            split_x = self.rng.integers(top_left_x + self.min_block_width + 1, bottom_right_x - self.min_block_width - self.min_distance)
+
+            self.gen_blocks(top_left_x, top_left_y, split_x, bottom_right_y)
+            self.gen_blocks(split_x + self.min_distance, top_left_y, bottom_right_x, bottom_right_y)
+
+        elif height > self.split_distance and width >= self.min_block_width:
+            split_y = self.rng.integers(top_left_y + self.min_block_width + 1, bottom_right_y - self.min_block_width - self.min_distance)
+
+            self.gen_blocks(top_left_x, top_left_y, bottom_right_x, split_y)
+            self.gen_blocks(top_left_x, split_y + self.min_distance, bottom_right_x, bottom_right_y)
+            
+        else: # width <= self.split_width and height <= self.split_width:
+            self.blocks.append((top_left_x, top_left_y, bottom_right_x, bottom_right_y))
+
+class LCZ:
+    def __init__(self, config_path='config.json', output_folder='.', seed=None):
+        self.rng = np.random.default_rng(seed)
+        self.config = self.load_config(config_path)
+        self.output_folder = output_folder
+
+        if not os.path.exists(output_folder):
+            os.mkdir(output_folder)
+
+        self.width = self.config['width']
+        self.height = self.config['height']
+
+        self.total_area = self.config['width'] * self.config['height']
+        self.config_building_area = self.total_area * self.config['building_surface_fraction']
+        self.mean_street_width = self.config['mean_height_of_roughness_elements'] / self.config['aspect_ratio'] # == ширина улицы, так как все улицы имеют одинаковую ширину
+
+        self.config_tree_area = self.total_area * self.config['pervious_surface_fraction']
+
+        self.min_building_height = self.config['min_building_height']
+        self.max_building_height = self.config['max_building_height']
+
+        self.possible_buildings = []
+
+        self.n_buildings = 0
+        self.real_building_area = 0
+        self.buildings = []
+
+        self.n_trees = 0
+        self.real_tree_area = 0
+        self.trees = []
+
+        self.building_heights = np.array([])
+        self.tree_heights = np.array([])
+
+        self.min_building_distance = max(int(self.mean_street_width), 1)
+        max_n_street = (self.total_area - self.config_building_area) / (self.mean_street_width * min(self.width, self.height)) - 1
+        
+        self.min_building_width =  2 * (int(self.config_building_area / (min(self.width, self.height) * max_n_street))) # int((max(self.width, self.height) - max_n_street * self.mean_street_width) / max_n_street)
+        self.split_width = self.min_building_width * 2 + self.min_building_distance + 1
+
+        self.gen_buildings_v2()
+
+    def check_params(self):
+        real_mean_height = np.mean(self.building_heights)
+        real_std_height = np.std(self.building_heights)
+        real_max_building_height = np.max(self.building_heights)
+        real_min_building_height = np.min(self.building_heights)
+
+        real = f"""
+        generated LCZ params:
+        std of height of roughness elements: {real_std_height}
+        mean height of roughness elements: {real_mean_height}
+        max height of roughness elements: {real_max_building_height}
+        min height of roughness elements: {real_min_building_height}
+        aspect ratio: {real_mean_height / self.mean_street_width}
+        building surface fraction: {self.real_building_area / self.total_area}
+        """
+
+        desired = f"""
+        desired LCZ params:
+        std of height of roughness elements: {self.config['standard_deviation_of_roughness_elements']}
+        mean height of roughness elements: {self.config['mean_height_of_roughness_elements']}
+        max height of roughness elements: {self.config['max_building_height']}
+        min height of roughness elements: {self.config['min_building_height']}
+        aspect ratio: {self.config['aspect_ratio']}
+        building surface fraction: {self.config['building_surface_fraction']}
+        """
+
+        print(real)
+        print(desired)
+
+    def gen_building_heights(self):
+        self.building_heights = self.rng.uniform(self.min_building_height, self.max_building_height - self.min_building_height, self.n_buildings)
+
+    def gen_buildings(self):
+        splitter = Splitter(self.min_building_distance, self.min_building_width, self.rng)
+        possible_buildings = splitter.gen(self.min_building_distance, self.min_building_distance, self.width - self.min_building_distance, self.height - self.min_building_distance)
+
+        shuffle(possible_buildings)
+
+        while self.real_building_area < self.config_building_area:
+            building = possible_buildings.pop()
+            self.buildings.append(building)
+
+            self.real_building_area += self.area(building)
+            self.n_buildings += 1
+
+            if not possible_buildings:
+                print("end of area for buildings")
+                break
+
+        self.gen_building_heights()
+
+    def gen_buildings_v2(self):
+        self.main_road = int((3/2) * self.min_building_distance)
+        self.road = 2 * self.min_building_distance - self.main_road
+
+        splitter_big = Splitter(self.main_road, self.width // 5, self.rng)
+
+        blocks = splitter_big.gen(self.min_building_distance, self.min_building_distance, self.width - self.min_building_distance, self.height - self.min_building_distance)
+
+        splitter_small = Splitter(self.road, self.min_building_width, self.rng)
+
+        possible_buildings = []
+
+        for block in blocks:
+            possible_buildings += splitter_small.gen(*block)
+
+        shuffle(possible_buildings)
+
+        self.possible_buildings = possible_buildings[:]
+
+        while self.real_building_area < self.config_building_area:
+            building = possible_buildings.pop()
+            self.buildings.append(building)
+
+            self.real_building_area += self.area(building)
+            self.n_buildings += 1
+
+            if not possible_buildings:
+                print("end of area for buildings")
+                break
+
+        # while self.real_tree_area < self.config_tree_area:
+        #     tree = possible_buildings.pop()
+        #     self.trees.append(tree)
+
+        #     self.real_tree_area += self.area(tree)
+        #     self.n_trees += 1
+
+        #     if not possible_buildings:
+        #         print("end of area for trees")
+        #         print(self.trees)
+        #         break
+
+        self.gen_building_heights()
+
+    def area(self, bbox):
+        return (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
+
+    def add_building(self, lcz, bbox, height=2):
+        lcz[bbox[1]:bbox[3], bbox[0]:bbox[2]] = height
+
+    def add_tree(self, lcz, bbox, height=2):
+        dx, dy = (bbox[2] - bbox[0]) / 2, (bbox[3] - bbox[1]) / 2
+        r = min([dx, dy])
+        cx = bbox[0] + dx
+        cy = bbox[1] + dy
+        yy, xx = np.mgrid[:self.height, :self.width]
+        circle = (xx - cx) ** 2 + (yy - cy) ** 2 <= r ** 2
+        lcz[circle] = height
+
+    def load_config(self, config_path):
+        return json.load(open(config_path, 'r', encoding='utf-8'))
+
+    def put_buildings(self, lcz):
+        for i in range(self.n_buildings):
+            self.add_building(lcz, self.buildings[i], self.building_heights[i])
+
+    def put_trees(self, lcz):
+        for i in range(self.n_trees):
+            self.add_tree(lcz, self.trees[i], height=16)
+
+    def to_model_input_building(self, filename='lcz.txt'):
+        lcz = self.to_height_map()
+        with open(os.path.join(self.output_folder, filename), "w", encoding='utf-8') as file:
+            file.write(f"{self.height} {self.width}\n")
+            file.write("\n".join([" ".join(list(map(str, row))) for row in lcz]))
+
+    def to_model_input_tree(self, filename='lcz.txt'):
+        lcz = self.to_height_map(False, True)
+        with open(os.path.join(self.output_folder, filename), "w", encoding='utf-8') as file:
+            file.write(f"{self.height} {self.width}\n")
+            file.write("\n".join([" ".join(list(map(str, row))) for row in lcz]))
+
+    def to_model_input_road(self, filename='lcz.txt'):
+        lcz = np.ones((self.height, self.width), dtype=int)
+        for bbox in self.possible_buildings:
+            self.add_building(lcz, bbox, 0)
+        with open(os.path.join(self.output_folder, filename), "w", encoding='utf-8') as file:
+            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)
+        if building:
+            self.put_buildings(lcz)
+        if tree:
+            self.put_trees(lcz)
+        return lcz
+
+    def model_input_to_height_map(self, filename='map.txt'):
+        height_map = np.array([list(map(int, row.split())) for row in open(filename).read().split("\n")])
+        return height_map
+
+if __name__ == '__main__':
+    lcz_types = ['compact_high_rise', 'compact_mid_rise', 'compact_low_rise', 'open_high_rise', 'open_mid_rise', 'open_low_rise', 'lightweight_low_rise', 'large_low_rise', 'sparsley_build', 'heavy_industry']
+
+    #sys.argv.append('sparsley_build')
+
+    fig, axs = plt.subplots(2, 2, figsize=(10, 6))
+
+    lcz_type = "open_mid_rise"
+
+    out_folder = "."
+
+    for i, ax in enumerate(axs.flat):
+        lcz = LCZ(config_path=f'configs/{lcz_type}.json', output_folder=out_folder)
+
+        lcz.to_model_input_building(f'lcz_{i + 1}_building.txt')
+        lcz.to_model_input_tree(f'lcz_{i + 1}_tree.txt')
+        lcz.to_model_input_road(f'lcz_{i + 1}_road.txt')
+        lcz.check_params()
+
+        height_map = lcz.to_height_map()
+        im = ax.imshow(height_map, cmap='viridis', origin='lower')
+        ax.set_title(f"{lcz_type} {i}")
+        fig.colorbar(im, ax=ax, shrink=0.3)
+
+    plt.tight_layout()
+    plt.savefig(f'{out_folder}/height_maps_mid_rise.png')
+    plt.show()
+
+
+    if False:#len(sys.argv) <= 1:
+        fig, axs = plt.subplots(2, 5, figsize=(16, 7))
+
+        for ax, lcz_type in zip(axs.flat, lcz_types):
+            print(lcz_type)
+            lcz = LCZ(config_path=f'configs/{lcz_type}.json', output_folder='v2')
+
+            # lcz.to_model_input_building(f'building_{fn}.txt')
+            # lcz.to_model_input_street(f'street_{fn}.txt')
+
+            # lcz.to_image(f'lcz_{lcz_type}_tree.png')
+            lcz.check_params()
+
+            height_map = lcz.to_height_map()
+            im = ax.imshow(height_map, cmap='viridis', origin='lower')
+            ax.set_title(lcz_type)
+            fig.colorbar(im, ax=ax, shrink=0.3)
+
+        plt.tight_layout()
+        plt.savefig('height_maps_main_road.png')
+        plt.show()
+
+    if len(sys.argv) > 1:
+        lcz_type = sys.argv[1]
+
+        if lcz_type not in lcz_types:
+            lcz_type = lcz_types[0]
+
+        lcz = LCZ(config_path=f'configs/{lcz_type}.json', output_folder='v2')
+
+        lcz.to_image(f'lcz_{lcz_type}_tree.png')
+        lcz.check_params()
+
diff --git a/prerun.py b/prerun.py
index ad253af..80a1640 100644
--- a/prerun.py
+++ b/prerun.py
@@ -7,4 +7,4 @@ subprocess.run(['g++', '-std=c++11', '-Wall', '-DEXCLUDE_GPU_BRANCH', '-c', 'con
 subprocess.run(['g++', '-std=c++11', '-Wall', '-DEXCLUDE_GPU_BRANCH', '-c', 'cfg-var.cpp', '-o', 'cfg-var.o'])
 subprocess.run(['g++', '-std=c++11', '-Wall', '-DEXCLUDE_GPU_BRANCH', '-c', 'str-com.cpp', '-o', 'str-com.o'])
 subprocess.run(['g++', '-std=c++11', '-Wall', '-DEXCLUDE_GPU_BRANCH', '-o', 'parser-cli', 'parser-cli.o', 'config-parser.o', 'cfg-var.o', 'str-com.o'])
-os.chdir('..')
\ No newline at end of file
+os.chdir('..')
diff --git a/styles.py b/styles.py
index eb4d17c..fcb7861 100644
--- a/styles.py
+++ b/styles.py
@@ -1,66 +1,106 @@
+from enum import Enum
+
 import dearpygui.dearpygui as dpg
 
 FONT_PATH = "fonts\\Montserrat-Medium.ttf"
+ICON_PATH = "icons\\icon.ico"
 FONT_SIZE = 24
 ACCENT_COLOR = (0, 119, 200, 100)
 ROUNDING = 10
 SPACE = 10
 INDENT = 40
-ICON_PATH = "icons\\icon.ico"
 BORDER = 3
 
+TEXT = (0, 1)
+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)
+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_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 = {
+    'TEXT': (255, 255, 255, 255),
+    'PRIMARY': (37, 37, 38, 255),
+    'SECONDARY': (51, 51, 55, 255),
+    'ACCENT': (0, 119, 200, 153)
+}
+
+ORANGE = {
+    'TEXT': (255, 255, 255, 255),
+    'PRIMARY': (37, 37, 38, 255),
+    'SECONDARY': (51, 51, 55, 255),
+    'ACCENT': (251, 133, 0, 153)
+}
+
+def apply_alpha(color, divisor=2):
+    r, g, b, a = color
+    return (r, g, b, a / divisor)
+
+def add_colors_to_theme(colors, theme):
+    with dpg.theme_component(dpg.mvAll, parent=theme):
+        for color_type in COLOR_TYPE_TO_DPG_ITEM:
+            for item in COLOR_TYPE_TO_DPG_ITEM[color_type]:
+                color = colors[color_type]
+                if item in APPLY_ALPHA:
+                    color = apply_alpha(color)
+                dpg.add_theme_color(item, color, category=dpg.mvThemeCat_Core)
+
+def add_styles_to_theme(theme):
+    rounding = (
+        dpg.mvStyleVar_ChildRounding,
+        dpg.mvStyleVar_WindowRounding,
+        dpg.mvStyleVar_FrameRounding,
+        dpg.mvStyleVar_TabRounding,
+        dpg.mvStyleVar_GrabRounding,
+        dpg.mvStyleVar_ScrollbarRounding,
+        dpg.mvStyleVar_PopupRounding
+    )
+
+    spacing = (
+        dpg.mvStyleVar_ItemSpacing,
+        dpg.mvStyleVar_ItemInnerSpacing,
+        dpg.mvStyleVar_WindowPadding,
+        dpg.mvStyleVar_FramePadding,
+    )
+
+    with dpg.theme_component(dpg.mvAll, parent=theme):
+        for item in rounding:
+            dpg.add_theme_style(item, ROUNDING, category=dpg.mvThemeCat_Core)
+        
+        for item in spacing:
+            dpg.add_theme_style(item, SPACE, SPACE, category=dpg.mvThemeCat_Core)
+
+        dpg.add_theme_style(dpg.mvStyleVar_ChildBorderSize, BORDER, category=dpg.mvThemeCat_Core)
+        dpg.add_theme_style(dpg.mvStyleVar_WindowBorderSize, 0, category=dpg.mvThemeCat_Core)
+        dpg.add_theme_style(dpg.mvStyleVar_FrameBorderSize, 0, category=dpg.mvThemeCat_Core)
+
+
 class GuiStyle:
     def __init__(self):
         self.font_registry = dpg.add_font_registry()
         self.montserrat_font = dpg.add_font(FONT_PATH, FONT_SIZE, parent=self.font_registry)
 
-        self.main_theme = dpg.add_theme()
-        theme_all = dpg.add_theme_component(dpg.mvAll, parent=self.main_theme, tag="theme_all")
-        
-        accents = (
-            dpg.mvThemeCol_Border,
-            dpg.mvThemeCol_CheckMark,
-            dpg.mvThemeCol_SliderGrab,
-            dpg.mvThemeCol_ScrollbarGrab,
-            dpg.mvThemeCol_TabActive,
-            dpg.mvThemeCol_ButtonActive,
-            dpg.mvThemeCol_Separator,
-            dpg.mvThemeCol_ResizeGripActive
-        )
-
-        rounding = (
-            dpg.mvStyleVar_ChildRounding,
-            dpg.mvStyleVar_WindowRounding,
-            dpg.mvStyleVar_FrameRounding,
-            dpg.mvStyleVar_TabRounding,
-            dpg.mvStyleVar_GrabRounding,
-            dpg.mvStyleVar_ScrollbarRounding,
-            dpg.mvStyleVar_PopupRounding
-        )
-
-        spacing = (
-            dpg.mvStyleVar_ItemSpacing,
-            dpg.mvStyleVar_ItemInnerSpacing,
-            dpg.mvStyleVar_WindowPadding,
-            dpg.mvStyleVar_FramePadding,
-        )
-
-        for item in accents:
-            dpg.add_theme_color(item, ACCENT_COLOR, category=dpg.mvThemeCat_Core, parent=theme_all)
-
-        # dpg.add_theme_color(dpg.mvThemeCol_Header, (255, 0, 0), category=dpg.mvThemeCat_Core, parent=theme_all)
-        # dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (255, 0, 0), category=dpg.mvThemeCat_Core, parent=theme_all)
-        # dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (255, 0, 0), category=dpg.mvThemeCat_Core, parent=theme_all)
+        self.fonts = {
+            'montserrat': self.montserrat_font
+        }
 
+        self.themes = {
+            'blue': BLUE,
+            'orange': ORANGE
+        }
 
-        for item in rounding:
-            dpg.add_theme_style(item, ROUNDING, category=dpg.mvThemeCat_Core, parent=theme_all)
-        
-        for item in spacing:
-            dpg.add_theme_style(item, SPACE, SPACE, category=dpg.mvThemeCat_Core, parent=theme_all)
+        self.dpg_themes = dict()
+
+        for theme_name in self.themes:
+            theme = dpg.add_theme()
+            add_styles_to_theme(theme)
+            add_colors_to_theme(self.themes[theme_name], theme)
+            self.dpg_themes[theme_name] = theme
+
+    def apply_font(self, font):
+        dpg.bind_font(self.fonts[font])
 
-        dpg.add_theme_style(dpg.mvStyleVar_IndentSpacing, 30, category=dpg.mvThemeCat_Core, parent=theme_all)
-        dpg.add_theme_style(dpg.mvStyleVar_ChildBorderSize, BORDER, category=dpg.mvThemeCat_Core, parent=theme_all)
-        dpg.add_theme_style(dpg.mvStyleVar_WindowBorderSize, 0, category=dpg.mvThemeCat_Core, parent=theme_all)
-        dpg.add_theme_style(dpg.mvStyleVar_FrameBorderSize, 0, category=dpg.mvThemeCat_Core, parent=theme_all)
-        dpg.add_theme_style(dpg.mvStyleVar_WindowMinSize, 200, 200, category=dpg.mvThemeCat_Core, parent=theme_all)
\ No newline at end of file
+    def apply_theme(self, theme):
+        print('appling', theme)
+        dpg.bind_theme(self.dpg_themes[theme])
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..78dc762
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,79 @@
+import dearpygui.dearpygui as dpg
+import numpy as np
+
+from styles import SPACE
+
+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")
+
+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 get_absolute_y(item):
+    y = dpg.get_item_pos(item)[1]
+    parent = dpg.get_item_parent(item)
+    while parent:
+        if dpg.get_item_type(parent) == 'mvAppItemType::mvChildWindow':
+            y += dpg.get_item_pos(parent)[1]
+        parent = dpg.get_item_parent(parent)
+    return y
+
+def get_item_level(item):
+    level = 0
+    parent = dpg.get_item_parent(item)
+    while parent:
+        if dpg.get_item_type(parent) == 'mvAppItemType::mvTabBar':
+            level += 1
+        parent = dpg.get_item_parent(parent)
+    return level
+
+def does_tab_bar_child_exist(item):
+    for uuid in dpg.get_item_children(item, 1):
+        if dpg.get_item_type(uuid) == "mvAppItemType::mvTabBar":
+            return True, uuid
+    return False, None
+
+def path_iter(path, prefix=""):
+    namespaces = path.split('.')
+    for i in range(len(namespaces)):
+        yield prefix + '.' + '.'.join(namespaces[:i+1])
+
+def combo_menu(callback):
+    def func(s, a, u):
+        for child in dpg.get_item_children(dpg.get_item_parent(s), 1):
+            if dpg.get_item_alias(child) != dpg.get_item_alias(s):
+                dpg.set_value(child, False)
+            else:
+                dpg.set_value(child, True)
+        dpg.set_item_user_data(s, dpg.get_item_alias(s))
+        callback(s, a, u)
+    return func
\ No newline at end of file
-- 
GitLab