lunar-toolkit 0.1.0.dev11__tar.gz → 0.1.0.dev13__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 (94) hide show
  1. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/PKG-INFO +1 -1
  2. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/app.py +14 -0
  3. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/index.html +203 -15
  4. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/PKG-INFO +1 -1
  5. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/pyproject.toml +2 -2
  6. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/LICENSE +0 -0
  7. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/README.md +0 -0
  8. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/__init__.py +0 -0
  9. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/asp.py +0 -0
  10. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/__init__.py +0 -0
  11. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/_jobs.py +0 -0
  12. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/_status.py +0 -0
  13. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/_utils.py +0 -0
  14. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/build_db.py +0 -0
  15. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/download.py +0 -0
  16. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/kaguya.py +0 -0
  17. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/ode_search.py +0 -0
  18. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/pairs.py +0 -0
  19. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/process.py +0 -0
  20. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/query.py +0 -0
  21. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/reference.py +0 -0
  22. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/search.py +0 -0
  23. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/serve.py +0 -0
  24. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/sfs.py +0 -0
  25. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/sfs_report.py +0 -0
  26. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/shadowcam.py +0 -0
  27. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/stereo.py +0 -0
  28. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/cli/workspace.py +0 -0
  29. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/config.py +0 -0
  30. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/constants.py +0 -0
  31. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/csm.py +0 -0
  32. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/downloader.py +0 -0
  33. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/geometry.py +0 -0
  34. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/isis.py +0 -0
  35. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/kaguya/__init__.py +0 -0
  36. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/kaguya/mi.py +0 -0
  37. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/kaguya/tc.py +0 -0
  38. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/lroc/__init__.py +0 -0
  39. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/lroc/cumindex.py +0 -0
  40. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/lroc/nac.py +0 -0
  41. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/observation.py +0 -0
  42. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/ode.py +0 -0
  43. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/provenance.py +0 -0
  44. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/reference.py +0 -0
  45. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/selection.py +0 -0
  46. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/sfs.py +0 -0
  47. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/sfs_products.py +0 -0
  48. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/sfs_report.py +0 -0
  49. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/shadowcam/__init__.py +0 -0
  50. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/shadowcam/sc.py +0 -0
  51. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_dem_global.tif +0 -0
  52. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_dem_north.tif +0 -0
  53. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_dem_south.tif +0 -0
  54. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_shade_global.jpg +0 -0
  55. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_shade_north.jpg +0 -0
  56. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/lola_shade_south.jpg +0 -0
  57. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/manifest.json +0 -0
  58. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/wac_global.jpg +0 -0
  59. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/wac_north.jpg +0 -0
  60. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/static/basemaps/wac_south.jpg +0 -0
  61. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/stereo.py +0 -0
  62. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/stereo_dem.py +0 -0
  63. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/tools.py +0 -0
  64. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit/workspace.py +0 -0
  65. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/SOURCES.txt +0 -0
  66. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/dependency_links.txt +0 -0
  67. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/entry_points.txt +0 -0
  68. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/requires.txt +0 -0
  69. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/lunar_toolkit.egg-info/top_level.txt +0 -0
  70. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/setup.cfg +0 -0
  71. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_aoi.py +0 -0
  72. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_app.py +0 -0
  73. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_build.py +0 -0
  74. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_config.py +0 -0
  75. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_csm.py +0 -0
  76. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_download.py +0 -0
  77. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_footprints.py +0 -0
  78. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_integration.py +0 -0
  79. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_kaguya.py +0 -0
  80. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_kaguya_mi.py +0 -0
  81. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_lbl.py +0 -0
  82. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_ode.py +0 -0
  83. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_reference.py +0 -0
  84. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_selection.py +0 -0
  85. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_sfs.py +0 -0
  86. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_sfs_products.py +0 -0
  87. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_sfs_report.py +0 -0
  88. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_shadowcam.py +0 -0
  89. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_static.py +0 -0
  90. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_stereo.py +0 -0
  91. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_tool_env.py +0 -0
  92. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_tools.py +0 -0
  93. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_update.py +0 -0
  94. {lunar_toolkit-0.1.0.dev11 → lunar_toolkit-0.1.0.dev13}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lunar-toolkit
3
- Version: 0.1.0.dev11
3
+ Version: 0.1.0.dev13
4
4
  Summary: Find, download and process lunar orbital imagery (LROC, ShadowCam, Kaguya TC) and build stereo and shape-from-shading DEMs with ISIS + ASP
