|
14 | 14 | @author Linda Karlovska <linda.karlovska seznam.cz> |
15 | 15 | """ |
16 | 16 |
|
17 | | -import json |
18 | | -import subprocess |
19 | | -import threading |
20 | | -from pathlib import Path |
21 | | - |
22 | 17 | import wx |
23 | 18 |
|
24 | 19 | try: |
25 | 20 | import wx.html2 as html # wx.html2 is available in wxPython 4.0 and later |
26 | 21 | except ImportError as e: |
27 | 22 | raise RuntimeError(_("wx.html2 is required for Jupyter integration.")) from e |
28 | 23 |
|
29 | | -import grass.script as gs |
30 | | -import grass.jupyter as gj |
31 | | - |
32 | 24 | from main_window.page import MainPageBase |
33 | 25 |
|
| 26 | +from grass.notebooks.launcher import NotebookServerManager |
| 27 | +from grass.notebooks.directory import NotebookDirectoryManager |
| 28 | + |
34 | 29 |
|
35 | 30 | class JupyterPanel(wx.Panel, MainPageBase): |
36 | 31 | def __init__( |
@@ -60,177 +55,70 @@ def __init__( |
60 | 55 |
|
61 | 56 | self.SetName("Jupyter") |
62 | 57 |
|
63 | | - def start_jupyter_server(self, notebooks_dir): |
64 | | - """Spustí Jupyter notebook server v daném adresáři na volném portu.""" |
65 | | - import socket |
66 | | - import time |
67 | | - |
68 | | - # Najdi volný port |
69 | | - sock = socket.socket() |
70 | | - sock.bind(("", 0)) |
71 | | - port = sock.getsockname()[1] |
72 | | - sock.close() |
73 | | - |
74 | | - # Spusť server v samostatném vlákně |
75 | | - def run_server(): |
76 | | - subprocess.Popen( |
77 | | - [ |
78 | | - "jupyter", |
79 | | - "notebook", |
80 | | - "--no-browser", |
81 | | - "--NotebookApp.token=''", |
82 | | - "--NotebookApp.password=''", |
83 | | - "--port", |
84 | | - str(port), |
85 | | - "--notebook-dir", |
86 | | - notebooks_dir, |
87 | | - ] |
88 | | - ) |
89 | | - |
90 | | - threading.Thread(target=run_server, daemon=True).start() |
91 | | - |
92 | | - output = subprocess.check_output(["jupyter", "notebook", "list"]).decode() |
93 | | - print(output) |
94 | | - |
95 | | - # Počkej, až server naběhne (lepší by bylo kontrolovat výstup, zde jen krátké čekání) |
96 | | - time.sleep(3) |
97 | | - print(port) |
98 | | - return f"http://localhost:{port}" |
| 58 | + def _hide_file_menu(self, event): |
| 59 | + """Inject JavaScript to hide Jupyter's File menu |
| 60 | + and header after load. |
| 61 | + :param event: wx.EVT_WEBVIEW_LOADED event |
| 62 | + """ |
| 63 | + # Hide File menu and header |
| 64 | + webview = event.GetEventObject() |
| 65 | + js = """ |
| 66 | + var interval = setInterval(function() { |
| 67 | + // Hide File menu |
| 68 | + var fileMenu = document.querySelector('li#file_menu, a#filelink, a[aria-controls="file_menu"]'); |
| 69 | + if (fileMenu) { |
| 70 | + if (fileMenu.tagName === "LI") { |
| 71 | + fileMenu.style.display = 'none'; |
| 72 | + } else if (fileMenu.parentElement && fileMenu.parentElement.tagName === "LI") { |
| 73 | + fileMenu.parentElement.style.display = 'none'; |
| 74 | + } |
| 75 | + } |
| 76 | + // Hide top header |
| 77 | + var header = document.getElementById('header-container'); |
| 78 | + if (header) { |
| 79 | + header.style.display = 'none'; |
| 80 | + } |
| 81 | + // Stop checking once both are hidden |
| 82 | + if (fileMenu && header) { |
| 83 | + clearInterval(interval); |
| 84 | + } |
| 85 | + }, 500); |
| 86 | + """ |
| 87 | + webview.RunScript(js) |
| 88 | + |
| 89 | + def _add_notebook_tab(self, url, title): |
| 90 | + """Add a new tab with a Jupyter notebook loaded in a WebView. |
| 91 | +
|
| 92 | + This method creates a new browser tab inside the notebook panel |
| 93 | + and loads the given URL. After the page is loaded, it injects |
| 94 | + JavaScript to hide certain UI elements from the Jupyter interface. |
| 95 | +
|
| 96 | + :param url: URL to the Jupyter notebook |
| 97 | + :param title: Title of the new tab |
| 98 | + """ |
| 99 | + webview = html.WebView.New(self.notebook) |
| 100 | + wx.CallAfter(webview.LoadURL, url) |
| 101 | + wx.CallAfter(webview.Bind, html.EVT_WEBVIEW_LOADED, self._hide_file_menu) |
| 102 | + self.notebook.AddPage(webview, title) |
99 | 103 |
|
100 | 104 | def SetUpPage(self): |
101 | 105 | """Set up the Jupyter Notebook interface.""" |
102 | | - gisenv = gs.gisenv() |
103 | | - gisdbase = gisenv["GISDBASE"] |
104 | | - location = gisenv["LOCATION_NAME"] |
105 | | - mapset = gisenv["MAPSET"] |
106 | | - mapset_path = f"{gisdbase}/{location}/{mapset}" |
107 | | - notebooks_dir = Path(mapset_path) / "notebooks" |
108 | | - notebooks_dir.mkdir(parents=True, exist_ok=True) |
109 | | - self.session = gj.init(mapset_path) |
110 | | - |
111 | | - # Spusť Jupyter server v adresáři notebooks |
112 | | - url_base = self.start_jupyter_server(notebooks_dir) |
113 | | - |
114 | | - # Najdi všechny .ipynb soubory v notebooks/ |
115 | | - ipynb_files = [f for f in Path.iterdir(notebooks_dir) if f.endswith(".ipynb")] |
116 | | - print(ipynb_files) |
117 | | - |
118 | | - if not ipynb_files: |
119 | | - print("No notebooks found in the directory.") |
120 | | - # Pokud nejsou žádné soubory, vytvoř template |
121 | | - new_notebook_name = "template.ipynb" |
122 | | - print(new_notebook_name) |
123 | | - new_notebook_path = Path(notebooks_dir) / (new_notebook_name) |
124 | | - template = { |
125 | | - "cells": [ |
126 | | - { |
127 | | - "cell_type": "markdown", |
128 | | - "metadata": {}, |
129 | | - "source": [ |
130 | | - "# Template file\n", |
131 | | - ], |
132 | | - }, |
133 | | - { |
134 | | - "cell_type": "markdown", |
135 | | - "metadata": {}, |
136 | | - "source": [ |
137 | | - "You can add your own code here\n", |
138 | | - "or create new notebooks in the GRASS GUI\n", |
139 | | - "and they will be automatically saved in the directory: `{}`\n".format( |
140 | | - notebooks_dir.replace("\\", "/") |
141 | | - ), |
142 | | - "and opened in the Jupyter Notebook interface.\n", |
143 | | - "\n", |
144 | | - ], |
145 | | - }, |
146 | | - { |
147 | | - "cell_type": "code", |
148 | | - "execution_count": None, |
149 | | - "metadata": {}, |
150 | | - "outputs": [], |
151 | | - "source": [ |
152 | | - "import grass.script as gs", |
153 | | - ], |
154 | | - }, |
155 | | - { |
156 | | - "cell_type": "code", |
157 | | - "execution_count": None, |
158 | | - "metadata": {}, |
159 | | - "outputs": [], |
160 | | - "source": [ |
161 | | - "print('Raster maps in the current mapset:')\n", |
162 | | - "for rast in gs.list_strings(type='raster'):\n", |
163 | | - " print(' ', rast)\n", |
164 | | - ], |
165 | | - }, |
166 | | - { |
167 | | - "cell_type": "code", |
168 | | - "execution_count": None, |
169 | | - "metadata": {}, |
170 | | - "outputs": [], |
171 | | - "source": [ |
172 | | - "print('\\nVector maps in the current mapset:')\n", |
173 | | - "for vect in gs.list_strings(type='vector'):\n", |
174 | | - " print(' ', vect)\n", |
175 | | - ], |
176 | | - }, |
177 | | - ], |
178 | | - "metadata": { |
179 | | - "kernelspec": { |
180 | | - "display_name": "Python 3", |
181 | | - "language": "python", |
182 | | - "name": "python3", |
183 | | - }, |
184 | | - "language_info": {"name": "python", "version": "3.x"}, |
185 | | - }, |
186 | | - "nbformat": 4, |
187 | | - "nbformat_minor": 2, |
188 | | - } |
189 | | - print(new_notebook_path) |
190 | | - print("template") |
191 | | - with open(new_notebook_path, "w", encoding="utf-8") as f: |
192 | | - json.dump(template, f, ensure_ascii=False, indent=2) |
193 | | - ipynb_files.append(new_notebook_name) |
194 | | - |
195 | | - notebook = wx.Notebook(self) |
196 | | - |
197 | | - # Po načtení stránky injektujte JS pro skrytí File menu |
198 | | - def hide_file_menu(event): |
199 | | - browser = event.GetEventObject() |
200 | | - print(browser) |
201 | | - js = """ |
202 | | - var interval = setInterval(function() { |
203 | | - // Skrytí File menu |
204 | | - var fileMenu = document.querySelector('li#file_menu, a#filelink, a[aria-controls="file_menu"]'); |
205 | | - if (fileMenu) { |
206 | | - if (fileMenu.tagName === "LI") { |
207 | | - fileMenu.style.display = 'none'; |
208 | | - } else if (fileMenu.parentElement && fileMenu.parentElement.tagName === "LI") { |
209 | | - fileMenu.parentElement.style.display = 'none'; |
210 | | - } |
211 | | - } |
212 | | - // Skrytí horního panelu |
213 | | - var header = document.getElementById('header-container'); |
214 | | - if (header) { |
215 | | - header.style.display = 'none'; |
216 | | - } |
217 | | - // Ukonči interval, pokud jsou oba prvky nalezeny |
218 | | - if (fileMenu && header) { |
219 | | - clearInterval(interval); |
220 | | - } |
221 | | - }, 500); |
222 | | - """ |
| 106 | + # Create a directory manager to handle notebook files |
| 107 | + # and a server manager to run the Jupyter server |
| 108 | + dir_manager = NotebookDirectoryManager() |
| 109 | + dir_manager.prepare_notebook_files() |
| 110 | + |
| 111 | + server_manager = NotebookServerManager(dir_manager.notebook_workdir) |
| 112 | + server_manager.start_server() |
223 | 113 |
|
224 | | - browser.RunScript(js) |
| 114 | + self.notebook = wx.Notebook(self) |
225 | 115 |
|
226 | | - for fname in ipynb_files: |
227 | | - url_base = url_base.rstrip("/") |
228 | | - url = f"{url_base}/notebooks/{fname}" |
229 | | - browser = html.WebView.New(notebook) |
230 | | - wx.CallAfter(browser.LoadURL, url) |
231 | | - wx.CallAfter(browser.Bind, html.EVT_WEBVIEW_LOADED, hide_file_menu) |
232 | | - notebook.AddPage(browser, fname) |
| 116 | + # Create a new tab for each notebook file |
| 117 | + for fname in dir_manager.notebook_files: |
| 118 | + print(fname) |
| 119 | + url = server_manager.get_notebook_url(fname) |
| 120 | + self._add_notebook_tab(url, fname) |
233 | 121 |
|
234 | 122 | sizer = wx.BoxSizer(wx.VERTICAL) |
235 | | - sizer.Add(notebook, 1, wx.EXPAND) |
| 123 | + sizer.Add(self.notebook, 1, wx.EXPAND) |
236 | 124 | self.SetSizer(sizer) |
0 commit comments