lunar-toolkit 0.1.0.dev6__tar.gz → 0.1.0.dev11__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.
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/PKG-INFO +2 -1
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/README.md +1 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/app.py +14 -4
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/serve.py +13 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_dem_global.tif +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_dem_north.tif +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_dem_south.tif +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_shade_global.jpg +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_shade_north.jpg +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/lola_shade_south.jpg +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/manifest.json +11 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/wac_global.jpg +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/wac_north.jpg +0 -0
- lunar_toolkit-0.1.0.dev11/lunar_toolkit/static/basemaps/wac_south.jpg +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/static/index.html +340 -11
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/PKG-INFO +2 -1
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/SOURCES.txt +10 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/pyproject.toml +2 -2
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_app.py +12 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_static.py +2 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/LICENSE +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/asp.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/_jobs.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/_status.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/_utils.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/build_db.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/download.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/kaguya.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/ode_search.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/pairs.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/process.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/query.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/reference.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/search.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/shadowcam.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/cli/workspace.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/config.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/constants.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/csm.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/downloader.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/geometry.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/isis.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/kaguya/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/kaguya/mi.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/kaguya/tc.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/lroc/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/lroc/cumindex.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/lroc/nac.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/observation.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/ode.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/provenance.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/reference.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/selection.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/sfs_products.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/shadowcam/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/shadowcam/sc.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/stereo_dem.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/tools.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit/workspace.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/dependency_links.txt +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/entry_points.txt +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/requires.txt +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/top_level.txt +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/setup.cfg +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_aoi.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_build.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_config.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_csm.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_download.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_footprints.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_integration.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_kaguya.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_kaguya_mi.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_lbl.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_ode.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_reference.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_selection.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_sfs_products.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_shadowcam.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_tool_env.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_tools.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/tests/test_update.py +0 -0
- {lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/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.
|
|
3
|
+
Version: 0.1.0.dev11
|
|
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
|
|
@@ -51,6 +51,7 @@ Dynamic: license-file
|
|
|
51
51
|
[](https://alexmaietta.github.io/lunar-toolkit/coverage/)
|
|
52
52
|
[](https://pypi.org/project/lunar-toolkit/)
|
|
53
53
|
[](LICENSE)
|
|
54
|
+
[](https://alexmaietta.github.io/lunar-toolkit/browser/)
|
|
54
55
|
|
|
55
56
|
# Lunar Toolkit
|
|
56
57
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
[](https://alexmaietta.github.io/lunar-toolkit/coverage/)
|
|
3
3
|
[](https://pypi.org/project/lunar-toolkit/)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://alexmaietta.github.io/lunar-toolkit/browser/)
|
|
5
6
|
|
|
6
7
|
# Lunar Toolkit
|
|
7
8
|
|
|
@@ -54,7 +54,7 @@ Examples
|
|
|
54
54
|
|
|
55
55
|
The server runs on this machine (a free port on 127.0.0.1 by default) and stops when
|
|
56
56
|
the window closes. Window backends, first available wins (--backend auto):
|
|
57
|
-
webview pywebview: Qt WebEngine on Linux (pip install
|
|
57
|
+
webview pywebview: Qt WebEngine on Linux (pip install "lunar-toolkit[app]",
|
|
58
58
|
or PyGObject + WebKitGTK), Edge WebView2 on Windows, WebKit on macOS
|
|
59
59
|
chromium Chrome / Chromium / Edge / Brave in --app mode (own profile in the cache dir)
|
|
60
60
|
browser the default browser (Ctrl-C stops the server)
|
|
@@ -128,13 +128,16 @@ def chromium_binary(platform: str | None = None) -> str | None:
|
|
|
128
128
|
return None
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
INSTALL_HINT = 'pip install "lunar-toolkit[app]"'
|
|
132
|
+
|
|
133
|
+
|
|
131
134
|
def choose_backend(requested: str = "auto", platform: str | None = None) -> tuple[str, str | None]:
|
|
132
135
|
"""``(backend, detail)``: the pywebview GUI name, the Chromium binary, or None for the browser."""
|
|
133
136
|
if requested in ("auto", "webview"):
|
|
134
137
|
gui = webview_gui(platform)
|
|
135
138
|
if gui:
|
|
136
139
|
return "webview", gui
|
|
137
|
-
if requested in ("auto", "chromium"
|
|
140
|
+
if requested in ("auto", "chromium"):
|
|
138
141
|
binary = chromium_binary(platform)
|
|
139
142
|
if binary:
|
|
140
143
|
return "chromium", binary
|
|
@@ -171,6 +174,12 @@ def _run_chromium(url: str, binary: str) -> None:
|
|
|
171
174
|
def run(args: argparse.Namespace) -> int:
|
|
172
175
|
from .serve import make_server, print_banner
|
|
173
176
|
|
|
177
|
+
backend, detail = choose_backend(args.backend)
|
|
178
|
+
if args.backend == "webview" and backend != "webview":
|
|
179
|
+
print(
|
|
180
|
+
f"No native window toolkit for pywebview here; {INSTALL_HINT}, or use --backend chromium / browser."
|
|
181
|
+
)
|
|
182
|
+
return 1
|
|
174
183
|
try:
|
|
175
184
|
server, cfg = make_server(args)
|
|
176
185
|
except (ValueError, OSError) as exc:
|
|
@@ -179,7 +188,8 @@ def run(args: argparse.Namespace) -> int:
|
|
|
179
188
|
url = print_banner(server, cfg, args.host, TITLE)
|
|
180
189
|
thread = threading.Thread(target=server.serve_forever, name="lunar-server", daemon=True)
|
|
181
190
|
thread.start()
|
|
182
|
-
backend
|
|
191
|
+
if backend != "webview" and args.backend == "auto":
|
|
192
|
+
print(f" note : no native window toolkit; {INSTALL_HINT} for one")
|
|
183
193
|
try:
|
|
184
194
|
if backend == "webview" and detail:
|
|
185
195
|
print(f" window : pywebview ({detail})")
|
|
@@ -193,7 +203,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
193
203
|
print(f" window : {Path(detail).name} --app")
|
|
194
204
|
_run_chromium(url, detail)
|
|
195
205
|
return 0
|
|
196
|
-
print(" window :
|
|
206
|
+
print(" window : no Chrome / Chromium / Edge either; opening a browser tab")
|
|
197
207
|
print(" Ctrl-C to stop.\n")
|
|
198
208
|
webbrowser.open(url)
|
|
199
209
|
while thread.is_alive():
|
|
@@ -29,6 +29,7 @@ COMMAND = "serve"
|
|
|
29
29
|
|
|
30
30
|
_STATIC_DIR = Path(__file__).parent.parent / "static"
|
|
31
31
|
_INDEX = _STATIC_DIR / "index.html"
|
|
32
|
+
_BASEMAPS = _STATIC_DIR / "basemaps"
|
|
32
33
|
|
|
33
34
|
_IMAGE_TYPES = {
|
|
34
35
|
".tif": "image/tiff",
|
|
@@ -44,7 +45,11 @@ _RELAY_HOSTS = (
|
|
|
44
45
|
"pds.lroc.im-ldi.com",
|
|
45
46
|
"pds.shadowcam.im-ldi.com",
|
|
46
47
|
"data.darts.isas.jaxa.jp",
|
|
48
|
+
"pgda.gsfc.nasa.gov",
|
|
47
49
|
)
|
|
50
|
+
# Hosts relayed only under these path prefixes (PGDA: the LOLA DEMs for the map's contours)
|
|
51
|
+
_RELAY_PATHS = {"pgda.gsfc.nasa.gov": "/data/LOLA_20mpp/"}
|
|
52
|
+
_BASEMAP_TYPES = {**_IMAGE_TYPES, ".json": "application/json"}
|
|
48
53
|
_MAX_LISTED_IMAGES = 2000
|
|
49
54
|
RELAY_MAX = 6
|
|
50
55
|
RELAY_WAIT = 30.0
|
|
@@ -189,6 +194,7 @@ def _relay_allowed(url: str) -> bool:
|
|
|
189
194
|
and port is None
|
|
190
195
|
and not u.username
|
|
191
196
|
and Path(u.path).suffix.lower() in _IMAGE_TYPES
|
|
197
|
+
and u.path.startswith(_RELAY_PATHS.get(u.hostname, "/"))
|
|
192
198
|
)
|
|
193
199
|
|
|
194
200
|
|
|
@@ -382,6 +388,13 @@ class LunarHandler(http.server.BaseHTTPRequestHandler):
|
|
|
382
388
|
},
|
|
383
389
|
body,
|
|
384
390
|
)
|
|
391
|
+
elif path.startswith("/basemaps/"):
|
|
392
|
+
name = path[len("/basemaps/") :]
|
|
393
|
+
ctype = _BASEMAP_TYPES.get(Path(name).suffix.lower())
|
|
394
|
+
if not ctype or "/" in name or name.startswith("."):
|
|
395
|
+
self._send_code(404)
|
|
396
|
+
return
|
|
397
|
+
self._serve(_BASEMAPS / name, ctype, body)
|
|
385
398
|
elif path == "/api/images":
|
|
386
399
|
self._json(_list_images(cfg.images_dir) if cfg.images_dir else [], body)
|
|
387
400
|
elif path.startswith("/images/") and cfg.images_dir is not None:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"wac_global": {"file": "wac_global.jpg", "proj": "eqc", "bounds": [-180, -90, 180, 90]},
|
|
3
|
+
"lola_shade_global": {"file": "lola_shade_global.jpg", "proj": "eqc", "bounds": [-180, -90, 180, 90]},
|
|
4
|
+
"lola_dem_global": {"file": "lola_dem_global.tif", "proj": "eqc"},
|
|
5
|
+
"wac_south": {"file": "wac_south.jpg", "proj": "S", "bounds": [-609600, -609600, 609600, 609600]},
|
|
6
|
+
"wac_north": {"file": "wac_north.jpg", "proj": "N", "bounds": [-609600, -609600, 609600, 609600]},
|
|
7
|
+
"lola_shade_south": {"file": "lola_shade_south.jpg", "proj": "S", "bounds": [-609600, -609600, 609600, 609600]},
|
|
8
|
+
"lola_shade_north": {"file": "lola_shade_north.jpg", "proj": "N", "bounds": [-609600, -609600, 609600, 609600]},
|
|
9
|
+
"lola_dem_south": {"file": "lola_dem_south.tif", "proj": "S"},
|
|
10
|
+
"lola_dem_north": {"file": "lola_dem_north.tif", "proj": "N"}
|
|
11
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
--fp: #1f4a8a;
|
|
37
37
|
--fp-sel: #c62828;
|
|
38
38
|
--region: #2e7d32;
|
|
39
|
+
--contour: #9a6a3a;
|
|
39
40
|
--view-bg: #303030;
|
|
40
41
|
--view-fg: #e8e8e8;
|
|
41
42
|
}
|
|
@@ -178,15 +179,21 @@
|
|
|
178
179
|
/* Footprint plot */
|
|
179
180
|
.mapbox { height: 100%; position: relative; }
|
|
180
181
|
.map { display: flex; flex-direction: column; height: 100%; }
|
|
181
|
-
.map
|
|
182
|
+
.map .plotarea { position: relative; flex: 1 1 0; min-height: 0; }
|
|
183
|
+
.map svg { position: absolute; inset: 0; display: block; width: 100%; height: 100%; touch-action: none; user-select: none; cursor: crosshair; }
|
|
184
|
+
.map canvas { position: absolute; background: var(--plot-bg); pointer-events: none; }
|
|
185
|
+
.mapbox.has-basemap .plot { fill: none; }
|
|
186
|
+
.map .contour { fill: none; stroke: var(--contour); stroke-width: 0.7; stroke-opacity: 0.85; vector-effect: non-scaling-stroke; pointer-events: none; }
|
|
187
|
+
.map .contour.major { stroke-width: 1.3; }
|
|
188
|
+
.panehead select { width: auto; height: 19px; padding: 0 20px 0 4px; font-size: 12px; background-position: right 10px top 7px, right 6px top 7px, 0 0; }
|
|
182
189
|
.map svg.dragging { cursor: grabbing; }
|
|
183
190
|
.map .plot { fill: var(--plot-bg); }
|
|
184
191
|
.map .frame { fill: none; stroke: var(--plot-fg); stroke-width: 1; shape-rendering: crispEdges; }
|
|
185
192
|
.map .tick { fill: none; stroke: var(--plot-fg); stroke-width: 1; shape-rendering: crispEdges; }
|
|
186
193
|
.map .grid { fill: none; stroke: var(--plot-grid); stroke-width: 1; }
|
|
187
|
-
.map .fp { fill: var(--fp); fill-opacity: 0.
|
|
188
|
-
.map .fp:hover { fill-opacity: 0.
|
|
189
|
-
.map .fp.sel { fill: var(--fp-sel); fill-opacity: 0.
|
|
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; }
|
|
190
197
|
.mapbox.hide-fp .fp, .mapbox.hide-region .region { display: none; }
|
|
191
198
|
.panehead .toggle { display: inline-flex; align-items: center; gap: 3px; margin-right: 8px; white-space: nowrap; }
|
|
192
199
|
.map .region { fill: none; fill-rule: evenodd; stroke: var(--region); stroke-width: 1.4; stroke-dasharray: 6 3; pointer-events: none; }
|
|
@@ -1236,12 +1243,19 @@
|
|
|
1236
1243
|
const NS = 'http://www.w3.org/2000/svg';
|
|
1237
1244
|
const svg = document.createElementNS(NS, 'svg');
|
|
1238
1245
|
const reset = el('button', { class: 'tool', type: 'button', title: 'Reset view (double click)' }, 'Reset view');
|
|
1246
|
+
const canvas = el('canvas');
|
|
1247
|
+
const spin = el('span', { class: 'loading', hidden: '' });
|
|
1248
|
+
const cinfo = el('span', { class: 'info' });
|
|
1249
|
+
const pick = el('select', { 'aria-label': 'Basemap', title: 'Basemap under the footprints' });
|
|
1250
|
+
const cbox = el('input', { type: 'checkbox' });
|
|
1239
1251
|
const bar = el('div', { class: 'panehead' },
|
|
1240
1252
|
el('span', { class: 'title' }, 'Footprints'),
|
|
1241
1253
|
el('span', { class: 'info' }, hemi ? `Polar stereographic (${hemi === 'S' ? 'south' : 'north'}), R = ${MOON_RADIUS_KM} km`
|
|
1242
1254
|
: 'Lon/lat (equirectangular)', ` | ${items.length} footprint(s) | wheel zoom, drag pan`),
|
|
1243
|
-
|
|
1244
|
-
|
|
1255
|
+
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)));
|
|
1245
1259
|
const clipId = `clip-${container.id}`;
|
|
1246
1260
|
const M = MARGIN;
|
|
1247
1261
|
|
|
@@ -1269,6 +1283,7 @@
|
|
|
1269
1283
|
if (x1 - x0 < minSpan) [x0, x1] = [(x0 + x1 - minSpan) / 2, (x0 + x1 + minSpan) / 2];
|
|
1270
1284
|
if (y1 - y0 < minSpan) [y0, y1] = [(y0 + y1 - minSpan) / 2, (y0 + y1 + minSpan) / 2];
|
|
1271
1285
|
const home = { cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, w: x1 - x0, h: y1 - y0 };
|
|
1286
|
+
const under = mapUnderlay({ container, hemi, kx, canvas, spin, cinfo, pick, cbox, home, redraw: () => redraw() });
|
|
1272
1287
|
// SW x SH: whole svg; W x H: plot area inside the axis margins
|
|
1273
1288
|
let view = null, SW = 0, SH = 0, W = 0, H = 0, sel = -1, touched = false;
|
|
1274
1289
|
const fit = () => {
|
|
@@ -1364,11 +1379,13 @@
|
|
|
1364
1379
|
if (sel >= 0) order.push(sel);
|
|
1365
1380
|
svg.innerHTML = `<defs><clipPath id="${clipId}"><rect width="${W}" height="${H}"/></clipPath></defs>`
|
|
1366
1381
|
+ `<g transform="translate(${M.l} ${M.t})"><rect class="plot" width="${W}" height="${H}"/>`
|
|
1367
|
-
+ `<g clip-path="url(#${clipId})">${a.grid}
|
|
1382
|
+
+ `<g clip-path="url(#${clipId})">${a.grid}<g class="cslot"/>`
|
|
1368
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('')
|
|
1369
1384
|
+ (reg.length ? `<path class="region" d="${pathD(reg)}"/>` : '')
|
|
1370
1385
|
+ `</g><rect class="frame" x="0.5" y="0.5" width="${W - 1}" height="${H - 1}"/>`
|
|
1371
1386
|
+ `<path class="tick" d="${a.ticks}"/>${a.labels}</g>`;
|
|
1387
|
+
svg.querySelector('.cslot').replaceWith(under.contours);
|
|
1388
|
+
under.draw(view, W, H);
|
|
1372
1389
|
}
|
|
1373
1390
|
|
|
1374
1391
|
let frame = 0;
|
|
@@ -1454,9 +1471,9 @@
|
|
|
1454
1471
|
return items;
|
|
1455
1472
|
}
|
|
1456
1473
|
|
|
1457
|
-
//
|
|
1474
|
+
// The details pane shows the selected product only
|
|
1458
1475
|
function showDetail(container, block) {
|
|
1459
|
-
container.
|
|
1476
|
+
container.replaceChildren(block);
|
|
1460
1477
|
container.scrollTop = 0;
|
|
1461
1478
|
}
|
|
1462
1479
|
|
|
@@ -1949,8 +1966,8 @@
|
|
|
1949
1966
|
const MAX_OVERVIEW_PX = 2048; // remote images without a level this small get no preview
|
|
1950
1967
|
const STRIP_TILE = 512; // block size for untiled (striped) TIFFs
|
|
1951
1968
|
const TILE_REQUESTS = 4; // concurrent tile reads (PDS rate-limits with HTTP 429)
|
|
1952
|
-
// pds.mcp.nasa.gov drops its CORS header for browser requests; lunar serve relays those hosts
|
|
1953
|
-
const RELAY_FIRST = ['pds.mcp.nasa.gov', 'pds.lroc.im-ldi.com'];
|
|
1969
|
+
// pds.mcp.nasa.gov drops its CORS header for browser requests and PGDA sends none; lunar serve relays those hosts
|
|
1970
|
+
const RELAY_FIRST = ['pds.mcp.nasa.gov', 'pds.lroc.im-ldi.com', 'pgda.gsfc.nasa.gov'];
|
|
1954
1971
|
let geotiffLib = null;
|
|
1955
1972
|
let serveConfig = { relay: [], images: false };
|
|
1956
1973
|
let localImages = []; // paths from `lunar serve --images`
|
|
@@ -1961,6 +1978,9 @@
|
|
|
1961
1978
|
}));
|
|
1962
1979
|
const hostOf = (url) => { try { return new URL(url, window.location.href).hostname; } catch { return ''; } };
|
|
1963
1980
|
const relayable = (url) => /^https:/i.test(url) && serveConfig.relay.includes(hostOf(url));
|
|
1981
|
+
// The PDS image hosts block reads from other sites; without the lunar serve relay they cannot load
|
|
1982
|
+
const BLOCKED_TEXT = 'LROC images need lunar app or lunar serve, because the PDS image host blocks reads from other websites.';
|
|
1983
|
+
const blocked = (url) => serveConfig.checked && RELAY_FIRST.includes(hostOf(url)) && !relayable(url);
|
|
1964
1984
|
const relayUrl = (url) => new URL(`remote?url=${encodeURIComponent(url)}`, window.location.href).href;
|
|
1965
1985
|
const readUrl = (url, viaRelay = RELAY_FIRST.includes(hostOf(url))) => (viaRelay && relayable(url) ? relayUrl(url) : url);
|
|
1966
1986
|
const abortError = () => new DOMException('Aborted', 'AbortError');
|
|
@@ -2270,6 +2290,7 @@
|
|
|
2270
2290
|
previewCtl?.abort();
|
|
2271
2291
|
const ctl = previewCtl = new AbortController();
|
|
2272
2292
|
const spec = previewSource(prod);
|
|
2293
|
+
if (spec.url && blocked(spec.url)) return box.replaceChildren(el('span', { class: 'msg' }, BLOCKED_TEXT));
|
|
2273
2294
|
const msg = (text, cls = 'msg') => box.replaceChildren(el('span', { class: cls }, text));
|
|
2274
2295
|
msg(`Loading preview (${spec.label})`, 'msg busy');
|
|
2275
2296
|
try {
|
|
@@ -2611,6 +2632,308 @@
|
|
|
2611
2632
|
}
|
|
2612
2633
|
}
|
|
2613
2634
|
|
|
2635
|
+
// Basemaps and contours under the footprint map: a bundled overview (basemaps/, works offline),
|
|
2636
|
+
// then COG tiles streamed when zoomed in past it. The map's lon/lat (x scaled by kx) and polar
|
|
2637
|
+
// stereographic metres are the rasters' own coordinates, so placement is an affine scale.
|
|
2638
|
+
const BM_MOON = 'https://asc-pds-services.s3.us-west-2.amazonaws.com/wms_basemaps3/Moon/';
|
|
2639
|
+
const BM_SP = 'https://asc-pds-services.s3.us-west-2.amazonaws.com/mosaic/Lunar_safed/LMAP/SouthPolar/';
|
|
2640
|
+
const PGDA_80S = 'https://pgda.gsfc.nasa.gov/data/LOLA_20mpp/LDEM_80S_20MPP_ADJ.TIF';
|
|
2641
|
+
// [bundled overview, streamed COG] per map projection and basemap
|
|
2642
|
+
const BASEMAPS = {
|
|
2643
|
+
eqc: { wac: ['wac_global', `${BM_MOON}LRO_WAC_Mosaic_Global_303ppd_v3_cog.tif`],
|
|
2644
|
+
shade: ['lola_shade_global', `${BM_MOON}LRO_LOLA_Shade_Global_128ppd_v04_cog.tif`] },
|
|
2645
|
+
S: { wac: ['wac_south', `${BM_MOON}south_pole/WAC_morphology_SP_June2013_100m_8bit_cog.tif`],
|
|
2646
|
+
shade: ['lola_shade_south', `${BM_MOON}south_pole/LRO_LOLA_ClrShade_SPole75_30m_v04_pct_cog.tif`],
|
|
2647
|
+
shade5: ['lola_shade_south', `${BM_SP}ldem_87s_5mpp_hillshade_cog.tif`],
|
|
2648
|
+
nac: ['wac_south', `${BM_SP}LRO_LROC_NAC_SPOLE-90_Mosaic_1m_controlled_cog.tif`] },
|
|
2649
|
+
N: { wac: ['wac_north', `${BM_MOON}north_pole/WAC_morphology_NP_June2013_100m_8bit_cog.tif`],
|
|
2650
|
+
shade: ['lola_shade_north', `${BM_MOON}north_pole/LRO_LOLA_ClrShade_NPole75_30m_v04_pct_cog.tif`] },
|
|
2651
|
+
};
|
|
2652
|
+
const BASEMAP_LABELS = { none: 'No basemap', wac: 'WAC mosaic', shade: 'LOLA hillshade', shade5: 'LOLA 5 m hillshade', nac: 'LROC NAC 1 m mosaic' };
|
|
2653
|
+
// Contour DEMs, finest first, the bundled overview last (PGDA only through the relay)
|
|
2654
|
+
const CONTOUR_DEMS = {
|
|
2655
|
+
eqc: [`${BM_MOON}LRO_LOLA_DEM_Global_256ppd_v06_16bit_cog.tif`, 'lola_dem_global'],
|
|
2656
|
+
S: [`${BM_SP}ldem_87s_5mpp_cog.tif`, PGDA_80S, 'lola_dem_south'],
|
|
2657
|
+
N: ['lola_dem_north'],
|
|
2658
|
+
};
|
|
2659
|
+
const BM_STRETCH = { min: 0, max: 255, gamma: 1 };
|
|
2660
|
+
const mapPrefs = { basemap: 'wac', contours: false }; // remembered across searches
|
|
2661
|
+
const bmOpened = new Map(); // ref -> { promise, src, pending, failed }
|
|
2662
|
+
let bmManifest = null, bmNotify = null;
|
|
2663
|
+
const bmUrl = (file) => new URL(`basemaps/${file}`, window.location.href).href;
|
|
2664
|
+
|
|
2665
|
+
// A bundled overview (manifest name) or a remote COG; failures are retried after a minute
|
|
2666
|
+
function bmLoad(ref) {
|
|
2667
|
+
let e = bmOpened.get(ref);
|
|
2668
|
+
if (e?.failed && Date.now() - e.failed > 60000) e = null;
|
|
2669
|
+
if (e) return e;
|
|
2670
|
+
e = { pending: true };
|
|
2671
|
+
bmOpened.set(ref, e);
|
|
2672
|
+
e.promise = (async () => {
|
|
2673
|
+
let url = ref, meta = null;
|
|
2674
|
+
if (!/^https:/.test(ref)) {
|
|
2675
|
+
bmManifest ??= fetch(bmUrl('manifest.json')).then((r) => (r.ok ? r.json() : {})).catch(() => ({}));
|
|
2676
|
+
meta = (await bmManifest)[ref];
|
|
2677
|
+
if (!meta) throw new Error(`No bundled ${ref}`);
|
|
2678
|
+
url = bmUrl(meta.file);
|
|
2679
|
+
}
|
|
2680
|
+
if (meta?.bounds) {
|
|
2681
|
+
const img = await loadImg(url);
|
|
2682
|
+
return { key: url, pic: img, w0: img.naturalWidth, h0: img.naturalHeight, box: meta.bounds };
|
|
2683
|
+
}
|
|
2684
|
+
const src = await openTiff({ url });
|
|
2685
|
+
const first = src.levels[0].img;
|
|
2686
|
+
src.box ??= first.getBoundingBox();
|
|
2687
|
+
src.palette ??= Number(first.fileDirectory.getValue('PhotometricInterpretation')) === 3;
|
|
2688
|
+
return src;
|
|
2689
|
+
})().then((src) => (e.src = src), () => { e.failed = Date.now(); return null; })
|
|
2690
|
+
.finally(() => { e.pending = false; bmNotify?.(); });
|
|
2691
|
+
return e;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
async function loadBasemapTile({ key, src, li, tx, ty }, signal) {
|
|
2695
|
+
const L = src.levels[li], [x0, y0] = [tx * L.tw, ty * L.th];
|
|
2696
|
+
const win = [x0, y0, Math.min(x0 + L.tw, L.w), Math.min(y0 + L.th, L.h)];
|
|
2697
|
+
const [w, h] = [win[2] - win[0], win[3] - win[1]];
|
|
2698
|
+
const r = src.palette && !L.virtual ? await L.img.readRGB({ window: win, interleave: false, signal })
|
|
2699
|
+
: await readWindow(src, L, win, signal);
|
|
2700
|
+
const canvas = paint(Array.from(r), w, h, BM_STRETCH, src.palette ? null : src.noData);
|
|
2701
|
+
tileAdd(key, { canvas, w, h, bytes: 0 }, w * h * 4);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// Marching squares over grid parts { z, w, h, x(i), y(j) }: SVG paths (minor, major every 5th)
|
|
2705
|
+
function contourPaths(parts, noData, step, X, Y) {
|
|
2706
|
+
const out = [[], []];
|
|
2707
|
+
for (const { z, w, h, x, y } of parts) {
|
|
2708
|
+
const seg = (k, [i0, j0], [i1, j1]) => out[k % 5 === 0 ? 1 : 0].push(
|
|
2709
|
+
`M${X(x(i0)).toFixed(1)} ${Y(y(j0)).toFixed(1)}L${X(x(i1)).toFixed(1)} ${Y(y(j1)).toFixed(1)}`);
|
|
2710
|
+
for (let j = 0; j < h - 1; j++) {
|
|
2711
|
+
for (let i = 0; i < w - 1; i++) {
|
|
2712
|
+
const a = z[j * w + i], b = z[j * w + i + 1], c = z[(j + 1) * w + i + 1], d = z[(j + 1) * w + i];
|
|
2713
|
+
if (isFill(a, noData) || isFill(b, noData) || isFill(c, noData) || isFill(d, noData)) continue;
|
|
2714
|
+
const k1 = Math.floor(Math.max(a, b, c, d) / step);
|
|
2715
|
+
for (let k = Math.ceil(Math.min(a, b, c, d) / step); k <= k1; k++) {
|
|
2716
|
+
const v = k * step, p = [];
|
|
2717
|
+
if ((a < v) !== (b < v)) p.push([i + (v - a) / (b - a), j]);
|
|
2718
|
+
if ((b < v) !== (c < v)) p.push([i + 1, j + (v - b) / (c - b)]);
|
|
2719
|
+
if ((d < v) !== (c < v)) p.push([i + (v - d) / (c - d), j + 1]);
|
|
2720
|
+
if ((a < v) !== (d < v)) p.push([i, j + (v - a) / (d - a)]);
|
|
2721
|
+
if (p.length >= 2) seg(k, p[0], p[1]);
|
|
2722
|
+
if (p.length === 4) seg(k, p[2], p[3]);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return out.map((a) => a.join(''));
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// The basemap canvas and the contour layer of one footprint map
|
|
2731
|
+
function mapUnderlay({ container, hemi, kx, canvas, spin, cinfo, pick, cbox, home, redraw }) {
|
|
2732
|
+
bmNotify?.abort?.();
|
|
2733
|
+
const proj = hemi ?? 'eqc', sc = hemi ? 1 : kx, NS = 'http://www.w3.org/2000/svg';
|
|
2734
|
+
const ctl = new AbortController();
|
|
2735
|
+
const st = { wanted: [], inflight: new Set(), failed: new Map(), timer: 0, ctimer: 0, ckey: '', gen: 0, cbusy: false, contour: null };
|
|
2736
|
+
const notify = () => { if (container.isConnected) redraw(); };
|
|
2737
|
+
notify.abort = () => ctl.abort();
|
|
2738
|
+
bmNotify = notify;
|
|
2739
|
+
const contours = document.createElementNS(NS, 'g');
|
|
2740
|
+
const paths = ['contour', 'contour major'].map((c) => {
|
|
2741
|
+
const p = document.createElementNS(NS, 'path');
|
|
2742
|
+
p.setAttribute('class', c);
|
|
2743
|
+
contours.append(p);
|
|
2744
|
+
return p;
|
|
2745
|
+
});
|
|
2746
|
+
// Raster extent in map data coordinates (x right, y down)
|
|
2747
|
+
const geom = (s) => { const [x0, y0, x1, y1] = s.box; return { X0: x0 * sc, X1: x1 * sc, Y0: -y1, Y1: -y0 }; };
|
|
2748
|
+
// x offsets of the wrapped lon/lat copies of g overlapping [vx0, vx1]
|
|
2749
|
+
const copies = (g, vx0, vx1) => {
|
|
2750
|
+
if (hemi) return [0];
|
|
2751
|
+
const p = 360 * sc, n0 = Math.ceil((vx0 - g.X1) / p), n1 = Math.floor((vx1 - g.X0) / p);
|
|
2752
|
+
return n1 - n0 > 8 ? [] : Array.from({ length: n1 - n0 + 1 }, (_, i) => (n0 + i) * p);
|
|
2753
|
+
};
|
|
2754
|
+
const choices = Object.keys(BASEMAPS[proj]).filter((c) => !['shade5', 'nac'].includes(c)
|
|
2755
|
+
|| Math.max(Math.abs(home.cx) - home.w / 2, Math.abs(home.cy) - home.h / 2) < 182107);
|
|
2756
|
+
const choice = () => (choices.includes(mapPrefs.basemap) ? mapPrefs.basemap
|
|
2757
|
+
: { shade5: 'shade', nac: 'wac' }[mapPrefs.basemap] ?? 'none');
|
|
2758
|
+
pick.replaceChildren(...['none', ...choices].map((c) => el('option', { value: c }, BASEMAP_LABELS[c])));
|
|
2759
|
+
pick.value = choice();
|
|
2760
|
+
container.classList.toggle('has-basemap', choice() !== 'none');
|
|
2761
|
+
pick.addEventListener('change', () => {
|
|
2762
|
+
mapPrefs.basemap = pick.value;
|
|
2763
|
+
container.classList.toggle('has-basemap', pick.value !== 'none');
|
|
2764
|
+
redraw();
|
|
2765
|
+
});
|
|
2766
|
+
cbox.checked = mapPrefs.contours;
|
|
2767
|
+
cbox.addEventListener('change', () => {
|
|
2768
|
+
mapPrefs.contours = cbox.checked;
|
|
2769
|
+
st.ckey = '';
|
|
2770
|
+
if (!cbox.checked) { st.gen++; st.cbusy = false; st.contour = null; paths.forEach((p) => p.removeAttribute('d')); cinfo.textContent = ''; }
|
|
2771
|
+
redraw();
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2774
|
+
function status() {
|
|
2775
|
+
const n = st.inflight.size + st.wanted.length, opening = [...bmOpened.values()].some((e) => e.pending);
|
|
2776
|
+
spin.hidden = !(n || opening || st.cbusy);
|
|
2777
|
+
spin.textContent = n || opening ? 'Loading basemap...' : 'Loading contours...';
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function pump() {
|
|
2781
|
+
while (st.inflight.size < TILE_REQUESTS && st.wanted.length) {
|
|
2782
|
+
const job = st.wanted.shift();
|
|
2783
|
+
if (st.inflight.has(job.key) || tiles.has(job.key) || (st.failed.get(job.key) ?? 0) >= 3) continue;
|
|
2784
|
+
st.inflight.add(job.key);
|
|
2785
|
+
loadBasemapTile(job, ctl.signal).then(notify, () => { st.failed.set(job.key, (st.failed.get(job.key) ?? 0) + 1); })
|
|
2786
|
+
.finally(() => { st.inflight.delete(job.key); if (!ctl.signal.aborted) pump(); });
|
|
2787
|
+
}
|
|
2788
|
+
status();
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
function draw(view, W, H) {
|
|
2792
|
+
const r = dpr();
|
|
2793
|
+
Object.assign(canvas.style, { left: `${MARGIN.l}px`, top: `${MARGIN.t}px`, width: `${W}px`, height: `${H}px` });
|
|
2794
|
+
if (canvas.width !== Math.round(W * r) || canvas.height !== Math.round(H * r)) [canvas.width, canvas.height] = [Math.round(W * r), Math.round(H * r)];
|
|
2795
|
+
const ctx = canvas.getContext('2d');
|
|
2796
|
+
ctx.setTransform(r, 0, 0, r, 0, 0);
|
|
2797
|
+
ctx.clearRect(0, 0, W, H);
|
|
2798
|
+
const vx0 = view.cx - W / 2 / view.k, vx1 = view.cx + W / 2 / view.k;
|
|
2799
|
+
const vy0 = view.cy - H / 2 / view.k, vy1 = view.cy + H / 2 / view.k;
|
|
2800
|
+
// Source pixels of img (iw x ih over extent g, shifted by dx) clipped to the canvas
|
|
2801
|
+
const put = (img, iw, ih, g, dx, sharp) => {
|
|
2802
|
+
const ax = ((g.X1 - g.X0) / iw) * view.k, ay = ((g.Y1 - g.Y0) / ih) * view.k;
|
|
2803
|
+
const ox = W / 2 + (g.X0 + dx - view.cx) * view.k, oy = H / 2 + (g.Y0 - view.cy) * view.k;
|
|
2804
|
+
const u0 = Math.max(0, Math.floor(-ox / ax)), u1 = Math.min(iw, Math.ceil((W - ox) / ax));
|
|
2805
|
+
const v0 = Math.max(0, Math.floor(-oy / ay)), v1 = Math.min(ih, Math.ceil((H - oy) / ay));
|
|
2806
|
+
if (u1 <= u0 || v1 <= v0) return;
|
|
2807
|
+
const [px0, py0] = [Math.round(ox + u0 * ax), Math.round(oy + v0 * ay)];
|
|
2808
|
+
ctx.imageSmoothingEnabled = !sharp;
|
|
2809
|
+
ctx.drawImage(img, u0, v0, u1 - u0, v1 - v0, px0, py0, Math.round(ox + u1 * ax) - px0, Math.round(oy + v1 * ay) - py0);
|
|
2810
|
+
};
|
|
2811
|
+
const want = [];
|
|
2812
|
+
const stack = BASEMAPS[proj][choice()];
|
|
2813
|
+
if (stack) {
|
|
2814
|
+
const [b, c] = stack.map((ref) => bmLoad(ref).src);
|
|
2815
|
+
let bpx = Infinity, inside = !hemi;
|
|
2816
|
+
if (b) {
|
|
2817
|
+
const g = geom(b);
|
|
2818
|
+
for (const dx of copies(g, vx0, vx1)) put(b.pic, b.w0, b.h0, g, dx, false);
|
|
2819
|
+
bpx = (g.Y1 - g.Y0) / b.h0;
|
|
2820
|
+
inside ||= vx0 >= g.X0 && vx1 <= g.X1 && vy0 >= g.Y0 && vy1 <= g.Y1;
|
|
2821
|
+
}
|
|
2822
|
+
const g = c && geom(c);
|
|
2823
|
+
// Stream when the overview is magnified, or the view reaches past it
|
|
2824
|
+
if (c && (!hemi || (vx1 > g.X0 && vx0 < g.X1)) && vy1 > g.Y0 && vy0 < g.Y1) {
|
|
2825
|
+
const py = (g.Y1 - g.Y0) / c.h0, target = targetLevel(c, view.k * py);
|
|
2826
|
+
if (py * c.levels[target].fy * 1.5 < bpx || !inside) {
|
|
2827
|
+
for (let li = c.levels.length - 1; li >= target; li--) {
|
|
2828
|
+
const L = c.levels[li], ux = (g.X1 - g.X0) / L.w, uy = (g.Y1 - g.Y0) / L.h;
|
|
2829
|
+
const [ty0, ty1] = [Math.max(0, Math.floor((vy0 - g.Y0) / uy / L.th)), Math.min(Math.ceil(L.h / L.th) - 1, Math.floor((vy1 - g.Y0) / uy / L.th))];
|
|
2830
|
+
for (const dx of copies(g, vx0, vx1)) {
|
|
2831
|
+
const [tx0, tx1] = [Math.max(0, Math.floor((vx0 - g.X0 - dx) / ux / L.tw)), Math.min(Math.ceil(L.w / L.tw) - 1, Math.floor((vx1 - g.X0 - dx) / ux / L.tw))];
|
|
2832
|
+
for (let ty = ty0; ty <= ty1; ty++) {
|
|
2833
|
+
for (let tx = tx0; tx <= tx1; tx++) {
|
|
2834
|
+
const key = `bm|${c.key}|${li}|${tx}|${ty}`, t = tileGet(key);
|
|
2835
|
+
const tg = { X0: g.X0 + tx * L.tw * ux, Y0: g.Y0 + ty * L.th * uy };
|
|
2836
|
+
[tg.X1, tg.Y1] = [g.X0 + Math.min((tx + 1) * L.tw, L.w) * ux, g.Y0 + Math.min((ty + 1) * L.th, L.h) * uy];
|
|
2837
|
+
if (t) put(t.canvas, t.w, t.h, tg, dx, li === target && view.k * uy * r >= 1.5);
|
|
2838
|
+
else if (li === target) {
|
|
2839
|
+
want.push({ key, src: c, li, tx, ty, d: Math.hypot((tg.X0 + tg.X1) / 2 + dx - view.cx, (tg.Y0 + tg.Y1) / 2 - view.cy) });
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
st.wanted = want.sort((a, b) => a.d - b.d);
|
|
2849
|
+
clearTimeout(st.timer);
|
|
2850
|
+
st.timer = setTimeout(pump, 80);
|
|
2851
|
+
placeContours(view, W, H);
|
|
2852
|
+
status();
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function placeContours(view, W, H) {
|
|
2856
|
+
const c = st.contour;
|
|
2857
|
+
if (c) {
|
|
2858
|
+
const f = view.k / c.k;
|
|
2859
|
+
contours.setAttribute('transform', `translate(${(W / 2 + (c.cx - view.cx) * view.k - (f * c.W) / 2).toFixed(2)} `
|
|
2860
|
+
+ `${(H / 2 + (c.cy - view.cy) * view.k - (f * c.H) / 2).toFixed(2)}) scale(${f})`);
|
|
2861
|
+
}
|
|
2862
|
+
const key = `${view.cx}|${view.cy}|${view.k}|${W}|${H}`;
|
|
2863
|
+
if (!mapPrefs.contours || key === st.ckey) return;
|
|
2864
|
+
st.ckey = key;
|
|
2865
|
+
clearTimeout(st.ctimer);
|
|
2866
|
+
st.ctimer = setTimeout(() => computeContours({ ...view }, W, H), 300);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// DEM samples about 3 screen px apart over the view, from the coarsest adequate level
|
|
2870
|
+
async function demGrid(src, view, W, H, want) {
|
|
2871
|
+
const g = geom(src);
|
|
2872
|
+
let li = 0;
|
|
2873
|
+
src.levels.forEach((L, i) => { if (((g.Y1 - g.Y0) / L.h) <= want) li = i; });
|
|
2874
|
+
const L = src.levels[li], ux = (g.X1 - g.X0) / L.w, uy = (g.Y1 - g.Y0) / L.h;
|
|
2875
|
+
const vx0 = view.cx - W / 2 / view.k, vx1 = view.cx + W / 2 / view.k;
|
|
2876
|
+
const vy0 = view.cy - H / 2 / view.k, vy1 = view.cy + H / 2 / view.k;
|
|
2877
|
+
const r0 = Math.max(0, Math.floor((vy0 - g.Y0) / uy) - 1), r1 = Math.min(L.h, Math.ceil((vy1 - g.Y0) / uy) + 1);
|
|
2878
|
+
const parts = [];
|
|
2879
|
+
for (const dx of copies(g, vx0, vx1)) {
|
|
2880
|
+
const c0 = Math.max(0, Math.floor((vx0 - g.X0 - dx) / ux) - 1), c1 = Math.min(L.w, Math.ceil((vx1 - g.X0 - dx) / ux) + 1);
|
|
2881
|
+
if (c1 - c0 < 2 || r1 - r0 < 2) continue;
|
|
2882
|
+
const z = (await readWindow({ ...src, samples: [0] }, L, [c0, r0, c1, r1], ctl.signal))[0];
|
|
2883
|
+
parts.push({ z, w: c1 - c0, h: r1 - r0, x: (i) => g.X0 + dx + (c0 + i + 0.5) * ux, y: (j) => g.Y0 + (r0 + j + 0.5) * uy });
|
|
2884
|
+
}
|
|
2885
|
+
return { parts, mpp: hemi ? uy : uy * RAD * MOON_RADIUS_KM * 1000 };
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
async function computeContours(view, W, H) {
|
|
2889
|
+
const gen = ++st.gen;
|
|
2890
|
+
st.cbusy = true;
|
|
2891
|
+
status();
|
|
2892
|
+
try {
|
|
2893
|
+
const want = 3 / view.k;
|
|
2894
|
+
const vx0 = view.cx - W / 2 / view.k, vx1 = view.cx + W / 2 / view.k;
|
|
2895
|
+
const vy0 = view.cy - H / 2 / view.k, vy1 = view.cy + H / 2 / view.k;
|
|
2896
|
+
const refs = CONTOUR_DEMS[proj].filter((ref) => ref !== PGDA_80S || relayable(ref));
|
|
2897
|
+
const bundled = await bmLoad(refs.at(-1)).promise;
|
|
2898
|
+
const res = bundled ? (geom(bundled).Y1 - geom(bundled).Y0) / bundled.h0 : Infinity;
|
|
2899
|
+
let grid = null, name = '';
|
|
2900
|
+
for (const ref of refs) {
|
|
2901
|
+
const last = ref === refs.at(-1);
|
|
2902
|
+
if (last ? !bundled : res <= want) continue;
|
|
2903
|
+
const src = last ? bundled : await bmLoad(ref).promise;
|
|
2904
|
+
if (gen !== st.gen) return;
|
|
2905
|
+
const g = src && geom(src);
|
|
2906
|
+
if (!src || (!last && hemi && !(vx0 >= g.X0 && vx1 <= g.X1 && vy0 >= g.Y0 && vy1 <= g.Y1))) continue;
|
|
2907
|
+
try {
|
|
2908
|
+
grid = await demGrid(src, view, W, H, want);
|
|
2909
|
+
grid.noData = src.noData;
|
|
2910
|
+
name = fileName(ref.startsWith('https:') ? ref : bmUrl(ref)).replace(/(_cog)?\.tif$/i, '');
|
|
2911
|
+
break;
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
if (ctl.signal.aborted) return;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
if (gen !== st.gen) return;
|
|
2917
|
+
let lo = Infinity, hi = -Infinity;
|
|
2918
|
+
for (const { z } of grid?.parts ?? []) for (const v of z) if (!isFill(v, grid.noData)) { if (v < lo) lo = v; if (v > hi) hi = v; }
|
|
2919
|
+
if (!(hi > lo)) { st.contour = null; paths.forEach((p) => p.removeAttribute('d')); cinfo.textContent = grid ? '| no relief in view' : ''; return; }
|
|
2920
|
+
const step = step125(hi - lo, 10);
|
|
2921
|
+
const X = (x) => W / 2 + (x - view.cx) * view.k, Y = (y) => H / 2 + (y - view.cy) * view.k;
|
|
2922
|
+
const d = contourPaths(grid.parts, grid.noData, step, X, Y);
|
|
2923
|
+
paths.forEach((p, i) => p.setAttribute('d', d[i]));
|
|
2924
|
+
st.contour = { ...view, W, H };
|
|
2925
|
+
contours.removeAttribute('transform');
|
|
2926
|
+
const fmtM = (m) => (m >= 1000 ? `${+(m / 1000).toPrecision(2)} km` : `${+m.toPrecision(2)} m`);
|
|
2927
|
+
cinfo.textContent = `| contours ${step} m, bold ${5 * step} m (${name}, ${fmtM(grid.mpp)}/px)`;
|
|
2928
|
+
redraw();
|
|
2929
|
+
} finally {
|
|
2930
|
+
if (gen === st.gen) { st.cbusy = false; status(); }
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
return { draw, contours };
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2614
2937
|
// Pixel readout: 1-based sample / line at full resolution (ISIS convention); the value comes
|
|
2615
2938
|
// from the full-resolution tile, fetched after the pointer rests, meanwhile from a coarser level
|
|
2616
2939
|
function pixelAt(src, sx, sy) {
|
|
@@ -2717,6 +3040,11 @@
|
|
|
2717
3040
|
const ctl = V.ctl = new AbortController();
|
|
2718
3041
|
V.spec = spec;
|
|
2719
3042
|
fillChoices();
|
|
3043
|
+
if (spec.url && blocked(spec.url)) {
|
|
3044
|
+
V.failedSpec = spec;
|
|
3045
|
+
viewMessage(BLOCKED_TEXT);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
2720
3048
|
viewMessage(`Loading ${spec.label ?? spec.file?.name ?? ''}...`);
|
|
2721
3049
|
try {
|
|
2722
3050
|
const src = await (spec.kind === 'img' ? openPicture(spec, ctl.signal) : openTiff(spec));
|
|
@@ -2880,6 +3208,7 @@
|
|
|
2880
3208
|
if (list.ok) localImages = await list.json();
|
|
2881
3209
|
}
|
|
2882
3210
|
} catch { /* not lunar serve */ }
|
|
3211
|
+
serveConfig.checked = true;
|
|
2883
3212
|
fillChoices();
|
|
2884
3213
|
const url = new URLSearchParams(window.location.search).get('image');
|
|
2885
3214
|
if (url) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lunar-toolkit
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev11
|
|
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
|
|
@@ -51,6 +51,7 @@ Dynamic: license-file
|
|
|
51
51
|
[](https://alexmaietta.github.io/lunar-toolkit/coverage/)
|
|
52
52
|
[](https://pypi.org/project/lunar-toolkit/)
|
|
53
53
|
[](LICENSE)
|
|
54
|
+
[](https://alexmaietta.github.io/lunar-toolkit/browser/)
|
|
54
55
|
|
|
55
56
|
# Lunar Toolkit
|
|
56
57
|
|
|
@@ -56,6 +56,16 @@ lunar_toolkit/lroc/nac.py
|
|
|
56
56
|
lunar_toolkit/shadowcam/__init__.py
|
|
57
57
|
lunar_toolkit/shadowcam/sc.py
|
|
58
58
|
lunar_toolkit/static/index.html
|
|
59
|
+
lunar_toolkit/static/basemaps/lola_dem_global.tif
|
|
60
|
+
lunar_toolkit/static/basemaps/lola_dem_north.tif
|
|
61
|
+
lunar_toolkit/static/basemaps/lola_dem_south.tif
|
|
62
|
+
lunar_toolkit/static/basemaps/lola_shade_global.jpg
|
|
63
|
+
lunar_toolkit/static/basemaps/lola_shade_north.jpg
|
|
64
|
+
lunar_toolkit/static/basemaps/lola_shade_south.jpg
|
|
65
|
+
lunar_toolkit/static/basemaps/manifest.json
|
|
66
|
+
lunar_toolkit/static/basemaps/wac_global.jpg
|
|
67
|
+
lunar_toolkit/static/basemaps/wac_north.jpg
|
|
68
|
+
lunar_toolkit/static/basemaps/wac_south.jpg
|
|
59
69
|
tests/test_aoi.py
|
|
60
70
|
tests/test_app.py
|
|
61
71
|
tests/test_build.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lunar-toolkit"
|
|
7
|
-
version = "0.1.0.
|
|
7
|
+
version = "0.1.0.dev11"
|
|
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"
|
|
@@ -69,7 +69,7 @@ where = ["."]
|
|
|
69
69
|
include = ["lunar_toolkit*"]
|
|
70
70
|
|
|
71
71
|
[tool.setuptools.package-data]
|
|
72
|
-
lunar_toolkit = ["static/*.html"]
|
|
72
|
+
lunar_toolkit = ["static/*.html", "static/basemaps/*"]
|
|
73
73
|
|
|
74
74
|
[tool.pytest.ini_options]
|
|
75
75
|
testpaths = ["tests"]
|
|
@@ -106,6 +106,17 @@ def test_config_reports_admin_for_loopback_only(server, remote):
|
|
|
106
106
|
assert _json(server, "/api/config?token=wrong")[1]["admin"] is False
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def test_basemaps_are_served_with_range(server):
|
|
110
|
+
code, headers, raw = _req(server, "/basemaps/manifest.json")
|
|
111
|
+
assert code == 200 and headers["Content-Type"] == "application/json"
|
|
112
|
+
manifest = json.loads(raw)
|
|
113
|
+
assert all((serve._BASEMAPS / m["file"]).is_file() for m in manifest.values())
|
|
114
|
+
code, headers, raw = _req(server, "/basemaps/lola_dem_global.tif", headers={"Range": "bytes=0-3"})
|
|
115
|
+
assert code == 206 and raw[:2] == b"II"
|
|
116
|
+
for bad in ("/basemaps/../index.html", "/basemaps/x.txt", "/basemaps/missing.jpg"):
|
|
117
|
+
assert _req(server, bad)[0] == 404, bad
|
|
118
|
+
|
|
119
|
+
|
|
109
120
|
def test_loopback_is_admin_but_needs_a_local_host_header(server):
|
|
110
121
|
assert _json(server, "/api/config")[1]["admin"] is True
|
|
111
122
|
assert _json(server, "/api/config", headers={"Host": "evil.example:80"})[1]["admin"] is False
|
|
@@ -447,6 +458,7 @@ def test_backend_selection(monkeypatch):
|
|
|
447
458
|
assert app.choose_backend("auto", "linux") == ("browser", None)
|
|
448
459
|
binary[0] = "/usr/bin/chromium"
|
|
449
460
|
assert app.choose_backend("auto", "linux") == ("chromium", "/usr/bin/chromium")
|
|
461
|
+
assert app.choose_backend("webview", "linux") == ("browser", None)
|
|
450
462
|
present |= {"webview", "gi"}
|
|
451
463
|
assert app.choose_backend("auto", "linux") == ("webview", "gtk")
|
|
452
464
|
present |= {"qtpy", "PyQt6.QtWebEngineWidgets"}
|
|
@@ -112,7 +112,9 @@ def test_relay_only_pds_image_files():
|
|
|
112
112
|
ok = "https://pds.mcp.nasa.gov/data/store/img/lunar_reconnaissance_orbiter/pds4/lroc/x/M1_pyr.tif"
|
|
113
113
|
assert _relay_allowed(ok)
|
|
114
114
|
assert _relay_allowed("https://data.darts.isas.jaxa.jp/pub/pds3/x/browse/large/TC1.jpg")
|
|
115
|
+
assert _relay_allowed("https://pgda.gsfc.nasa.gov/data/LOLA_20mpp/LDEM_80S_20MPP_ADJ.TIF")
|
|
115
116
|
for bad in (
|
|
117
|
+
"https://pgda.gsfc.nasa.gov/other/a.tif",
|
|
116
118
|
ok.replace("https:", "http:"),
|
|
117
119
|
"https://example.com/a.tif",
|
|
118
120
|
"https://pds.mcp.nasa.gov.evil.com/a.tif",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{lunar_toolkit-0.1.0.dev6 → lunar_toolkit-0.1.0.dev11}/lunar_toolkit.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|