lunar-toolkit 0.1.0.dev14__tar.gz → 0.1.0.dev15__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.dev14 → lunar_toolkit-0.1.0.dev15}/PKG-INFO +3 -1
  2. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/README.md +2 -0
  3. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/serve.py +2 -2
  4. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/index.html +258 -233
  5. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/PKG-INFO +3 -1
  6. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/pyproject.toml +1 -1
  7. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_static.py +1 -1
  8. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/LICENSE +0 -0
  9. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/__init__.py +0 -0
  10. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/asp.py +0 -0
  11. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/__init__.py +0 -0
  12. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/_jobs.py +0 -0
  13. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/_status.py +0 -0
  14. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/_utils.py +0 -0
  15. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/app.py +0 -0
  16. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/build_db.py +0 -0
  17. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/download.py +0 -0
  18. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/kaguya.py +0 -0
  19. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/ode_search.py +0 -0
  20. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/pairs.py +0 -0
  21. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/process.py +0 -0
  22. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/query.py +0 -0
  23. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/reference.py +0 -0
  24. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/search.py +0 -0
  25. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/sfs.py +0 -0
  26. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/sfs_report.py +0 -0
  27. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/shadowcam.py +0 -0
  28. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/stereo.py +0 -0
  29. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/cli/workspace.py +0 -0
  30. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/config.py +0 -0
  31. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/constants.py +0 -0
  32. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/csm.py +0 -0
  33. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/downloader.py +0 -0
  34. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/geometry.py +0 -0
  35. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/isis.py +0 -0
  36. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/kaguya/__init__.py +0 -0
  37. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/kaguya/mi.py +0 -0
  38. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/kaguya/tc.py +0 -0
  39. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/lroc/__init__.py +0 -0
  40. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/lroc/cumindex.py +0 -0
  41. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/lroc/nac.py +0 -0
  42. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/observation.py +0 -0
  43. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/ode.py +0 -0
  44. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/provenance.py +0 -0
  45. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/reference.py +0 -0
  46. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/selection.py +0 -0
  47. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/sfs.py +0 -0
  48. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/sfs_products.py +0 -0
  49. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/sfs_report.py +0 -0
  50. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/shadowcam/__init__.py +0 -0
  51. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/shadowcam/sc.py +0 -0
  52. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_dem_global.tif +0 -0
  53. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_dem_north.tif +0 -0
  54. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_dem_south.tif +0 -0
  55. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_shade_global.jpg +0 -0
  56. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_shade_north.jpg +0 -0
  57. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/lola_shade_south.jpg +0 -0
  58. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/manifest.json +0 -0
  59. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/wac_global.jpg +0 -0
  60. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/wac_north.jpg +0 -0
  61. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/static/basemaps/wac_south.jpg +0 -0
  62. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/stereo.py +0 -0
  63. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/stereo_dem.py +0 -0
  64. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/tools.py +0 -0
  65. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit/workspace.py +0 -0
  66. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/SOURCES.txt +0 -0
  67. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/dependency_links.txt +0 -0
  68. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/entry_points.txt +0 -0
  69. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/requires.txt +0 -0
  70. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/lunar_toolkit.egg-info/top_level.txt +0 -0
  71. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/setup.cfg +0 -0
  72. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_aoi.py +0 -0
  73. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_app.py +0 -0
  74. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_build.py +0 -0
  75. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_config.py +0 -0
  76. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_csm.py +0 -0
  77. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_download.py +0 -0
  78. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_footprints.py +0 -0
  79. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_integration.py +0 -0
  80. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_kaguya.py +0 -0
  81. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_kaguya_mi.py +0 -0
  82. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_lbl.py +0 -0
  83. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_ode.py +0 -0
  84. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_reference.py +0 -0
  85. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_selection.py +0 -0
  86. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_sfs.py +0 -0
  87. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_sfs_products.py +0 -0
  88. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_sfs_report.py +0 -0
  89. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_shadowcam.py +0 -0
  90. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_stereo.py +0 -0
  91. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_tool_env.py +0 -0
  92. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_tools.py +0 -0
  93. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/tests/test_update.py +0 -0
  94. {lunar_toolkit-0.1.0.dev14 → lunar_toolkit-0.1.0.dev15}/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.dev14
3
+ Version: 0.1.0.dev15
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
@@ -55,6 +55,8 @@ Dynamic: license-file
55
55
 
56
56
  # Lunar Toolkit
57
57
 