5
5
  License-Expression: BSD-3-Clause
6
6
  Keywords: moon,lroc,shadowcam,kaguya,isis,ames-stereo-pipeline,shape-from-shading,dem
@@ -144,9 +144,23 @@ def choose_backend(requested: str = "auto", platform: str | None = None) -> tupl
144
144
  return "browser", None
145
145
 
146
146
 
147
+ def _quiet_qt_exit() -> None:
148
+ """Drop Qt WebEngine's harmless "Release of profile requested" warning on window close."""
149
+ from qtpy.QtCore import qInstallMessageHandler
150
+
151
+ def handler(mode: Any, context: Any, message: str | None) -> None:
152
+ if message and "WebEnginePage still not deleted" not in message:
153
+ sys.stderr.write(message + "\n")
154
+
155
+ qInstallMessageHandler(handler)
156
+
157
+
147
158
  def _run_webview(url: str, gui: str) -> None:
148
159
  import webview
149
160
 
161
+ if gui == "qt":
162
+ _quiet_qt_exit()
163
+
150
164
  from ..constants import DEFAULT_CACHE_DIR
151
165
 
152
166
  webview.create_window(TITLE, url, width=SIZE[0], height=SIZE[1], min_size=MIN_SIZE)
@@ -191,10 +191,28 @@
191
191
  .map .frame { fill: none; stroke: var(--plot-fg); stroke-width: 1; shape-rendering: crispEdges; }
192
192
  .map .tick { fill: none; stroke: var(--plot-fg); stroke-width: 1; shape-rendering: crispEdges; }
193
193
  .map .grid { fill: none; stroke: var(--plot-grid); stroke-width: 1; }
