geospatial-ai-tool 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. geospatial_ai_tool-0.1.0/.gitignore +88 -0
  2. geospatial_ai_tool-0.1.0/GeoAI_App.spec +61 -0
  3. geospatial_ai_tool-0.1.0/PKG-INFO +20 -0
  4. geospatial_ai_tool-0.1.0/README.md +1 -0
  5. geospatial_ai_tool-0.1.0/environment.yml +46 -0
  6. geospatial_ai_tool-0.1.0/pyproject.toml +40 -0
  7. geospatial_ai_tool-0.1.0/src/impr_app/__init__.py +0 -0
  8. geospatial_ai_tool-0.1.0/src/impr_app/core/app_shell.py +323 -0
  9. geospatial_ai_tool-0.1.0/src/impr_app/core/models.py +70 -0
  10. geospatial_ai_tool-0.1.0/src/impr_app/impr_app.py +48 -0
  11. geospatial_ai_tool-0.1.0/src/impr_app/pages/base_page.py +51 -0
  12. geospatial_ai_tool-0.1.0/src/impr_app/pages/home_page.py +282 -0
  13. geospatial_ai_tool-0.1.0/src/impr_app/pages/output_page.py +621 -0
  14. geospatial_ai_tool-0.1.0/src/impr_app/pages/page_2d.py +668 -0
  15. geospatial_ai_tool-0.1.0/src/impr_app/pages/page_3d.py +224 -0
  16. geospatial_ai_tool-0.1.0/src/impr_app/pages/settings_page.py +89 -0
  17. geospatial_ai_tool-0.1.0/src/impr_app/scripts_2d/DOE_ANN_v2/doe_tiff/__init__.py +19 -0
  18. geospatial_ai_tool-0.1.0/src/impr_app/scripts_2d/DOE_ANN_v2/doe_tiff/doe_kernel.py +97 -0
  19. geospatial_ai_tool-0.1.0/src/impr_app/scripts_2d/DOE_ANN_v2/doe_tiff/io.py +68 -0
  20. geospatial_ai_tool-0.1.0/src/impr_app/scripts_2d/DOE_ANN_v2/predict3.py +169 -0
  21. geospatial_ai_tool-0.1.0/src/impr_app/scripts_2d/DOE_ANN_v2/stack_rasters_for_geoai.py +205 -0
  22. geospatial_ai_tool-0.1.0/src/impr_app/scripts_3d/threed.py +182 -0
  23. geospatial_ai_tool-0.1.0/src/impr_app/services/geoai_metrics.py +709 -0
  24. geospatial_ai_tool-0.1.0/src/impr_app/services/raster_processing.py +293 -0
  25. geospatial_ai_tool-0.1.0/src/impr_app/services/script_runners.py +449 -0
  26. geospatial_ai_tool-0.1.0/src/impr_app/ui_helpers.py +96 -0
  27. geospatial_ai_tool-0.1.0/tifviewer.py +41 -0