58
+ ![Lunar Toolkit app](https://raw.githubusercontent.com/alexmaietta/lunar-toolkit/main/docs/images/hero.png)
59
+
58
60
  Lunar Toolkit finds, downloads and processes lunar orbital imagery and turns it into DEMs.
59
61
 
60
62
  It builds a searchable index of LROC images whose footprint search also works near the poles, finds ShadowCam, LROC and Kaguya products through the PDS Orbital Data Explorer, imports images into [ISIS](https://isis.astrogeology.usgs.gov/), and builds stereo and shape-from-shading (SfS) DEMs with the [Ames Stereo Pipeline](https://stereopipeline.readthedocs.io/) through configurable, reproducible pipelines. A desktop app browses the index, ODE, footprints and full-resolution images.
@@ -6,6 +6,8 @@
6
6
 
7
7
  # Lunar Toolkit
8
8
 
9
+ ![Lunar Toolkit app](https://raw.githubusercontent.com/alexmaietta/lunar-toolkit/main/docs/images/hero.png)
10
+
9
11
  Lunar Toolkit finds, downloads and processes lunar orbital imagery and turns it into DEMs.
10
12
 
11
13
  It builds a searchable index of LROC images whose footprint search also works near the poles, finds ShadowCam, LROC and Kaguya products through the PDS Orbital Data Explorer, imports images into [ISIS](https://isis.astrogeology.usgs.gov/), and builds stereo and shape-from-shading (SfS) DEMs with the [Ames Stereo Pipeline](https://stereopipeline.readthedocs.io/) through configurable, reproducible pipelines. A desktop app browses the index, ODE, footprints and full-resolution images.
@@ -90,7 +90,7 @@ def add_server_args(p: argparse.ArgumentParser, host: str, port: int) -> None:
90
90
  "--runs-dir",
91
91
  default=None,
92
92
  metavar="DIR",
93
- help="SfS runs listed in the Settings tab (default: LUNAR_RUNS_DIR).",
93
+ help="SfS runs listed in the Settings panel (default: LUNAR_RUNS_DIR).",
94
94
  )
95
95
  p.add_argument(
96
96
  "--admin-token",
@@ -116,7 +116,7 @@ Examples
116
116
  lunar serve --host 0.0.0.0 # share on the network; remote clients are read-only
117
117
  lunar serve --host 0.0.0.0 --admin-token auto # ... and print a token for remote admin
118
118
 
119
- Admin (the Settings tab: build indexes, download, see local paths) is allowed from this
119
+ Admin (the Settings panel: build indexes, download, see local paths) is allowed from this
120
120
  machine (loopback) and for requests carrying --admin-token; everyone else can browse,
121
121
  search and view. Use `lunar app` for a desktop window.
122
122
  """,
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Lunar Toolkit Browser</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dockview@8.4.0/dist/styles/dockview.css">
7
8
  <style>
8
9
  /* Classic desktop look: flat grey panels, white sunken fields, one-pixel edges, square
9
10
  corners, a navy accent. */
@@ -60,16 +61,6 @@
60
61
  .main { display: grid; grid-template-columns: 340px minmax(0, 1fr); min-height: 0; }
61
62
 
62
63
  .controls { display: flex; flex-direction: column; min-height: 0; padding: 6px 3px 6px 6px; }
63
- .tabbar { display: flex; position: relative; z-index: 1; }
64
- .tabbar button {
65
- font: inherit; color: var(--muted); padding: 3px 12px; margin-right: -1px; min-width: 0; min-height: 0;
66
- background: var(--chrome); border: 1px solid var(--border); border-bottom: none; cursor: default;
67
- }
68
- .tabbar button:hover { background: var(--hover); }
69
- .tabbar button[aria-selected="true"] {
70
- color: var(--text); background: var(--window); margin-bottom: -1px; padding-bottom: 4px; position: relative; z-index: 1;
71
- }
72
- .tabbar button:focus-visible { outline: 1px dotted var(--text); outline-offset: -4px; }
73
64
  .panel {
74
65
  flex: 1; min-height: 0; overflow: auto; border: 1px solid var(--border);
75
66
  background: var(--window); padding: 2px 8px 8px;
@@ -120,29 +111,55 @@
120
111
  button:disabled { color: var(--disabled); }
121
112
  button.tool { min-width: 0; padding: 0 8px; min-height: 20px; font-size: 12px; }
122
113
 
123
- /* Workspace: map over (results | details), with splitters */
124
- .workspace {
125
- display: grid; grid-template-rows: minmax(140px, var(--map-h, 55%)) 6px minmax(0, 1fr);
126
- min-height: 0; padding: 6px 6px 4px 3px;
114
+ /* Workspace: dockview panels (Footprints, Image, Settings over Results | Details by default) */
115
+ .workspace { min-height: 0; min-width: 0; padding: 6px 6px 4px 3px; }
116
+ .dock { height: 100%; }
117
+ .dockpanel { display: flex; flex-direction: column; height: 100%; min-height: 0; min-width: 0; background: var(--base); }
118
+ .dockview-theme-light {
119
+ --dv-group-view-background-color: var(--base);
120
+ --dv-tabs-and-actions-container-background-color: transparent;
121
+ --dv-tabs-and-actions-container-height: 23px;
122
+ --dv-tabs-and-actions-container-font-size: 13px;
123
+ --dv-activegroup-visiblepanel-tab-background-color: var(--window);
124
+ --dv-inactivegroup-visiblepanel-tab-background-color: var(--window);
125
+ --dv-activegroup-hiddenpanel-tab-background-color: var(--chrome);
126
+ --dv-inactivegroup-hiddenpanel-tab-background-color: var(--chrome);
127
+ --dv-activegroup-visiblepanel-tab-color: var(--text);
128
+ --dv-inactivegroup-visiblepanel-tab-color: var(--text);
129
+ --dv-activegroup-hiddenpanel-tab-color: var(--muted);
130
+ --dv-inactivegroup-hiddenpanel-tab-color: var(--muted);
131
+ --dv-tab-divider-color: transparent;
132
+ --dv-separator-border: transparent;
133
+ --dv-sash-color: transparent;
134
+ --dv-active-sash-color: color-mix(in srgb, var(--accent) 30%, transparent);
135
+ --dv-active-sash-transition-delay: 0s;
136
+ --dv-drag-over-background-color: color-mix(in srgb, var(--accent) 22%, transparent);
137
+ --dv-drag-over-border-color: var(--accent);
138
+ --dv-tabs-container-scrollbar-color: var(--border);
139
+ color: var(--text);
140
+ }
141
+ .dock .dv-tabs-and-actions-container { background: var(--chrome); box-shadow: inset 0 -1px 0 var(--border); }
142
+ .dock .dv-tab {
143
+ padding: 0 12px; margin-right: -1px; border: 1px solid var(--border); cursor: default;
127
144
  }
128
- .lower { display: grid; grid-template-columns: minmax(0, 1fr) 6px minmax(200px, var(--det-w, 380px)); min-height: 0; }
129
- .pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; border: 1px solid var(--border); background: var(--base); }
145
+ .dock .dv-tab.dv-inactive-tab:hover { background: var(--hover); }
146
+ .dock .dv-tab.dv-active-tab { border-bottom-color: var(--window); position: relative; z-index: 1; }
147
+ .dock .dv-tab .dv-default-tab { padding: 0; }
148
+ .dock .dv-tab .dv-default-tab .dv-default-tab-action { display: none; }
149
+ .dock .dv-tab:focus-visible::after { outline: 1px dotted var(--text) !important; outline-offset: -4px; }
150
+ .dock .dv-content-container { border: 1px solid var(--border); border-top: none; }
130
151
  .panehead {
131
- display: flex; align-items: center; gap: 6px; padding: 1px 2px 1px 6px; min-height: 23px;
152
+ display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; padding: 1px 2px 1px 6px; min-height: 23px;
132
153
  background: var(--header); border-bottom: 1px solid var(--sep);
133
154
  }
134
- .panehead .title { font-weight: bold; }
135
155
  .panehead .info { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
136
- .panehead .right { margin-left: auto; display: flex; gap: 3px; }
156
+ .panehead .right { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 3px; }
157
+ #view-results .panehead:has(> #nav:empty):has(> #results-loading[hidden]) { display: none; }
137
158
  .panebody { flex: 1; min-height: 0; overflow: auto; position: relative; }
138
159
  .panebody:empty::before, .mapbox:empty::before {
139
160
  content: attr(data-empty); position: absolute; inset: 0; display: flex; align-items: center;
140
161
  justify-content: center; color: var(--disabled); padding: 1em; text-align: center;
141
162
  }
142
- .splitter { position: relative; touch-action: none; }
143
- .splitter.rows { cursor: row-resize; }
144
- .splitter.cols { cursor: col-resize; }
145
- .splitter:hover { background: color-mix(in srgb, var(--accent) 30%, transparent); }
146
163
 
147
164
  /* Tables */
148
165
  table { border-collapse: separate; border-spacing: 0; }
@@ -220,11 +237,8 @@
220
237
  .map text.gl { fill: var(--muted); font-size: 10.5px; }
221
238
  .map text.axis { font-size: 11.5px; }
222
239
 
223
- /* Top pane: Footprints | Image | Settings tabs; the image view is a framed canvas under a tool bar */
224
- .viewwrap { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
225
- .viewwrap > .pane { flex: 1; }
226
- .viewbody { flex: 1; min-height: 0; display: flex; flex-direction: column; }
227
- .viewbody > .mapbox { flex: 1; min-height: 0; height: auto; }
240
+ /* The image view is a framed canvas under a tool bar */
241
+ .dockpanel > .mapbox { flex: 1; min-height: 0; height: auto; }
228
242
  .toolbar {
229
243
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px; padding: 2px 4px;
230
244
  background: var(--window); border-bottom: 1px solid var(--sep);
@@ -276,9 +290,9 @@
276
290
  .statusbar .cell { border-left: 1px solid var(--sep); }
277
291
  .statusbar #cursor { min-width: 22em; font-family: "DejaVu Sans Mono", Consolas, "Liberation Mono", monospace; font-size: 12px; }
278
292
 
279
- /* Settings view: group boxes over the whole workspace */
280
- .workspace.settings-on { grid-template-rows: minmax(0, 1fr); }
281
- .workspace.settings-on > .splitter, .workspace.settings-on > .lower { display: none; }
293
+ .statusbar .layout { display: flex; gap: 3px; padding: 0 0 0 4px; }
294
+
295
+ /* Settings view: group boxes */
282
296
  .settings {
283
297
  flex: 1; min-height: 0; overflow: auto; padding: 0 10px 10px; background: var(--window);
284
298
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 460px), 1fr)); gap: 0 16px; align-content: start;
@@ -329,16 +343,11 @@
329
343
  .main { grid-template-columns: minmax(0, 1fr); }
330
344
  .controls { padding: 6px; }
331
345
  .panel { overflow: visible; }
332
- .workspace { display: flex; flex-direction: column; gap: 6px; padding: 0 6px 6px; }
333
- .workspace > .viewwrap { height: 420px; flex: none; }
334
- .workspace.settings-on > .viewwrap { height: auto; }
335
- .settings { overflow: visible; }
336
- .lower { display: flex; flex-direction: column; gap: 6px; }
337
- .lower > .pane { height: 320px; flex: none; }
338
- .splitter { display: none; }
346
+ .workspace { height: 1110px; padding: 0 6px 6px; }
339
347
  .statusbar { position: sticky; bottom: 0; flex-wrap: wrap; }
340
348
  .statusbar .status { flex-basis: 100%; white-space: normal; }
341
349
  .statusbar #cursor { min-width: 0; }
350
+ #layout-side { display: none; }
342
351
  }
343
352
  </style>
344
353
  </head>
@@ -417,171 +426,160 @@
417
426
  </div>
418
427
  </aside>
419
428
 
420
- <main class="workspace" id="workspace">
421
- <div class="viewwrap">
422
- <div class="tabbar" role="tablist" aria-label="View">
423
- <button role="tab" id="vtab-map" data-view="map" aria-selected="true" aria-controls="view-map">Footprints</button>
424
- <button role="tab" id="vtab-image" data-view="image" aria-selected="false" aria-controls="view-image" tabindex="-1">Image</button>
425
- <button role="tab" id="vtab-settings" data-view="settings" aria-selected="false" aria-controls="view-settings" tabindex="-1">Settings</button>
429
+ <main class="workspace" id="workspace"><div class="dock" id="dock"></div></main>
430
+ <div id="dock-park" hidden>
431
+ <div class="dockpanel" id="view-map">
432
+ <div class="mapbox" id="map" data-empty="No footprints. Run a search or look up product IDs."></div>
433
+ </div>
434
+ <div class="dockpanel" id="view-image">
435
+ <div class="panehead">
436
+ <span class="loading" id="view-loading" hidden></span>
437
+ <span class="info" id="view-info">No image</span>
426
438
  </div>
427
- <section class="pane">
428
- <div class="viewbody" id="view-map" role="tabpanel" aria-labelledby="vtab-map">
429
- <div class="mapbox" id="map" data-empty="No footprints. Run a search or look up product IDs."></div>
430
- </div>
431
- <div class="viewbody" id="view-image" role="tabpanel" aria-labelledby="vtab-image" hidden>
432
- <div class="panehead">
433
- <span class="title">Image</span>
434
- <span class="loading" id="view-loading" hidden></span>
435
- <span class="info" id="view-info">No image</span>
439
+ <div class="toolbar" role="toolbar" aria-label="Image view">
440
+ <select id="view-source" aria-label="Image source" title="Image source" disabled><option value="">(no image)</option></select>
441
+ <button class="tool" id="view-open" type="button" title="Open a local GeoTIFF / COG (or PNG / JPEG)">Open file...</button>
442
+ <input id="view-file" type="file" accept=".tif,.tiff,image/tiff,.png,.jpg,.jpeg" hidden>
443
+ <span class="sep"></span>
444
+ <button class="tool" id="view-zoom-out" type="button" title="Zoom out (-)">-</button>
445
+ <button class="tool" id="view-zoom-in" type="button" title="Zoom in (+)">+</button>
446
+ <button class="tool" id="view-1to1" type="button" title="Full resolution: one image pixel per screen pixel (0)">1:1</button>
447
+ <button class="tool" id="view-fit" type="button" title="Fit the whole image (F)">Fit</button>
448
+ <button class="tool" id="view-fit-width" type="button" title="Fit the image width (W)">Fit width</button>
449
+ <span class="sep"></span>
450
+ <span class="lbl">Rotate</span>
451
+ <button class="tool" id="view-rot-left" type="button" title="Rotate 90 deg counterclockwise (Shift+R; [ ] rotate 5 deg)">-90</button>
452
+ <button class="tool" id="view-rot-right" type="button" title="Rotate 90 deg clockwise (R)">+90</button>
453
+ <input id="view-rot" type="number" step="any" value="0" aria-label="Rotation angle" title="Rotation in degrees, clockwise">
454
+ <span class="sep"></span>
455
+ <label class="lbl" for="view-min">Min</label><input id="view-min" type="number" step="any" disabled>
456
+ <label class="lbl" for="view-max">Max</label><input id="view-max" type="number" step="any" disabled>
457
+ <label class="lbl" for="view-gamma">Gamma</label><input id="view-gamma" type="number" step="0.1" min="0.1" max="10" value="1" disabled>
458
+ <button class="tool" id="view-auto" type="button" title="Stretch the 2nd - 98th percentile of the overview" disabled>Auto</button>
459
+ </div>
460
+ <div class="viewframe" id="view-frame">
461
+ <canvas id="view-canvas" tabindex="0" aria-label="Image (wheel zoom, drag pan, + - 0 F W R [ ] keys)"></canvas>
462
+ <div class="viewmsg" id="view-msg"><span>Select a product with a browse image (LROC NAC / WAC pyramid, ShadowCam COG,
463
+ Kaguya JPEG), open a local GeoTIFF, or start <code>lunar serve --images DIR</code>.</span></div>
464
+ </div>
465
+ </div>
466
+ <div class="dockpanel" id="view-settings">
467
+ <div class="panehead">
468
+ <span class="info" id="set-info">Needs lunar serve or lunar app</span>
469
+ <span class="right"><button class="tool" id="set-refresh" type="button" title="Read the status again (sizes, tools)">Refresh</button></span>
470
+ </div>
471
+ <div class="settings" id="settings">
472
+ <p class="note wide" id="set-note" hidden></p>
473
+
474
+ <fieldset class="wide">
475
+ <legend>Data and indexes</legend>
476
+ <div class="path" id="set-data-dir"></div>
477
+ <div class="tablebox" id="set-indexes"></div>
478
+ <div class="row">
479
+ <label class="lbl" for="set-release">Release</label>
480
+ <input id="set-release" type="text" class="mono" style="max-width: 24em" placeholder="newest (e.g. LROLRC_0066C)" spellcheck="false" data-admin>
481
+ <span class="checks"><label><input id="set-volume-only" type="checkbox" data-admin> Volume index only</label></span>
436
482
  </div>
437
- <div class="toolbar" role="toolbar" aria-label="Image view">
438
- <select id="view-source" aria-label="Image source" title="Image source" disabled><option value="">(no image)</option></select>
439
- <button class="tool" id="view-open" type="button" title="Open a local GeoTIFF / COG (or PNG / JPEG)">Open file...</button>
440
- <input id="view-file" type="file" accept=".tif,.tiff,image/tiff,.png,.jpg,.jpeg" hidden>
441
- <span class="sep"></span>
442
- <button class="tool" id="view-zoom-out" type="button" title="Zoom out (-)">-</button>
443
- <button class="tool" id="view-zoom-in" type="button" title="Zoom in (+)">+</button>
444
- <button class="tool" id="view-1to1" type="button" title="Full resolution: one image pixel per screen pixel (0)">1:1</button>
445
- <button class="tool" id="view-fit" type="button" title="Fit the whole image (F)">Fit</button>
446
- <button class="tool" id="view-fit-width" type="button" title="Fit the image width (W)">Fit width</button>
447
- <span class="sep"></span>
448
- <span class="lbl">Rotate</span>
449
- <button class="tool" id="view-rot-left" type="button" title="Rotate 90 deg counterclockwise (Shift+R; [ ] rotate 5 deg)">-90</button>
450
- <button class="tool" id="view-rot-right" type="button" title="Rotate 90 deg clockwise (R)">+90</button>
451
- <input id="view-rot" type="number" step="any" value="0" aria-label="Rotation angle" title="Rotation in degrees, clockwise">
452
- <span class="sep"></span>
453
- <label class="lbl" for="view-min">Min</label><input id="view-min" type="number" step="any" disabled>
454
- <label class="lbl" for="view-max">Max</label><input id="view-max" type="number" step="any" disabled>
455
- <label class="lbl" for="view-gamma">Gamma</label><input id="view-gamma" type="number" step="0.1" min="0.1" max="10" value="1" disabled>
456
- <button class="tool" id="view-auto" type="button" title="Stretch the 2nd - 98th percentile of the overview" disabled>Auto</button>
483
+ <p class="hint">Build downloads the cumulative CUMINDEX.TAB (EDR about 3.8 GB) and writes the Parquet with
484
+ DuckDB; "Volume index only" uses one release's INDEX.TAB (about 20 k rows). Update appends the volumes
485
+ released since the build. Same as <code>lunar build-db</code>.</p>
486
+ </fieldset>
487
+
488
+ <fieldset class="wide">
489
+ <legend>Jobs</legend>
490
+ <div class="tablebox" id="set-jobs"></div>
491
+ <p class="hint" id="set-jobs-empty">No jobs yet. Builds and downloads run here in the background (two at a time)
492
+ and resume from their .part files when started again.</p>
493
+ </fieldset>
494
+
495
+ <fieldset>
496
+ <legend>Reference data</legend>
497
+ <div class="row">
498
+ <label class="lbl" for="set-region">Region</label>
499
+ <select id="set-region" data-admin></select>
457
500
  </div>
458
- <div class="viewframe" id="view-frame">
459
- <canvas id="view-canvas" tabindex="0" aria-label="Image (wheel zoom, drag pan, + - 0 F W R [ ] keys)"></canvas>
460
- <div class="viewmsg" id="view-msg">Select a product with a browse image (LROC NAC / WAC pyramid, ShadowCam COG,
461
- Kaguya JPEG), open a local GeoTIFF, or start <code>lunar serve --images DIR</code>.</div>
501
+ <div class="row">
502
+ <span class="lbl">PGDA LOLA 5 m</span>
503
+ <span class="checks" id="set-pgda-kinds"></span>
504
+ <span class="push"><button id="set-pgda-btn" type="button" data-admin>Download</button></span>
462
505
  </div>
463
- </div>
464
- <div class="viewbody" id="view-settings" role="tabpanel" aria-labelledby="vtab-settings" hidden>
465
- <div class="panehead">
466
- <span class="title">Settings</span>
467
- <span class="info" id="set-info">Needs lunar serve or lunar app</span>
468
- <span class="right"><button class="tool" id="set-refresh" type="button" title="Read the status again (sizes, tools)">Refresh</button></span>
506
+ <div class="row">
507
+ <label class="lbl" for="set-sdem-pat">SfS DEM members</label>
508
+ <input id="set-sdem-pat" type="text" class="mono" value="GLDELEV" spellcheck="false" data-admin
509
+ title="Product codes / substrings or globs, e.g. GLDELEV GLDSIGM README">
510
+ <span class="push">
511
+ <button id="set-sdem-list" type="button" data-admin title="Read the Zenodo zip directory (HTTP Range)">List members</button>
512
+ <button id="set-sdem-btn" type="button" data-admin>Extract</button>
513
+ </span>
469
514
  </div>
470
- <div class="settings" id="settings">
471
- <p class="note wide" id="set-note" hidden></p>
472
-
473
- <fieldset class="wide">
474
- <legend>Data and indexes</legend>
475
- <div class="path" id="set-data-dir"></div>
476
- <div class="tablebox" id="set-indexes"></div>
477
- <div class="row">
478
- <label class="lbl" for="set-release">Release</label>
479
- <input id="set-release" type="text" class="mono" style="max-width: 24em" placeholder="newest (e.g. LROLRC_0066C)" spellcheck="false" data-admin>
480
- <span class="checks"><label><input id="set-volume-only" type="checkbox" data-admin> Volume index only</label></span>
481
- </div>
482
- <p class="hint">Build downloads the cumulative CUMINDEX.TAB (EDR about 3.8 GB) and writes the Parquet with
483
- DuckDB; "Volume index only" uses one release's INDEX.TAB (about 20 k rows). Update appends the volumes
484
- released since the build. Same as <code>lunar build-db</code>.</p>
485
- </fieldset>
486
-
487
- <fieldset class="wide">
488
- <legend>Jobs</legend>
489
- <div class="tablebox" id="set-jobs"></div>
490
- <p class="hint" id="set-jobs-empty">No jobs yet. Builds and downloads run here in the background (two at a time)
491
- and resume from their .part files when started again.</p>
492
- </fieldset>
493
-
494
- <fieldset>
495
- <legend>Reference data</legend>
496
- <div class="row">
497
- <label class="lbl" for="set-region">Region</label>
498
- <select id="set-region" data-admin></select>
499
- </div>
500
- <div class="row">
501
- <span class="lbl">PGDA LOLA 5 m</span>
502
- <span class="checks" id="set-pgda-kinds"></span>
503
- <span class="push"><button id="set-pgda-btn" type="button" data-admin>Download</button></span>
504
- </div>
505
- <div class="row">
506
- <label class="lbl" for="set-sdem-pat">SfS DEM members</label>
507
- <input id="set-sdem-pat" type="text" class="mono" value="GLDELEV" spellcheck="false" data-admin
508
- title="Product codes / substrings or globs, e.g. GLDELEV GLDSIGM README">
509
- <span class="push">
510
- <button id="set-sdem-list" type="button" data-admin title="Read the Zenodo zip directory (HTTP Range)">List members</button>
511
- <button id="set-sdem-btn" type="button" data-admin>Extract</button>
512
- </span>
513
- </div>
514
- <select id="set-sdem-members" multiple aria-label="Zip members" hidden data-admin></select>
515
- <div class="tablebox" id="set-reference"></div>
516
- <p class="hint" id="set-ref-dir"></p>
517
- </fieldset>
518
-
519
- <fieldset>
520
- <legend>Downloads</legend>
521
- <textarea id="set-dl-ids" class="ids" rows="3" spellcheck="false" aria-label="Product IDs to download"
522
- placeholder="M1096757719LE M1096757719RE ..." data-admin></textarea>
523
- <div class="row">
524
- <label class="lbl" for="set-dl-product">Product</label>
525
- <select id="set-dl-product" style="flex: 0 0 auto; width: 7em" data-admin>
526
- <option value="edr">EDR</option>
527
- <option value="cdr">CDR</option>
528
- </select>
529
- <label class="lbl" for="set-dl-dest">To</label>
530
- <input id="set-dl-dest" type="text" class="mono" spellcheck="false" data-admin>
531
- <span class="push"><button id="set-dl-btn" type="button" data-admin>Download</button></span>
532
- </div>
533
- <div class="tablebox" id="set-downloads"></div>
534
- <div class="path" id="set-cache-dir" style="margin-top: 6px"></div>
535
- <div class="tablebox" id="set-cache"></div>
536
- </fieldset>
537
-
538
- <fieldset>
539
- <legend>Environment</legend>
540
- <div class="tablebox" id="set-env" style="max-height: none"></div>
541
- </fieldset>
542
-
543
- <fieldset>
544
- <legend>Runs</legend>
545
- <div class="path" id="set-runs-dir"></div>
546
- <div class="tablebox" id="set-runs"></div>
547
- </fieldset>
515
+ <select id="set-sdem-members" multiple aria-label="Zip members" hidden data-admin></select>
516
+ <div class="tablebox" id="set-reference"></div>
517
+ <p class="hint" id="set-ref-dir"></p>
518
+ </fieldset>
519
+
520
+ <fieldset>
521
+ <legend>Downloads</legend>
522
+ <textarea id="set-dl-ids" class="ids" rows="3" spellcheck="false" aria-label="Product IDs to download"
523
+ placeholder="M1096757719LE M1096757719RE ..." data-admin></textarea>
524
+ <div class="row">
525
+ <label class="lbl" for="set-dl-product">Product</label>
526
+ <select id="set-dl-product" style="flex: 0 0 auto; width: 7em" data-admin>
527
+ <option value="edr">EDR</option>
528
+ <option value="cdr">CDR</option>
529
+ </select>
530
+ <label class="lbl" for="set-dl-dest">To</label>
531
+ <input id="set-dl-dest" type="text" class="mono" spellcheck="false" data-admin>
532
+ <span class="push"><button id="set-dl-btn" type="button" data-admin>Download</button></span>
548
533
  </div>
549
- </div>
550
- </section>
534
+ <div class="tablebox" id="set-downloads"></div>
535
+ <div class="path" id="set-cache-dir" style="margin-top: 6px"></div>
536
+ <div class="tablebox" id="set-cache"></div>
537
+ </fieldset>
538
+
539
+ <fieldset>
540
+ <legend>Environment</legend>
541
+ <div class="tablebox" id="set-env" style="max-height: none"></div>
542
+ </fieldset>
543
+
544
+ <fieldset>
545
+ <legend>Runs</legend>
546
+ <div class="path" id="set-runs-dir"></div>
547
+ <div class="tablebox" id="set-runs"></div>
548
+ </fieldset>
549
+ </div>
551
550
  </div>
552
- <div class="splitter rows" id="split-rows" role="separator" aria-orientation="horizontal" title="Drag to resize"></div>
553
- <div class="lower" id="lower">
554
- <section class="pane">
555
- <div class="panehead">
556
- <span class="title">Results</span>
557
- <span class="loading" id="results-loading" hidden>Loading...</span>
558
- <span class="right" id="nav"></span>
559
- </div>
560
- <div class="panebody" id="results" data-empty="No results"></div>
561
- </section>
562
- <div class="splitter cols" id="split-cols" role="separator" aria-orientation="vertical" title="Drag to resize"></div>
563
- <section class="pane">
564
- <div class="panehead">
565
- <span class="title">Details</span>
566
- <span class="right"><button class="tool" id="detail-clear" type="button">Clear</button></span>
567
- </div>
568
- <div class="panebody" id="detail" data-empty="Click a row or footprint"></div>
569
- </section>
551
+ <div class="dockpanel" id="view-results">
552
+ <div class="panehead">
553
+ <span class="loading" id="results-loading" hidden>Loading...</span>
554
+ <span class="right" id="nav"></span>
555
+ </div>
556
+ <div class="panebody" id="results" data-empty="No results"></div>
570
557
  </div>
571
- </main>
558
+ <div class="dockpanel" id="view-details">
559
+ <div class="panehead">
560
+ <span class="right"><button class="tool" id="detail-clear" type="button">Clear</button></span>
561
+ </div>
562
+ <div class="panebody" id="detail" data-empty="Click a row or footprint"></div>
563
+ </div>
564
+ </div>
572
565
  </div>
573
566
 
574
567
  <footer class="statusbar">
575
568
  <span class="status busy" id="status">Initializing DuckDB...</span>
576
569
  <span class="cell" id="count">0 rows</span>
577
570
  <span class="cell" id="view-zoom" hidden></span>
578
- <span class="cell" id="jobs-summary" title="Open the Settings tab" hidden></span>
571
+ <span class="cell" id="jobs-summary" title="Open the Settings panel" hidden></span>
579
572
  <span class="cell" id="cursor"></span>
573
+ <span class="cell layout">
574
+ <button class="tool" id="layout-side" type="button" title="Footprints and Image side by side, Results and Details below">Map | Image</button>
575
+ <button class="tool" id="layout-reset" type="button" title="Restore the default panel layout">Reset layout</button>
576
+ </span>
580
577
  </footer>
581
578
  </div>
582
579
 
583
580
  <script type="module">
584
581
  import * as duckdb from 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/+esm';
582
+ import { createDockview, themeLight } from 'https://cdn.jsdelivr.net/npm/dockview-core@8.4.0/+esm';
585
583
 
586
584
  // Mirrors lunar_toolkit.lroc.nac.METADATA_LABELS (PDS_URL is derived below);
587
585
  // tests/test_static.py keeps the two in sync.
@@ -736,27 +734,6 @@
736
734
  $('count').textContent = `${n} row${n === 1 ? '' : 's'}`;
737
735
  }
738
736
 
739
- // Splitters: map | lower pane (rows), results | details (columns)
740
- function splitter(handle, onmove) {
741
- handle.addEventListener('pointerdown', (e) => {
742
- e.preventDefault();
743
- handle.setPointerCapture(e.pointerId);
744
- const move = (ev) => onmove(ev);
745
- const up = () => { handle.removeEventListener('pointermove', move); handle.removeEventListener('pointerup', up); };
746
- handle.addEventListener('pointermove', move);
747
- handle.addEventListener('pointerup', up);
748
- });
749
- }
750
- splitter($('split-rows'), (e) => {
751
- const r = $('workspace').getBoundingClientRect();
752
- const h = Math.min(Math.max(e.clientY - r.top - 6, 140), r.height - 120);
753
- $('workspace').style.setProperty('--map-h', `${h}px`);
754
- });
755
- splitter($('split-cols'), (e) => {
756
- const r = $('lower').getBoundingClientRect();
757
- const w = Math.min(Math.max(r.right - e.clientX - 3, 200), r.width - 200);
758
- $('lower').style.setProperty('--det-w', `${w}px`);
759
- });
760
737
  $('detail-clear').addEventListener('click', () => $('detail').replaceChildren());
761
738
 
762
739
  // Geometry: ports of lunar_toolkit.geometry (query_region, aoi_region, aoi_mask), so the
@@ -1454,7 +1431,6 @@
1454
1431
  const tools = el('span', { class: 'tools' }, ...Object.entries(MAP_TOOLS).map(([t, [label, title]]) =>
1455
1432
  el('button', { class: `tool${t === 'pan' ? ' on' : ''}`, type: 'button', 'data-tool': t, title }, label)));
1456
1433
  const bar = el('div', { class: 'panehead' },
1457
- el('span', { class: 'title' }, 'Footprints'),
1458
1434
  el('span', { class: 'info' }, generic ? `${MAP_PROJECTIONS[pj.id]}, R = ${MOON_RADIUS_KM} km`
1459
1435
  : hemi ? `Polar stereographic (${hemi === 'S' ? 'south' : 'north'}), R = ${MOON_RADIUS_KM} km`
1460
1436
  : 'Lon/lat (equirectangular)', ` | ${items.length} footprint(s) | wheel zoom, drag pan`),
@@ -2743,7 +2719,6 @@
2743
2719
  };
2744
2720
  const viewCanvas = $('view-canvas');
2745
2721
  const frameSize = () => [viewCanvas.clientWidth, viewCanvas.clientHeight];
2746
- const viewActive = () => ($('view-image').hidden ? 'map' : 'image');
2747
2722
  const dpr = () => window.devicePixelRatio || 1;
2748
2723
 
2749
2724
  function viewMessage(text, isError = false) {
@@ -3613,46 +3588,95 @@
3613
3588
  }
3614
3589
  }
3615
3590
 
3616
- const viewTabs = () => [$('vtab-map'), $('vtab-image'), $('vtab-settings')];
3617
- function selectView(name) {
3618
- for (const t of viewTabs()) {
3619
- const on = t.dataset.view === name;
3620
- t.setAttribute('aria-selected', String(on));
3621
- t.tabIndex = on ? 0 : -1;
3622
- }
3623
- $('view-map').hidden = name !== 'map';
3624
- $('view-image').hidden = $('view-zoom').hidden = name !== 'image';
3625
- $('view-settings').hidden = name !== 'settings';
3626
- $('workspace').classList.toggle('settings-on', name === 'settings');
3627
- $('cursor').textContent = '';
3628
- if (name === 'settings') settingsShown();
3629
- if (name === 'image') {
3591
+ // Docking workspace (dockview). Panels keep their DOM nodes, parked off-screen while hidden so
3592
+ // lookups by id keep working; the layout is saved per viewer.
3593
+ const PANELS = { map: 'Footprints', image: 'Image', settings: 'Settings', results: 'Results', details: 'Details' };
3594
+ const LAYOUT_KEY = 'lunar-layout-v1';
3595
+ const narrow = matchMedia('(max-width: 800px)');
3596
+ let dock = null, saveTimer = 0;
3597
+ const panelShown = (id) => !!dock?.getPanel(id)?.api.isVisible;
3598
+
3599
+ function panelVisible(id, on) {
3600
+ if (id === 'image') $('view-zoom').hidden = !on;
3601
+ if (!on) return;
3602
+ if (id === 'settings') settingsShown();
3603
+ if (id === 'image') {
3630
3604
  if (V.pending) { const p = V.pending; V.pending = null; viewProduct(p); }
3631
3605
  viewRedraw();
3632
3606
  }
3633
3607
  }
3634
3608
 
3609
+ function selectView(id) {
3610
+ $('cursor').textContent = '';
3611
+ dock.getPanel(id)?.api.setActive();
3612
+ }
3613
+
3614
+ function buildLayout(kind) {
3615
+ dock.clear();
3616
+ const add = (id, position, inactive = false) => dock.addPanel({ id, component: id, title: PANELS[id], position, inactive });
3617
+ add('map');
3618
+ add('image', { referencePanel: 'map', direction: kind === 'side' ? 'right' : 'within' }, kind !== 'side');
3619
+ add('settings', { referencePanel: 'image', direction: 'within' }, true);
3620
+ add('results', { direction: 'below' });
3621
+ add('details', { referencePanel: 'results', direction: kind === 'stacked' ? 'below' : 'right' });
3622
+ const size = (id, s) => dock.getPanel(id).group.api.setSize(s);
3623
+ if (kind === 'stacked') { size('map', { height: 440 }); size('results', { height: 320 }); }
3624
+ else { size('map', { height: Math.round(dock.height * 0.55) }); size('details', { width: 380 }); }
3625
+ }
3626
+
3627
+ function restoreLayout() {
3628
+ if (!narrow.matches) {
3629
+ try {
3630
+ dock.fromJSON(JSON.parse(localStorage.getItem(LAYOUT_KEY)));
3631
+ if (Object.keys(PANELS).every((id) => dock.getPanel(id))) return;
3632
+ } catch { /* no saved layout */ }
3633
+ }
3634
+ buildLayout(narrow.matches ? 'stacked' : 'default');
3635
+ }
3636
+
3637
+ function saveLayout() {
3638
+ if (narrow.matches) return;
3639
+ try { localStorage.setItem(LAYOUT_KEY, JSON.stringify(dock.toJSON())); } catch { /* storage blocked */ }
3640
+ }
3641
+
3642
+ function initDock() {
3643
+ const park = $('dock-park');
3644
+ dock = createDockview($('dock'), {
3645
+ theme: { ...themeLight, gap: 6 },
3646
+ disableFloatingGroups: true,
3647
+ disableTabsOverflowList: true,
3648
+ createComponent: ({ name }) => {
3649
+ const element = $(`view-${name}`);
3650
+ return {
3651
+ element,
3652
+ init: ({ api }) => api.onDidVisibilityChange(({ isVisible }) => panelVisible(name, isVisible)),
3653
+ onHide: () => park.append(element),
3654
+ dispose: () => park.append(element),
3655
+ };
3656
+ },
3657
+ });
3658
+ dock.layout($('dock').clientWidth, $('dock').clientHeight);
3659
+ restoreLayout();
3660
+ for (const id of Object.keys(PANELS)) panelVisible(id, panelShown(id));
3661
+ dock.onDidLayoutChange(() => { clearTimeout(saveTimer); saveTimer = setTimeout(saveLayout, 300); });
3662
+ narrow.addEventListener('change', restoreLayout);
3663
+ $('layout-side').addEventListener('click', () => buildLayout('side'));
3664
+ $('layout-reset').addEventListener('click', () => {
3665
+ try { localStorage.removeItem(LAYOUT_KEY); } catch { /* storage blocked */ }
3666
+ buildLayout(narrow.matches ? 'stacked' : 'default');
3667
+ });
3668
+ }
3669
+
3635
3670
  // A picked row / footprint: preview in its detail block, and the viewer follows if visible
3636
3671
  function selectProduct(block) {
3637
3672
  const prod = block._product;
3638
3673
  if (!prod) return;
3639
3674
  block._preview?._load?.();
3640
- if (viewActive() === 'image') viewProduct(prod);
3675
+ if (panelShown('image')) viewProduct(prod);
3641
3676
  else V.pending = prod;
3642
3677
  }
3643
3678
 
3644
3679
  function initViewer() {
3645
- const all = viewTabs();
3646
- for (const t of all) {
3647
- t.addEventListener('click', () => selectView(t.dataset.view));
3648
- t.addEventListener('keydown', (e) => {
3649
- const d = { ArrowRight: 1, ArrowLeft: -1 }[e.key];
3650
- if (!d) return;
3651
- const next = all[(all.indexOf(t) + d + all.length) % all.length];
3652
- selectView(next.dataset.view);
3653
- next.focus();
3654
- });
3655
- }
3656
3680
  $('view-zoom-in').addEventListener('click', () => viewZoom(2));
3657
3681
  $('view-zoom-out').addEventListener('click', () => viewZoom(0.5));
3658
3682
  $('view-1to1').addEventListener('click', () => viewSetScale(1 / dpr()));
@@ -4139,7 +4163,7 @@
4139
4163
  applyAdmin();
4140
4164
  $('set-info').textContent = S.admin ? 'Admin' : 'Read-only';
4141
4165
  watchJobs();
4142
- if (!$('view-settings').hidden) loadStatus();
4166
+ if (panelShown('settings')) loadStatus();
4143
4167
  } catch {
4144
4168
  for (const node of document.querySelectorAll('#settings [data-admin], #set-refresh')) node.disabled = true;
4145
4169
  $('set-note').hidden = false;
@@ -4148,6 +4172,7 @@
4148
4172
  }
4149
4173
 
4150
4174
  // Boot
4175
+ initDock();
4151
4176
  initSearchForm();
4152
4177
  showFormRegion();
4153
4178
  initViewer();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lunar-toolkit
3
- Version: 0.1.0.dev14
3
+ Version: 0.1.0.dev15
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
@@ -55,6 +55,8 @@ Dynamic: license-file
55
55
 
56
56
  # Lunar Toolkit
57
57
 
58
+ ![Lunar Toolkit app](https://raw.githubusercontent.com/alexmaietta/lunar-toolkit/main/docs/images/hero.png)
59
+
58
60
  Lunar Toolkit finds, downloads and processes lunar orbital imagery and turns it into DEMs.
59
61
 
60
62
  It builds a searchable index of LROC images whose footprint search also works near the poles, finds ShadowCam, LROC and Kaguya products through the PDS Orbital Data Explorer, imports images into [ISIS](https://isis.astrogeology.usgs.gov/), and builds stereo and shape-from-shading (SfS) DEMs with the [Ames Stereo Pipeline](https://stereopipeline.readthedocs.io/) through configurable, reproducible pipelines. A desktop app browses the index, ODE, footprints and full-resolution images.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lunar-toolkit"
7
- version = "0.1.0.dev14"
7
+ version = "0.1.0.dev15"
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"
@@ -130,7 +130,7 @@ def test_relay_only_pds_image_files():
130
130
 
131
131
  def test_settings_view_and_job_api_in_page():
132
132
  for needle in (
133
- 'id="vtab-settings"',
133
+ "dockview-core@8.4.0",
134
134
  'id="view-settings"',
135
135
  'id="jobs-summary"',
136
136
  "api/jobs/events",