194
- .map .fp { fill: var(--fp); fill-opacity: 0.06; fill-rule: evenodd; stroke: var(--fp); stroke-opacity: 0.55; stroke-width: 0.8; cursor: pointer; }
195
- .map .fp:hover { fill-opacity: 0.22; stroke-opacity: 0.9; stroke-width: 1.3; }
196
- .map .fp.sel { fill: var(--fp-sel); fill-opacity: 0.25; stroke: var(--fp-sel); stroke-opacity: 1; stroke-width: 1.8; }
197
- .mapbox.hide-fp .fp, .mapbox.hide-region .region { display: none; }
194
+ /* Fills share one group whose opacity applies once, so overlaps do not build up */
195
+ .map .fpfill { opacity: var(--fp-fill, 0.07); pointer-events: none; }
196
+ .map .fpfill path { fill: var(--fp-color, #3d8bfd); fill-rule: evenodd; stroke: none; }
197
+ .map .fp { fill: transparent; fill-rule: evenodd; stroke: var(--fp-color, #3d8bfd); stroke-opacity: var(--fp-line, 0.85); stroke-width: 0.9; stroke-dasharray: var(--fp-dash, 1.5 2.5); cursor: pointer; }
198
+ .map .fp:hover { stroke-opacity: 1; stroke-width: 1.5; stroke-dasharray: none; }
199
+ .map .fp.sel { fill: var(--fp-sel); fill-opacity: 0.2; stroke: var(--fp-sel); stroke-opacity: 1; stroke-width: 1.8; stroke-dasharray: none; }
200
+ .map .sketch { fill: var(--accent); fill-opacity: 0.12; stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 5 3; pointer-events: none; }
201
+ .map .sketch.pt { fill: var(--accent); fill-opacity: 1; stroke: #fff; stroke-dasharray: none; }
202
+ .mapcoord {
203
+ position: absolute; pointer-events: none; padding: 1px 5px; font: 11px "DejaVu Sans Mono", Consolas, monospace;
204
+ background: rgba(255, 255, 255, 0.85); border: 1px solid var(--border); color: #000; white-space: nowrap;
205
+ }
206
+ .mapcoord:empty { display: none; }
207
+ .panehead .tools { display: inline-flex; margin-right: 8px; }
208
+ .panehead .tools button { margin-right: -1px; }
209
+ .stylewrap { position: relative; }
210
+ .stylepop {
211
+ position: absolute; right: 0; top: 100%; z-index: 5; margin-top: 3px; padding: 6px 8px; display: grid;
212
+ grid-template-columns: auto auto; gap: 4px 8px; align-items: center; background: var(--window); border: 1px solid var(--border);
213
+ }
214
+ .stylepop select { width: auto; }
215
+ .mapbox.hide-fp .fp, .mapbox.hide-fp .fpfill, .mapbox.hide-region .region { display: none; }
198
216
  .panehead .toggle { display: inline-flex; align-items: center; gap: 3px; margin-right: 8px; white-space: nowrap; }
199
217
  .map .region { fill: none; fill-rule: evenodd; stroke: var(--region); stroke-width: 1.4; stroke-dasharray: 6 3; pointer-events: none; }
200
218
  .map text { fill: var(--plot-label); font-size: 11px; pointer-events: none; }
@@ -1189,6 +1207,10 @@
1189
1207
  };
1190
1208
  }
1191
1209
 
1210
+ $('search-form').addEventListener('change', (e) => {
1211
+ if (e.target.id === 's-mode' || e.target.closest('[data-mode]')) showFormRegion();
1212
+ });
1213
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') $('map')._cancel?.(); });
1192
1214
  for (const p of ['s']) {
1193
1215
  const sync = () => {
1194
1216
  for (const node of $(`${p}-mode`).closest('fieldset').querySelectorAll('[data-mode]')) {
@@ -1236,6 +1258,67 @@
1236
1258
  return el('label', { class: 'toggle' }, box, ` ${label}`);
1237
1259
  }
1238
1260
 
1261
+ const MAP_TOOLS = {
1262
+ pan: ['Pan', 'Drag to pan, wheel to zoom, click a footprint for details'],
1263
+ box: ['Box', 'Drag a box on the map to set the search region'],
1264
+ circle: ['Circle', 'Drag from the centre to set a point and radius'],
1265
+ poly: ['Polygon', 'Click the polygon vertices, then double click or click the first point (Esc cancels)'],
1266
+ };
1267
+ const FP_LINES = { dotted: '1.5 2.5', dashed: '5 3', solid: 'none', none: 'none' };
1268
+ const FP_FILLS = { none: 0, light: 0.07, medium: 0.18 };
1269
+ const FP_COLORS = { blue: '#3d8bfd', yellow: '#ffd43b', cyan: '#22d3ee', magenta: '#e64980', navy: '#1f4a8a', white: '#ffffff' };
1270
+
1271
+ function applyFootprintStyle(container) {
1272
+ container.style.setProperty('--fp-dash', FP_LINES[mapPrefs.fpLine] ?? FP_LINES.dotted);
1273
+ container.style.setProperty('--fp-line', mapPrefs.fpLine === 'none' ? '0' : '0.85');
1274
+ container.style.setProperty('--fp-fill', String(FP_FILLS[mapPrefs.fpFill] ?? FP_FILLS.light));
1275
+ container.style.setProperty('--fp-color', FP_COLORS[mapPrefs.fpColor] ?? FP_COLORS.blue);
1276
+ }
1277
+
1278
+ // Footprint outline, fill and colour, in a small pop-up next to the Footprints toggle
1279
+ function footprintStyle(container) {
1280
+ applyFootprintStyle(container);
1281
+ const field = (key, label, options) => {
1282
+ const s = el('select', { 'aria-label': label }, ...Object.keys(options).map((o) => el('option', { value: o }, o)));
1283
+ s.value = mapPrefs[key];
1284
+ s.addEventListener('change', () => { mapPrefs[key] = s.value; saveMapPrefs(); applyFootprintStyle(container); });
1285
+ return [el('span', {}, label), s];
1286
+ };
1287
+ const pop = el('div', { class: 'stylepop', hidden: '' },
1288
+ ...field('fpLine', 'Outline', FP_LINES), ...field('fpFill', 'Fill', FP_FILLS), ...field('fpColor', 'Colour', FP_COLORS));
1289
+ const button = el('button', { class: 'tool', type: 'button', title: 'Footprint outline, fill and colour' }, 'Style');
1290
+ button.addEventListener('click', (e) => { e.stopPropagation(); pop.hidden = !pop.hidden; });
1291
+ pop.addEventListener('click', (e) => e.stopPropagation());
1292
+ document.addEventListener('click', () => { pop.hidden = true; });
1293
+ return el('span', { class: 'stylewrap' }, button, pop);
1294
+ }
1295
+
1296
+ function greatCircleKm(lat1, lon1, lat2, lon2) {
1297
+ const h = Math.sin(((lat2 - lat1) * RAD) / 2) ** 2
1298
+ + Math.cos(lat1 * RAD) * Math.cos(lat2 * RAD) * Math.sin(((lon2 - lon1) * RAD) / 2) ** 2;
1299
+ return 2 * MOON_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(h)));
1300
+ }
1301
+
1302
+ // Fill the search form's region from the map; its change event redraws the region
1303
+ function setFormRegion(mode, v) {
1304
+ const round = (x, d = 5) => String(Math.round(x * 10 ** d) / 10 ** d);
1305
+ if (mode === 'point') { $('s-lat').value = round(v.lat); $('s-lon').value = round(mod(v.lon, 360)); $('s-radius').value = round(v.radius, 3); }
1306
+ if (mode === 'box') for (const k of ['west', 'east', 'south', 'north']) $(`s-${k}`).value = round(v[k]);
1307
+ if (mode === 'aoi') $('s-aoi').value = v.aoi;
1308
+ $('s-mode').value = mode;
1309
+ $('s-mode').dispatchEvent(new Event('change', { bubbles: true }));
1310
+ setStatus('status', 'Search region set from the map | press Search');
1311
+ }
1312
+
1313
+ // Show the form's search region on the map (before any search, or over the current results)
1314
+ function showFormRegion() {
1315
+ let sel;
1316
+ try { sel = readRegion('s'); } catch { return; }
1317
+ const map = $('map');
1318
+ if (map._setRegion) map._setRegion(sel);
1319
+ else renderMap(map, { hemi: mapHemi(sel.region, []), region: sel, items: [] });
1320
+ }
1321
+
1239
1322
  function renderMap(container, { hemi, region, items }) {
1240
1323
  container._resize?.disconnect();
1241
1324
  container._highlight = null;
@@ -1248,14 +1331,17 @@
1248
1331
  const cinfo = el('span', { class: 'info' });
1249
1332
  const pick = el('select', { 'aria-label': 'Basemap', title: 'Basemap under the footprints' });
1250
1333
  const cbox = el('input', { type: 'checkbox' });
1334
+ const coord = el('div', { class: 'mapcoord' });
1335
+ const tools = el('span', { class: 'tools' }, ...Object.entries(MAP_TOOLS).map(([t, [label, title]]) =>
1336
+ el('button', { class: `tool${t === 'pan' ? ' on' : ''}`, type: 'button', 'data-tool': t, title }, label)));
1251
1337
  const bar = el('div', { class: 'panehead' },
1252
1338
  el('span', { class: 'title' }, 'Footprints'),
1253
1339
  el('span', { class: 'info' }, hemi ? `Polar stereographic (${hemi === 'S' ? 'south' : 'north'}), R = ${MOON_RADIUS_KM} km`
1254
1340
  : 'Lon/lat (equirectangular)', ` | ${items.length} footprint(s) | wheel zoom, drag pan`),
1255
1341
  cinfo, spin,
1256
- el('span', { class: 'right' }, pick, el('label', { class: 'toggle', title: 'Elevation contours (LOLA)' }, cbox, ' Contours'),
1257
- layerToggle(container, 'fp', 'Footprints'), layerToggle(container, 'region', 'Region'), reset));
1258
- container.replaceChildren(el('div', { class: 'map' }, bar, el('div', { class: 'plotarea' }, canvas, svg)));
1342
+ el('span', { class: 'right' }, tools, pick, el('label', { class: 'toggle', title: 'Elevation contours (LOLA)' }, cbox, ' Contours'),
1343
+ layerToggle(container, 'fp', 'Footprints'), footprintStyle(container), layerToggle(container, 'region', 'Region'), reset));
1344
+ container.replaceChildren(el('div', { class: 'map' }, bar, el('div', { class: 'plotarea' }, canvas, svg, coord)));
1259
1345
  const clipId = `clip-${container.id}`;
1260
1346
  const M = MARGIN;
1261
1347
 
@@ -1263,11 +1349,12 @@
1263
1349
  const regionGeom = region ? region.shape(hemi) : [];
1264
1350
  let geoms = items.map((it) => it.geom);
1265
1351
  let kx = 1;
1352
+ let recenter = (g) => g;
1266
1353
  if (!hemi) {
1267
1354
  const mids = [...regionGeom, ...geoms.flat()].map((P) => { const b = bboxOf([P[0]]); return (b[0] + b[1]) / 2; });
1268
1355
  const c = region ? (region.region.span < 360 ? region.region.west + region.region.span / 2 : 180)
1269
1356
  : Math.atan2(sumOf(mids.map((m) => Math.sin(m * RAD))), sumOf(mids.map((m) => Math.cos(m * RAD)))) * DEG;
1270
- const recenter = (g) => g.map((P) => { const b = bboxOf([P[0]]); return shiftX([P], -360 * Math.round(((b[0] + b[1]) / 2 - c) / 360))[0]; });
1357
+ recenter = (g) => g.map((P) => { const b = bboxOf([P[0]]); return shiftX([P], -360 * Math.round(((b[0] + b[1]) / 2 - c) / 360))[0]; });
1271
1358
  geoms = geoms.map(recenter);
1272
1359
  const lats = [...regionGeom, ...geoms.flat()].flatMap((P) => P[0].map((p) => p[1]));
1273
1360
  kx = Math.max(0.2, Math.cos(((minOf(lats) + maxOf(lats)) / 2) * RAD));
@@ -1275,7 +1362,7 @@
1275
1362
  }
1276
1363
  // Data coordinates: x right, y down
1277
1364
  const toXY = (g) => g.map((P) => P.map((r) => r.map(([x, y]) => (hemi ? [x, -y] : [x * kx, -y]))));
1278
- const reg = toXY(regionGeom);
1365
+ let reg = toXY(regionGeom);
1279
1366
  const fps = geoms.map(toXY);
1280
1367
  const all = [...reg, ...fps.flat()].flat();
1281
1368
  let [x0, x1, y0, y1] = all.length ? bboxOf(all) : [-1, 1, -1, 1];
@@ -1299,6 +1386,11 @@
1299
1386
  const sgn = hemi === 'S' ? 1 : -1;
1300
1387
  const rho = (la) => 2 * MOON_RADIUS_KM * 1000 * Math.tan(Math.PI / 4 + sgn * (la * RAD) / 2);
1301
1388
  const latOf = (r) => sgn * (2 * Math.atan(r / (2 * MOON_RADIUS_KM * 1000)) - Math.PI / 2) * DEG;
1389
+ // Data coordinates (x right, y down) to [lon, lat]
1390
+ const toLonLat = ([x, y]) => (hemi ? [mod(Math.atan2(x, -sgn * y) * DEG, 360), latOf(Math.hypot(x, y))] : [x / kx, -y]);
1391
+ coord.style.left = `${M.l + 6}px`;
1392
+ coord.style.bottom = `${M.b + 6}px`;
1393
+ let tool = 'pan', sketch = null;
1302
1394
 
1303
1395
  // Grid lines (clipped), graticule labels inside the plot, ticks + tick labels on the frame
1304
1396
  function axes() {
@@ -1377,11 +1469,14 @@
1377
1469
  const a = axes();
1378
1470
  const order = fps.map((_, i) => i).filter((i) => i !== sel);
1379
1471
  if (sel >= 0) order.push(sel);
1472
+ const ds = fps.map((g) => pathD(g));
1380
1473
  svg.innerHTML = `<defs><clipPath id="${clipId}"><rect width="${W}" height="${H}"/></clipPath></defs>`
1381
1474
  + `<g transform="translate(${M.l} ${M.t})"><rect class="plot" width="${W}" height="${H}"/>`
1382
1475
  + `<g clip-path="url(#${clipId})">${a.grid}<g class="cslot"/>`
1383
- + order.map((i) => `<path class="fp${i === sel ? ' sel' : ''}" data-i="${i}" d="${pathD(fps[i])}"><title>${esc(items[i].title)}</title></path>`).join('')
1476
+ + `<g class="fpfill">${ds.map((d) => `<path d="${d}"/>`).join('')}</g>`
1477
+ + order.map((i) => `<path class="fp${i === sel ? ' sel' : ''}" data-i="${i}" d="${ds[i]}"><title>${esc(items[i].title)}</title></path>`).join('')
1384
1478
  + (reg.length ? `<path class="region" d="${pathD(reg)}"/>` : '')
1479
+ + sketchSvg()
1385
1480
  + `</g><rect class="frame" x="0.5" y="0.5" width="${W - 1}" height="${H - 1}"/>`
1386
1481
  + `<path class="tick" d="${a.ticks}"/>${a.labels}</g>`;
1387
1482
  svg.querySelector('.cslot').replaceWith(under.contours);
@@ -1401,9 +1496,58 @@
1401
1496
  view.cx = x - (mx - W / 2) / view.k; view.cy = y - (my - H / 2) / view.k;
1402
1497
  redraw();
1403
1498
  }, { passive: false });
1499
+ const dataXY = (e) => { const [mx, my] = plotXY(e); return [view.cx + (mx - W / 2) / view.k, view.cy + (my - H / 2) / view.k]; };
1500
+ const screenDist = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1]) * view.k;
1501
+ // Drawn region in progress: box and circle while dragging, polygon vertices plus the pointer
1502
+ function sketchSvg() {
1503
+ if (!sketch) return '';
1504
+ const P = ([x, y]) => `${sx(x).toFixed(1)} ${sy(y).toFixed(1)}`;
1505
+ if (sketch.tool === 'box') {
1506
+ const [[ax, ay], [bx, by]] = [sketch.a, sketch.b];
1507
+ return `<path class="sketch" d="M${P([ax, ay])}L${P([bx, ay])}L${P([bx, by])}L${P([ax, by])}Z"/>`;
1508
+ }
1509
+ if (sketch.tool === 'circle') {
1510
+ const r = screenDist(sketch.a, sketch.b).toFixed(1);
1511
+ return `<circle class="sketch" cx="${sx(sketch.a[0]).toFixed(1)}" cy="${sy(sketch.a[1]).toFixed(1)}" r="${r}"/>`;
1512
+ }
1513
+ const pts = [...sketch.pts, sketch.hover].filter(Boolean);
1514
+ return `<path class="sketch" d="M${pts.map(P).join('L')}"/>`
1515
+ + sketch.pts.map(([x, y]) => `<circle class="sketch pt" cx="${sx(x).toFixed(1)}" cy="${sy(y).toFixed(1)}" r="3"/>`).join('');
1516
+ }
1517
+ const setTool = (t) => {
1518
+ tool = t; sketch = null;
1519
+ for (const b of tools.children) b.classList.toggle('on', b.dataset.tool === t);
1520
+ if (t !== 'pan') setStatus('status', MAP_TOOLS[t][1]);
1521
+ redraw();
1522
+ };
1523
+ for (const b of tools.children) b.addEventListener('click', () => setTool(b.dataset.tool));
1524
+ // Densified ring in data coordinates -> lon/lat, so edges follow the map's straight lines
1525
+ const ringLonLat = (pts, n = hemi ? 16 : 1) => pts.flatMap((p, i) => {
1526
+ const q = pts[(i + 1) % pts.length];
1527
+ return Array.from({ length: n }, (_, j) => toLonLat([p[0] + ((q[0] - p[0]) * j) / n, p[1] + ((q[1] - p[1]) * j) / n]));
1528
+ });
1529
+ function finishSketch() {
1530
+ const sk = sketch;
1531
+ setTool('pan');
1532
+ if (!sk) return;
1533
+ if (sk.tool === 'circle') {
1534
+ const [lon, lat] = toLonLat(sk.a), [lon2, lat2] = toLonLat(sk.b);
1535
+ setFormRegion('point', { lat, lon, radius: greatCircleKm(lat, lon, lat2, lon2) });
1536
+ } else if (sk.tool === 'box' && !hemi) {
1537
+ const [[w, n], [e, s]] = [toLonLat([Math.min(sk.a[0], sk.b[0]), Math.min(sk.a[1], sk.b[1])]),
1538
+ toLonLat([Math.max(sk.a[0], sk.b[0]), Math.max(sk.a[1], sk.b[1])])];
1539
+ const full = e - w >= 360;
1540
+ setFormRegion('box', { west: full ? 0 : mod(w, 360), east: full ? 360 : mod(e, 360), south: s, north: n });
1541
+ } else {
1542
+ const pts = sk.tool === 'box' ? [sk.a, [sk.b[0], sk.a[1]], sk.b, [sk.a[0], sk.b[1]]] : sk.pts;
1543
+ const ring = ringLonLat(pts).map(([lon, lat]) => [mod(lon, 360), lat]);
1544
+ setFormRegion('aoi', { aoi: `POLYGON((${[...ring, ring[0]].map(([lon, lat]) => `${lon.toFixed(5)} ${lat.toFixed(5)}`).join(', ')}))` });
1545
+ }
1546
+ }
1547
+ container._cancel = () => { if (tool !== 'pan') setTool('pan'); };
1404
1548
  const showCursor = (e) => {
1405
1549
  const [mx, my] = plotXY(e);
1406
- if (mx < 0 || my < 0 || mx > W || my > H) { $('cursor').textContent = ''; return; }
1550
+ if (mx < 0 || my < 0 || mx > W || my > H) { $('cursor').textContent = coord.textContent = ''; return; }
1407
1551
  const x = view.cx + (mx - W / 2) / view.k, y = -(view.cy + (my - H / 2) / view.k);
1408
1552
  let lat, lon, xy = '';
1409
1553
  if (hemi) {
@@ -1413,31 +1557,68 @@
1413
1557
  } else {
1414
1558
  [lat, lon] = [y, mod(x / kx, 360)];
1415
1559
  }
1416
- $('cursor').textContent = Math.abs(lat) > 90 ? ''
1560
+ $('cursor').textContent = coord.textContent = Math.abs(lat) > 90 ? ''
1417
1561
  : `Lat ${lat.toFixed(4)} Lon ${lon.toFixed(4)} E${xy}`;
1418
1562
  };