@@ -0,0 +1,88 @@
1
+ # =========================
2
+ # Environments
3
+ # =========================
4
+ .venv/
5
+ venv/
6
+ env/
7
+ .env/
8
+ .conda/
9
+ miniconda3/
10
+ anaconda3/
11
+
12
+ # =========================
13
+ # Python Cache & Build
14
+ # =========================
15
+ __pycache__/
16
+ *.py[cod]
17
+ *$py.class
18
+ build/
19
+ develop-eggs/
20
+ dist/
21
+ downloads/
22
+ eggs/
23
+ .eggs/
24
+ parts/
25
+ sdist/
26
+ var/
27
+ wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # =========================
33
+ # OS & Editor Files
34
+ # =========================
35
+ .DS_Store
36
+ .vscode/
37
+ .idea/
38
+ *.swp
39
+
40
+ # =========================
41
+ # Databases & Local State
42
+ # =========================
43
+ *.db
44
+ *.sqlite3
45
+ .jupyter/
46
+ .pytest_cache/
47
+
48
+ # =========================
49
+ # Data & Large Files
50
+ # =========================
51
+ # Ignore local raster/geospatial files and datasets
52
+ *.tif
53
+ *.tiff
54
+ *.grd
55
+ *.gri
56
+ *.hdr
57
+ *.nc
58
+ *.sdat
59
+ *.db
60
+ *.cpg
61
+ *.prj
62
+ *.dbf
63
+ *.sbn
64
+ *.shp
65
+ *.shx
66
+ *.sbx
67
+ *.xml
68
+ *.mgrd
69
+ *.sgrd
70
+ *.aux
71
+ *.kmz
72
+ data/
73
+ temp/
74
+
75
+ # =========================
76
+ # Secrets & Env Variables
77
+ # =========================
78
+ .env
79
+
80
+ # =========================
81
+ # Project Specifics
82
+ # =========================
83
+ GeoAI_Outputs/
84
+ saved_models.json
85
+ Pretrained_Brady_model/
86
+ 3D_AI/
87
+ MYLES_2D_AI_MODELS_CLEAN/
88
+ training_stacks/
@@ -0,0 +1,61 @@
1
+ # -*- mode: python ; coding: utf-8 -*-
2
+ from PyInstaller.utils.hooks import collect_all
3
+
4
+ datas = []
5
+ binaries = []
6
+ hiddenimports = []
7
+ tmp_ret = collect_all('customtkinter')
8
+ datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
9
+ tmp_ret = collect_all('tensorflow')
10
+ datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
11
+ tmp_ret = collect_all('rasterio')
12
+ datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
13
+
14
+
15
+ a = Analysis(
16
+ ['impr_app.py'],
17
+ pathex=[],
18
+ binaries=binaries,
19
+ datas=datas,
20
+ hiddenimports=hiddenimports,
21
+ hookspath=[],
22
+ hooksconfig={},
23
+ runtime_hooks=[],
24
+ excludes=[],
25
+ noarchive=False,
26
+ optimize=0,
27
+ )
28
+ pyz = PYZ(a.pure)
29
+
30
+ exe = EXE(
31
+ pyz,
32
+ a.scripts,
33
+ [],
34
+ exclude_binaries=True,
35
+ name='GeoAI_App',
36
+ debug=False,
37
+ bootloader_ignore_signals=False,
38
+ strip=False,
39
+ upx=True,
40
+ console=False,
41
+ disable_windowed_traceback=False,
42
+ argv_emulation=False,
43
+ target_arch=None,
44
+ codesign_identity=None,
45
+ entitlements_file=None,
46
+ )
47
+ coll = COLLECT(
48
+ exe,
49
+ a.binaries,
50
+ a.datas,
51
+ strip=False,
52
+ upx=True,
53
+ upx_exclude=[],
54
+ name='GeoAI_App',
55
+ )
56
+ app = BUNDLE(
57
+ coll,
58
+ name='GeoAI.app',
59
+ icon="MyIcon.icns",
60
+ bundle_identifier=None,
61
+ )
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.5
2
+ Name: geospatial-ai-tool
3
+ Version: 0.1.0
4
+ Summary: 2D and 3D geospatial AI model prediction tool.
5
+ Requires-Python: ==3.10.*
6
+ Requires-Dist: customtkinter
7
+ Requires-Dist: geopandas
8
+ Requires-Dist: matplotlib
9
+ Requires-Dist: numpy
10
+ Requires-Dist: pandas
11
+ Requires-Dist: pillow
12
+ Requires-Dist: pyshortcuts
13
+ Requires-Dist: rasterio
14
+ Requires-Dist: scikit-learn
15
+ Requires-Dist: tensorflow==2.21.0
16
+ Requires-Dist: tkinterdnd2
17
+ Requires-Dist: tqdm
18
+ Description-Content-Type: text/markdown
19
+
20
+ empty lol
@@ -0,0 +1 @@
1
+ empty lol
@@ -0,0 +1,46 @@
1
+ name: geo_app
2
+ channels:
3
+ - conda-forge
4
+ dependencies:
5
+ - python=3.10
6
+ - numpy
7
+ - tqdm
8
+ - pillow
9
+ - geopandas
10
+ - tk
11
+ - pandas
12
+ - matplotlib
13
+ - gdal
14
+ - rasterio
15
+ - scikit-learn
16
+ - pip:
17
+ - absl-py==2.5.0
18
+ - altgraph==0.17.5
19
+ - astunparse==1.6.3
20
+ - customtkinter==6.0.0
21
+ - darkdetect==0.8.0
22
+ - flatbuffers==25.12.19
23
+ - gast==0.7.0
24
+ - google-pasta==0.2.0
25
+ - grpcio==1.83.1
26
+ - h5py==3.14.0
27
+ - keras==3.12.4
28
+ - libclang==18.1.1
29
+ - macholib==1.16.4
30
+ - markdown-it-py==4.2.0
31
+ - mdurl==0.1.2
32
+ - ml-dtypes==0.6.0
33
+ - namex==0.1.0
34
+ - opt-einsum==3.4.0
35
+ - optree==0.20.0
36
+ - protobuf==7.36.1
37
+ - pygments==2.21.0
38
+ - pyinstaller==6.22.2
39
+ - pyinstaller-hooks-contrib==2026.7
40
+ - rich==15.0.0
41
+ - tensorflow==2.21.0
42
+ - termcolor==3.3.0
43
+ - tkinterdnd2==0.6.3
44
+ - typing-extensions==4.16.0
45
+ - wrapt==2.4.0
46
+ prefix: /opt/homebrew/Caskroom/miniforge/base/envs/my_app
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "geospatial-ai-tool" # The pip/uv install name (must be unique on PyPI)
7
+ version = "0.1.0"
8
+ description = "2D and 3D geospatial AI model prediction tool."
9
+ readme = "README.md"
10
+ requires-python = "==3.10.*"
11
+ dependencies = [
12
+ # Core Application
13
+ "customtkinter",
14
+ "pyshortcuts",
15
+
16
+ # Data & Machine Learning
17
+ "numpy",
18
+ "pandas",
19
+ "scikit-learn",
20
+ "tensorflow==2.21.0", # Pinned to the version you are currently using
21
+
22
+ # Geospatial
23
+ "geopandas",
24
+ "rasterio",
25
+
26
+ # Utilities
27
+ "matplotlib",
28
+ "pillow",
29
+ "tqdm",
30
+
31
+ "tkinterdnd2"
32
+ ]
33
+
34
+ [project.scripts]
35
+ # This creates the terminal command.
36
+ # Format: terminal-command = "module.file:function"
37
+ run-geospatial = "impr_app.impr_app:main"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/impr_app"]
File without changes
@@ -0,0 +1,323 @@
1
+ # core/app_shell.py
2
+ import os
3
+ import queue
4
+ import shutil
5
+ import tkinter as tk
6
+ from tkinter import messagebox
7
+
8
+ import customtkinter as ctk
9
+ from impr_app.core.models import SavedModelLibrary
10
+ from impr_app.pages.home_page import HomePage
11
+ from impr_app.pages.output_page import OutputPage
12
+ from impr_app.pages.page_2d import Page2D
13
+ from impr_app.pages.page_3d import Page3D
14
+ from impr_app.pages.settings_page import SettingsPage
15
+ from tkinterdnd2 import DND_FILES, TkinterDnD
16
+
17
+
18
+ class DnDCTk(ctk.CTk, TkinterDnD.DnDWrapper):
19
+ """Enables drag and drop feature for the UI as a whole"""
20
+
21
+ def __init__(self, *args, **kwargs):
22
+ """Initializes custom tkinter with drag and drop tkinter"""
23
+ super().__init__(*args, **kwargs)
24
+ self.TkdndVersion = TkinterDnD._require(self)
25
+
26
+
27
+ class App(DnDCTk):
28
+ """Main ctk controller that manages generic settings like app width as
29
+ well as the generation of pages and switching between them.
30
+ """
31
+
32
+ def __init__(self):
33
+ """Builds window, initializes queue, activates drag and drop, builds pages
34
+ and shows the home page.
35
+ """
36
+ self.THEMES = {
37
+ # ---------------------------------------------------------
38
+ # DEFAULT: Your current earth-toned & navy palette
39
+ # ---------------------------------------------------------
40
+ "Earth-Toned": {
41
+ "bg": "#EDF1F5",
42
+ "card_bg": "#FFFFFF",
43
+ "border": "#D5DBE1",
44
+ "text_primary": "#182A45",
45
+ "text_secondary": "#63758A",
46
+ "text_muted": "#4F6075",
47
+ "text_inverse": "white",
48
+ "text_input": "black",
49
+ "text_placeholder": "#8A9BB0",
50
+ "badge_bg": "#8F7870",
51
+ "input_bg": "#F8FAFC",
52
+ "btn_primary": "#D4B592",
53
+ "btn_primary_hover": "#C0A07B",
54
+ "btn_secondary_bg": "#FFFFFF",
55
+ "btn_secondary_hover": "#F0F4F8",
56
+ "btn_accent": "#F2C94C",
57
+ "btn_accent_hover": "#D4AF37",
58
+ "btn_dropdown_hover": "#7A654E",
59
+ },
60
+ # ---------------------------------------------------------
61
+ # DEUTERANOPIA (Red/Green weak - Most Common)
62
+ # Replaces warm earth tones with distinct cool blues and bright oranges.
63
+ # ---------------------------------------------------------
64
+ "Default (deuteranopia)": {
65
+ "bg": "#EDF1F5",
66
+ "card_bg": "#FFFFFF",
67
+ "border": "#D5DBE1",
68
+ "text_primary": "#182A45",
69
+ "text_secondary": "#63758A",
70
+ "text_muted": "#4F6075",
71
+ "text_inverse": "white",
72
+ "text_input": "black",
73
+ "text_placeholder": "#8A9BB0",
74
+ "badge_bg": "#0072B2", # Replaced warm brown with Dark Blue
75
+ "input_bg": "#F8FAFC",
76
+ "btn_primary": "#56B4E9", # Replaced Tan with Sky Blue
77
+ "btn_primary_hover": "#3D8CBB",
78
+ "btn_secondary_bg": "#FFFFFF",
79
+ "btn_secondary_hover": "#F0F4F8",
80
+ "btn_accent": "#E69F00", # Replaced Gold with High-Vis Orange
81
+ "btn_accent_hover": "#C28600",
82
+ "btn_dropdown_hover": "#005A8E",
83
+ },
84
+ # ---------------------------------------------------------
85
+ # PROTANOPIA (Red-blind)
86
+ # Similar to Deuteranopia, but avoids dark reds/browns which appear black.
87
+ # ---------------------------------------------------------
88
+ "protanopia": {
89
+ "bg": "#EDF1F5",
90
+ "card_bg": "#FFFFFF",
91
+ "border": "#D5DBE1",
92
+ "text_primary": "#182A45",
93
+ "text_secondary": "#63758A",
94
+ "text_muted": "#4F6075",
95
+ "text_inverse": "white",
96
+ "text_input": "black",
97
+ "text_placeholder": "#8A9BB0",
98
+ "badge_bg": "#E69F00", # Replaced warm brown with Bright Orange
99
+ "input_bg": "#F8FAFC",
100
+ "btn_primary": "#0072B2", # Replaced Tan with deep, visible Blue
101
+ "btn_primary_hover": "#005A8E",
102
+ "btn_secondary_bg": "#FFFFFF",
103
+ "btn_secondary_hover": "#F0F4F8",
104
+ "btn_accent": "#F0E442", # Replaced Gold with brighter Yellow
105
+ "btn_accent_hover": "#D9CE36",
106
+ "btn_dropdown_hover": "#00456E",
107
+ },
108
+ # ---------------------------------------------------------
109
+ # TRITANOPIA (Blue/Yellow blind - Rare)
110
+ # Strips out the Navy Blue and Gold completely.
111
+ # Uses Charcoal for text, Vermilion (Red) and Teal for actions.
112
+ # ---------------------------------------------------------
113
+ "tritanopia": {
114
+ "bg": "#F4F4F4", # Shifted away from cool blue-grey
115
+ "card_bg": "#FFFFFF",
116
+ "border": "#D0D0D0",
117
+ "text_primary": "#2C2C2C", # Shifted Navy to neutral Charcoal
118
+ "text_secondary": "#5A5A5A", # Shifted Slate to neutral Grey
119
+ "text_muted": "#4A4A4A",
120
+ "text_inverse": "white",
121
+ "text_input": "black",
122
+ "text_placeholder": "#999999",
123
+ "badge_bg": "#CC79A7", # Reddish-purple (very clear for Tritanopes)
124
+ "input_bg": "#FAFAFA",
125
+ "btn_primary": "#D55E00", # Vermilion (Red-Orange) replaces Tan
126
+ "btn_primary_hover": "#A34800",
127
+ "btn_secondary_bg": "#FFFFFF",
128
+ "btn_secondary_hover": "#EEEEEE",
129
+ "btn_accent": "#009E73", # Teal/Green replaces Gold
130
+ "btn_accent_hover": "#007A59",
131
+ "btn_dropdown_hover": "#7A3500",
132
+ },
133
+ }
134
+
135
+ super().__init__()
136
+ self.theme_colors = self.THEMES["Default (deuteranopia)"]
137
+ self.configure(fg_color=self.theme_colors["bg"])
138
+ self.current_scale = 1.2
139
+ screen_width = self.winfo_screenwidth()
140
+ screen_height = self.winfo_screenheight()
141
+ # UI Looks better when using the entire screen, also gives images
142
+ # more space
143
+ self.geometry(f"{screen_width}x{screen_height}")
144
+ self.title("GEO AI for Geothermal Exploration")
145
+
146
+ # Multithreading is used so the UI does not freeze during processing,
147
+ # which makes the queue necessary for thread safety
148
+ self.log_queue = queue.Queue()
149
+ self.model_library = SavedModelLibrary("DEADMEAT")
150
+ self.current_probability_map = None
151
+
152
+ self.container = ctk.CTkFrame(self)
153
+ self.container = ctk.CTkFrame(self, fg_color="transparent")
154
+ self.container.pack(fill="both", expand=True)
155
+
156
+ self.pages = {}
157
+ self.active_page = None
158
+
159
+ self._build_pages()
160
+
161
+ self.drop_target_register(DND_FILES)
162
+ self.dnd_bind("<<Drop>>", self._handle_drop)
163
+
164
+ self.show_page("HomePage")
165
+ self.apply_scaling(self.current_scale)
166
+ self.after(100, self.check_queue)
167
+
168
+ self.saved_files = []
169
+ self.protocol("WM_DELETE_WINDOW", self.on_closing)
170
+
171
+ def _build_pages(self):
172
+ """Loops through every provided page class, creates an object
173
+ for each page, and saves those objects to the pages dict.
174
+ """
175
+ for PageClass in (HomePage, Page2D, Page3D, OutputPage, SettingsPage):
176
+ page = PageClass(self.container, controller=self)
177
+ self.pages[PageClass.__name__] = page
178
+
179
+ def show_page(self, page_name):
180
+ """Shows the desired page while hiding the others.
181
+
182
+ Args:
183
+ page_name (str): key for dictionary containing corresponding
184
+ homepage object.
185
+ """
186
+ # Unpacking pages instead of hiding them for improved efficiency
187
+ for page in self.pages.values():
188
+ page.pack_forget()
189
+ page = self.pages[page_name]
190
+ page.pack(fill="both", expand=True)
191
+ self.active_page = page
192
+ page.on_show()
193
+ self.update_idletasks()
194
+
195
+ def check_queue(self):
196
+ """Loops through queue to display messages to app console safely.
197
+ Continuously calls itself.
198
+ """
199
+ while not self.log_queue.empty():
200
+ message = self.log_queue.get()
201
+ # This prevents another thread other than the UI thread from
202
+ # trying to manipulate the log box
203
+ self.active_page.append_log(message)
204
+ self.after(100, self.check_queue)
205
+
206
+ def _handle_drop(self, event):
207
+ """Checks if a page uses drag and drop, if it does
208
+ has page handle the drop
209
+
210
+ Args:
211
+ event (tkinter.Event): Contains data about the drag and drop action
212
+ """
213
+
214
+ # Not all pages in the application use drag and drop
215
+ if hasattr(self.active_page, "handle_drop"):
216
+ self.active_page.handle_drop(event)
217
+
218
+ def change_theme(self, new_theme_name):
219
+ # Store the old colors before we update so we can map them
220
+ old_colors = self.theme_colors
221
+
222
+ # Update to the new colors
223
+ self.current_theme_name = new_theme_name
224
+ self.theme_colors = self.THEMES[new_theme_name]
225
+
226
+ for page in self.pages.values():
227
+ page.colors = self.theme_colors
228
+
229
+ # Update the main window background
230
+ self.configure(fg_color=self.theme_colors["bg"])
231
+
232
+ # Start the recursion, passing both old and new dictionaries
233
+ self._update_widget_tree(self, old_colors, self.theme_colors)
234
+
235
+ def _update_widget_tree(self, parent_widget, old_colors, new_colors):
236
+ """Recursively checks and updates every widget."""
237
+ for widget in parent_widget.winfo_children():
238
+ # Update the 4 color properties CTk uses in your UI
239
+ self._swap_color(widget, "fg_color", old_colors, new_colors)
240
+ self._swap_color(widget, "text_color", old_colors, new_colors)
241
+ self._swap_color(widget, "border_color", old_colors, new_colors)
242
+ self._swap_color(widget, "hover_color", old_colors, new_colors)
243
+ self._swap_color(widget, "button_color", old_colors, new_colors)
244
+ self._swap_color(widget, "button_hover_color", old_colors, new_colors)
245
+
246
+ # Recurse deeper if this widget has children
247
+ if widget.winfo_children():
248
+ self._update_widget_tree(widget, old_colors, new_colors)
249
+
250
+ def _swap_color(self, widget, property_name, old_colors, new_colors):
251
+ """Looks at a widget's current hex code and maps it to the new theme."""
252
+ try:
253
+ # cget() grabs the current value of a widget property
254
+ current_color = widget.cget(property_name)
255
+
256
+ # CTk sometimes returns colors as a list/tuple for light/dark mode (e.g. ['#FFF', '#000'])
257
+ # We just need to check the string representation
258
+ if isinstance(current_color, (list, tuple)):
259
+ current_color = current_color[0]
260
+
261
+ # Never alter transparent widgets
262
+ if str(current_color).lower() == "transparent":
263
+ return
264
+
265
+ # Find this color in the OLD dictionary, apply from the NEW dictionary
266
+ for key, old_hex in old_colors.items():
267
+ if str(current_color).lower() == str(old_hex).lower():
268
+ # We found a match! Apply the new hex code for this key.
269
+ # **{} is just dynamically unpacking the property name (e.g. fg_color="#NEW_HEX")
270
+ widget.configure(**{property_name: new_colors[key]})
271
+ return # Stop searching once we find a match
272
+
273
+ except (ValueError, tk.TclError, AttributeError):
274
+ # Pass gracefully if the widget doesn't support the property we are checking
275
+ pass
276
+
277
+ def update_scaling(self, new_scale):
278
+ self.current_scale = new_scale
279
+
280
+ self.apply_scaling(new_scale)
281
+
282
+ def apply_scaling(self, scale_factor):
283
+ ctk.set_widget_scaling(scale_factor)
284
+
285
+ # Get the current dimensions of the window
286
+ current_width = self.winfo_width()
287
+ current_height = self.winfo_height()
288
+
289
+ # Expand the window by 1 pixel to force a hard OS-level redraw
290
+ self.geometry(f"{current_width + 1}x{current_height}")
291
+ self.update()
292
+
293
+ # Snap it instantly back to the correct size
294
+ self.geometry(f"{current_width}x{current_height}")
295
+
296
+ def on_closing(self):
297
+ # Trigger the pop-up when the window close is attempted
298
+ # askyesnocancel returns: True (Yes), False (No), or None (Cancel)
299
+ response = messagebox.askyesnocancel(
300
+ title="Save before closing?",
301
+ message="Would you like to save your generated files before exiting?",
302
+ )
303
+
304
+ # Handle the user's choice
305
+ if response:
306
+ self.destroy()
307
+
308
+ elif response is False:
309
+ for dir_path in self.saved_files:
310
+ if os.path.exists(dir_path):
311
+ shutil.rmtree(dir_path)
312
+ self.destroy()
313
+
314
+ def register_saved_file(self, filepath):
315
+ """Safely appends a newly generated file path to the saved files list."""
316
+ if filepath not in self.saved_files:
317
+ self.saved_files.append(filepath)
318
+ print(f"Tracking new file: {filepath}") # Optional: for debugging
319
+
320
+
321
+ if __name__ == "__main__":
322
+ app = App()
323
+ app.mainloop()
@@ -0,0 +1,70 @@
1
+ # core/models.py
2
+ import json
3
+ from pathlib import Path
4
+
5
+
6
+ class SavedModelLibrary:
7
+ """Manages the saving, loading, and deletion of user-imported AI models
8
+ to a local JSON file so they persist between app sessions.
9
+ """
10
+
11
+ def __init__(self, memory_file_path: str) -> None:
12
+ """Initializes saved model library with memory file path
13
+
14
+ Args:
15
+ memory_file_path (str): raw relative file path to the applications saved memory.
16
+ """
17
+ memory_file_path = Path.home() / "Documents"
18
+ print(memory_file_path)
19
+
20
+ def set_memory_file(self, memory_file_path: str) -> None:
21
+ self.memory_file = Path(memory_file_path) / "saved_models.json"
22
+ if not self.memory_file.exists():
23
+ self.memory_file.write_text("{}")
24
+
25
+ def get_all_models(self) -> dict:
26
+ """Loads and returns the dictionary of saved models.
27
+
28
+ Returns:
29
+ dict: A dictionary containing model names as keys which point to their file paths or
30
+ an empty dictionary if file DNE or is corrupted
31
+ """
32
+ if self.memory_file.exists():
33
+ try:
34
+ with open(self.memory_file, "r") as f:
35
+ return json.load(f)
36
+ except json.JSONDecodeError:
37
+ return {}
38
+ return {}
39
+
40
+ def _save_models(self, models_dict: dict) -> None:
41
+ """Helper function that saves the models in dictionary to json file
42
+
43
+ Args:
44
+ models_dict (dict): A dictionary containing user generated label
45
+ and filepath to model.
46
+ """
47
+ with open(self.memory_file, "w") as f:
48
+ json.dump(models_dict, f, indent=4)
49
+
50
+ def add_model(self, name: str, filepath: str | Path) -> None:
51
+ """Adds a GeoAI model to the UI as a selectable choice
52
+
53
+ Args:
54
+ name (str): The name a user has given the model
55
+ filepath (str | Path): The filepath to the model they selected
56
+ """
57
+ models = self.get_all_models()
58
+ models[name] = str(filepath)
59
+ self._save_models(models)
60
+
61
+ def delete_model(self, name) -> None:
62
+ """Deletes model from working dictionary (if exists) and updates the json model data
63
+
64
+ Args:
65
+ name (str): Name the user generated for a specific model
66
+ """
67
+ models = self.get_all_models()
68
+ if name in models:
69
+ del models[name]
70
+ self._save_models(models)