1419
1563
  let drag = null;
1420
1564
  svg.addEventListener('pointerdown', (e) => {
1565
+ if (tool !== 'pan') {
1566
+ if (e.button !== 0) return;
1567
+ const p = dataXY(e);
1568
+ if (tool !== 'poly') sketch = { tool, a: p, b: p, down: true };
1569
+ else if (!sketch) sketch = { tool, pts: [p], hover: p };
1570
+ else if (sketch.pts.length >= 3 && screenDist(sketch.pts[0], p) < 8) { finishSketch(); return; }
1571
+ else sketch.pts.push(p);
1572
+ try { svg.setPointerCapture(e.pointerId); } catch { /* synthetic event */ }
1573
+ redraw();
1574
+ return;
1575
+ }
1421
1576
  drag = { x: e.clientX, y: e.clientY, cx: view.cx, cy: view.cy, i: e.target.dataset?.i, moved: false };
1422
1577
  try { svg.setPointerCapture(e.pointerId); } catch { /* synthetic event */ }
1423
1578
  });
1424
1579
  svg.addEventListener('pointermove', (e) => {
1425
1580
  showCursor(e);
1581
+ if (sketch) {
1582
+ if (sketch.tool === 'poly') sketch.hover = dataXY(e);
1583
+ else if (sketch.down) sketch.b = dataXY(e);
1584
+ redraw();
1585
+ return;
1586
+ }
1426
1587
  if (!drag) return;
1427
1588
  const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
1428
1589
  if (Math.hypot(dx, dy) > 4) { drag.moved = true; svg.classList.add('dragging'); }
1429
1590
  if (drag.moved) { touched = true; view.cx = drag.cx - dx / view.k; view.cy = drag.cy - dy / view.k; redraw(); }
1430
1591
  });
1431
- svg.addEventListener('pointerleave', () => { if (!drag) $('cursor').textContent = ''; });
1592
+ svg.addEventListener('pointerleave', () => { if (!drag) $('cursor').textContent = coord.textContent = ''; });
1432
1593
  svg.addEventListener('pointerup', () => {
1594
+ if (sketch && sketch.tool !== 'poly') {
1595
+ if (screenDist(sketch.a, sketch.b) < 4) { sketch = null; redraw(); } else finishSketch();
1596
+ return;
1597
+ }
1433
1598
  if (drag && !drag.moved && drag.i != null) items[Number(drag.i)].onclick();
1434
1599
  drag = null;
1435
1600
  svg.classList.remove('dragging');
1436
1601
  });
1437
1602
  const home0 = () => { view = null; touched = false; draw(); };
1438
- svg.addEventListener('dblclick', home0);
1603
+ svg.addEventListener('dblclick', () => {
1604
+ if (tool === 'poly' && sketch) {
1605
+ // The double click's own clicks added the last point twice
1606
+ sketch.pts = sketch.pts.filter((p, i, a) => !i || screenDist(p, a[i - 1]) > 3);
1607
+ if (sketch.pts.length >= 3) finishSketch();
1608
+ return;
1609
+ }
1610
+ if (tool === 'pan') home0();
1611
+ });
1439
1612
  reset.addEventListener('click', home0);
1440
1613
  container._highlight = (row) => { sel = items.findIndex((it) => it.row === row); draw(); };
1614
+ // A new search region from the form or the map, keeping the view and the results
1615
+ container._setRegion = (next) => {
1616
+ const h = mapHemi(next.region, []);
1617
+ if (h !== hemi) { renderMap(container, { hemi: h, region: next, items }); return; }
1618
+ region = next;
1619
+ reg = toXY(hemi ? next.shape(hemi) : recenter(next.shape(hemi)));
1620
+ redraw();
1621
+ };
1441
1622
  let lastW = 0, lastH = 0;
1442
1623
  container._resize = new ResizeObserver(() => {
1443
1624
  const [w, h] = [svg.clientWidth, svg.clientHeight];
@@ -2657,7 +2838,11 @@
2657
2838
  N: ['lola_dem_north'],
2658
2839
  };
2659
2840
  const BM_STRETCH = { min: 0, max: 255, gamma: 1 };
2660
- const mapPrefs = { basemap: 'wac', contours: false }; // remembered across searches
2841
+ // Map preferences, remembered across searches and (when storage works) sessions
2842
+ const MAP_PREFS_KEY = 'lunar-toolkit-map';
2843
+ const mapPrefs = { basemap: 'wac', contours: false, fpLine: 'dotted', fpFill: 'light', fpColor: 'blue' };
2844
+ try { Object.assign(mapPrefs, JSON.parse(localStorage.getItem(MAP_PREFS_KEY) ?? '{}')); } catch { /* storage blocked */ }
2845
+ const saveMapPrefs = () => { try { localStorage.setItem(MAP_PREFS_KEY, JSON.stringify(mapPrefs)); } catch { /* storage blocked */ } };
2661
2846
  const bmOpened = new Map(); // ref -> { promise, src, pending, failed }
2662
2847
  let bmManifest = null, bmNotify = null;
2663
2848
  const bmUrl = (file) => new URL(`basemaps/${file}`, window.location.href).href;
@@ -2760,12 +2945,14 @@
2760
2945
  container.classList.toggle('has-basemap', choice() !== 'none');
2761
2946
  pick.addEventListener('change', () => {
2762
2947
  mapPrefs.basemap = pick.value;
2948
+ saveMapPrefs();
2763
2949
  container.classList.toggle('has-basemap', pick.value !== 'none');
2764
2950
  redraw();
2765
2951
  });
2766
2952
  cbox.checked = mapPrefs.contours;
2767
2953
  cbox.addEventListener('change', () => {
2768
2954
  mapPrefs.contours = cbox.checked;
2955
+ saveMapPrefs();
2769
2956
  st.ckey = '';
2770
2957
  if (!cbox.checked) { st.gen++; st.cbusy = false; st.contour = null; paths.forEach((p) => p.removeAttribute('d')); cinfo.textContent = ''; }
2771
2958
  redraw();
@@ -3609,6 +3796,7 @@
3609
3796
 
3610
3797
  // Boot
3611
3798
  initSearchForm();
3799
+ showFormRegion();
3612
3800
  initViewer();
3613
3801
  initImages();
3614
3802
  initSettings();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lunar-toolkit
3
- Version: 0.1.0.dev11
3
+ Version: 0.1.0.dev13
4
4
  Summary: Find, download and process lunar orbital imagery (LROC, ShadowCam, Kaguya TC) and build stereo and shape-from-shading DEMs with ISIS + ASP
5
5
  License-Expression: BSD-3-Clause
6
6
  Keywords: moon,lroc,shadowcam,kaguya,isis,ames-stereo-pipeline,shape-from-shading,dem
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lunar-toolkit"
7
- version = "0.1.0.dev11"
7
+ version = "0.1.0.dev13"
8
8
  description = "Find, download and process lunar orbital imagery (LROC, ShadowCam, Kaguya TC) and build stereo and shape-from-shading DEMs with ISIS + ASP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -108,5 +108,5 @@ check_untyped_defs = true
108
108
  no_implicit_optional = true
109
109
 
110
110
  [[tool.mypy.overrides]]
111
- module = ["duckdb.*", "shapely.*", "platformdirs.*", "pyarrow.*", "matplotlib.*", "rasterio.*"]
111
+ module = ["duckdb.*", "shapely.*", "platformdirs.*", "pyarrow.*", "matplotlib.*", "rasterio.*", "qtpy.*", "webview.*"]
112
112
  ignore_missing_imports = true