lunar-toolkit 0.1.0.dev13__tar.gz → 0.1.0.dev14__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.dev13 → lunar_toolkit-0.1.0.dev14}/PKG-INFO +1 -1
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/index.html +429 -76
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/PKG-INFO +1 -1
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/pyproject.toml +1 -1
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/LICENSE +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/README.md +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/asp.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/_jobs.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/_status.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/_utils.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/app.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/build_db.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/download.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/kaguya.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/ode_search.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/pairs.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/process.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/query.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/reference.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/search.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/serve.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/shadowcam.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/cli/workspace.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/config.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/constants.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/csm.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/downloader.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/geometry.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/isis.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/kaguya/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/kaguya/mi.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/kaguya/tc.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/lroc/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/lroc/cumindex.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/lroc/nac.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/observation.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/ode.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/provenance.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/reference.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/selection.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/sfs_products.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/shadowcam/__init__.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/shadowcam/sc.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_dem_global.tif +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_dem_north.tif +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_dem_south.tif +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_shade_global.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_shade_north.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/lola_shade_south.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/manifest.json +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_global.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_north.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_south.jpg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/stereo_dem.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/tools.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/workspace.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/SOURCES.txt +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/dependency_links.txt +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/entry_points.txt +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/requires.txt +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/top_level.txt +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/setup.cfg +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_aoi.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_app.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_build.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_config.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_csm.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_download.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_footprints.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_integration.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_kaguya.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_kaguya_mi.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_lbl.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_ode.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_reference.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_selection.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_sfs.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_sfs_products.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_sfs_report.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_shadowcam.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_static.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_stereo.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_tool_env.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_tools.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/tests/test_update.py +0 -0
- {lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/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.dev14
|
|
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
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
.stylepop select { width: auto; }
|
|
215
215
|
.mapbox.hide-fp .fp, .mapbox.hide-fp .fpfill, .mapbox.hide-region .region { display: none; }
|
|
216
216
|
.panehead .toggle { display: inline-flex; align-items: center; gap: 3px; margin-right: 8px; white-space: nowrap; }
|
|
217
|
+
.map .limb { fill: none; stroke: var(--border); stroke-width: 1; pointer-events: none; }
|
|
217
218
|
.map .region { fill: none; fill-rule: evenodd; stroke: var(--region); stroke-width: 1.4; stroke-dasharray: 6 3; pointer-events: none; }
|
|
218
219
|
.map text { fill: var(--plot-label); font-size: 11px; pointer-events: none; }
|
|
219
220
|
.map text.gl { fill: var(--muted); font-size: 10.5px; }
|
|
@@ -1051,12 +1052,13 @@
|
|
|
1051
1052
|
return replicate ? [...g, ...shiftX(g, 360), ...shiftX(g, -360)] : g;
|
|
1052
1053
|
}
|
|
1053
1054
|
|
|
1055
|
+
// hemi 'S' / 'N' polar stereographic, null lon/lat for the cylindrical map, 'LL' raw lon/lat rings
|
|
1054
1056
|
function footprintGeometry(ringsList, hemi) {
|
|
1055
1057
|
const parts = [];
|
|
1056
1058
|
for (let rings of ringsList) {
|
|
1057
1059
|
rings = rings.map(openRing);
|
|
1058
1060
|
if (!rings.length || !rings[0].length) continue;
|
|
1059
|
-
parts.push(polygon(hemi ? rings.map((r) => psRing(r, hemi))
|
|
1061
|
+
parts.push(polygon(hemi === 'LL' ? rings : hemi ? rings.map((r) => psRing(r, hemi))
|
|
1060
1062
|
: shiftTo0360(rings.map((r) => (r.length >= 3 ? llRing(r) : r)))));
|
|
1061
1063
|
}
|
|
1062
1064
|
return parts.length ? parts : null;
|
|
@@ -1094,7 +1096,7 @@
|
|
|
1094
1096
|
span = Math.min(east >= west ? east - west : east - west + 360, 360);
|
|
1095
1097
|
}
|
|
1096
1098
|
const ring = boxRing(s, n, west, span);
|
|
1097
|
-
return [[hemi ? psRing(ring, hemi) : ring]];
|
|
1099
|
+
return [[hemi && hemi !== 'LL' ? psRing(ring, hemi) : ring]];
|
|
1098
1100
|
}
|
|
1099
1101
|
|
|
1100
1102
|
function bboxOf(rings) {
|
|
@@ -1176,7 +1178,7 @@
|
|
|
1176
1178
|
const finite = (...vs) => vs.every((v) => v != null && Number.isFinite(v));
|
|
1177
1179
|
if (mode === 'aoi') {
|
|
1178
1180
|
const aoi = loadAoi($(`${p}-aoi`).value);
|
|
1179
|
-
return { aoi, region: aoiRegion(aoi), shape: (hemi) => aoiGeometry(aoi, hemi, false) };
|
|
1181
|
+
return { aoi, region: aoiRegion(aoi), shape: (hemi) => (hemi === 'LL' ? aoi.polygons : aoiGeometry(aoi, hemi, false)) };
|
|
1180
1182
|
}
|
|
1181
1183
|
if (mode === 'point') {
|
|
1182
1184
|
const [lat, lon, r] = [`${p}-lat`, `${p}-lon`, `${p}-radius`].map(num);
|
|
@@ -1185,7 +1187,7 @@
|
|
|
1185
1187
|
const ring = circleRing(lat, lon, radiusKm);
|
|
1186
1188
|
return {
|
|
1187
1189
|
region: queryRegion({ point: [lat, lon], radiusKm }),
|
|
1188
|
-
shape: (hemi) => [[hemi ? psRing(ring, hemi) : ring.length > 2 ? llRing(ring) : ring]],
|
|
1190
|
+
shape: (hemi) => [[hemi === 'LL' ? ring : hemi ? psRing(ring, hemi) : ring.length > 2 ? llRing(ring) : ring]],
|
|
1189
1191
|
};
|
|
1190
1192
|
}
|
|
1191
1193
|
const [west, south, east, north] = [`${p}-west`, `${p}-south`, `${p}-east`, `${p}-north`].map(num);
|
|
@@ -1195,6 +1197,11 @@
|
|
|
1195
1197
|
region,
|
|
1196
1198
|
shape: (hemi) => {
|
|
1197
1199
|
const [s, n] = region.lat;
|
|
1200
|
+
if (hemi === 'LL') {
|
|
1201
|
+
if (region.span < 360) return [[boxRing(s, n, region.west, region.span)]];
|
|
1202
|
+
const rings = [s, n].filter((la) => Math.abs(la) < 90).map((la) => Array.from({ length: 360 }, (_, i) => [i, la]));
|
|
1203
|
+
return rings.length ? [rings] : [];
|
|
1204
|
+
}
|
|
1198
1205
|
if (hemi && region.span >= 360) {
|
|
1199
1206
|
// A full-longitude box is a disc or annulus around the pole
|
|
1200
1207
|
const circle = (la) => psRing(Array.from({ length: 360 }, (_, i) => [i, la]), hemi);
|
|
@@ -1319,10 +1326,119 @@
|
|
|
1319
1326
|
else renderMap(map, { hemi: mapHemi(sel.region, []), region: sel, items: [] });
|
|
1320
1327
|
}
|
|
1321
1328
|
|
|
1329
|
+
// Map projections: fwd(lon, lat) -> data [x, y] in metres (y down), null beyond the horizon
|
|
1330
|
+
// (the limb for orthographic); inv([x, y]) -> [lon, lat] or null. Auto keeps the lon/lat map
|
|
1331
|
+
// (x = lon * kx, y = -lat) or polar stereographic poleward of +/-60 deg.
|
|
1332
|
+
const MAP_PROJECTIONS = {
|
|
1333
|
+
auto: 'Auto', eqc: 'Cylindrical',
|
|
1334
|
+
stS: 'Stereographic - south pole', stN: 'Stereographic - north pole', stNear: 'Stereographic - near side', stFar: 'Stereographic - far side',
|
|
1335
|
+
orS: 'Orthographic - south pole', orN: 'Orthographic - north pole', orNear: 'Orthographic - near side', orFar: 'Orthographic - far side',
|
|
1336
|
+
};
|
|
1337
|
+
// Short menu text, to keep the pane head narrow
|
|
1338
|
+
const projMenu = (v) => MAP_PROJECTIONS[v].replace('graphic -', '');
|
|
1339
|
+
const PROJ_CENTRES = { S: [-90, 0], N: [90, 0], Near: [0, 0], Far: [0, 180] };
|
|
1340
|
+
|
|
1341
|
+
function azimuthal(ortho, lat0, lon0, cmin = ortho ? 0 : Math.cos(160 * RAD)) {
|
|
1342
|
+
const R = MOON_RADIUS_KM * 1000, s0 = Math.sin(lat0 * RAD), c0 = Math.cos(lat0 * RAD);
|
|
1343
|
+
// Unit-sphere components in data orientation, and the cosine of the distance from the centre
|
|
1344
|
+
const raw = (lon, lat) => {
|
|
1345
|
+
const p = lat * RAD, d = (lon - lon0) * RAD, cp = Math.cos(p);
|
|
1346
|
+
return [cp * Math.sin(d), -(c0 * Math.sin(p) - s0 * cp * Math.cos(d)), s0 * Math.sin(p) + c0 * cp * Math.cos(d)];
|
|
1347
|
+
};
|
|
1348
|
+
const fwd = (lon, lat) => {
|
|
1349
|
+
const [x, y, cc] = raw(lon, lat);
|
|
1350
|
+
if (cc < cmin - 1e-12) return null;
|
|
1351
|
+
const f = ortho ? R : (2 * R) / (1 + cc);
|
|
1352
|
+
return [f * x, f * y];
|
|
1353
|
+
};
|
|
1354
|
+
const rOfC = ortho ? (c) => R * Math.sin(Math.min(c, Math.PI / 2)) : (c) => 2 * R * Math.tan(c / 2);
|
|
1355
|
+
const cOfR = ortho ? (r) => (r <= R ? Math.asin(r / R) : NaN) : (r) => 2 * Math.atan(r / (2 * R));
|
|
1356
|
+
const inv = ([x, y]) => {
|
|
1357
|
+
y = -y;
|
|
1358
|
+
const r = Math.hypot(x, y), c = cOfR(r);
|
|
1359
|
+
if (!(c <= Math.acos(cmin) + 1e-9)) return null;
|
|
1360
|
+
if (r < 1e-9) return [mod(lon0, 360), lat0];
|
|
1361
|
+
const sc = Math.sin(c), cc = Math.cos(c);
|
|
1362
|
+
const lat = Math.asin(Math.max(-1, Math.min(1, cc * s0 + (y * sc * c0) / r))) * DEG;
|
|
1363
|
+
return [mod(lon0 + Math.atan2(x * sc, r * c0 * cc - y * s0 * sc) * DEG, 360), lat];
|
|
1364
|
+
};
|
|
1365
|
+
return { kind: ortho ? 'ortho' : 'stere', cmin, raw, fwd, inv, rOfC, cOfR, rMax: rOfC(Math.acos(cmin)),
|
|
1366
|
+
cosc: (lon, lat) => raw(lon, lat)[2], hemi: lat0 === -90 ? 'S' : lat0 === 90 ? 'N' : null };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// pj.raster: the basemap family drawn natively (affine), else the map is reprojected
|
|
1370
|
+
function mapProjection(choice, hemi) {
|
|
1371
|
+
const az = /^(st|or)/.test(choice) && choice in MAP_PROJECTIONS;
|
|
1372
|
+
if (choice === 'eqc' || (!az && !hemi)) return { id: 'eqc', kind: 'eqc', auto: choice !== 'eqc', raster: 'eqc', hemi: null };
|
|
1373
|
+
if (!az) {
|
|
1374
|
+
return { ...azimuthal(false, hemi === 'S' ? -90 : 90, 0, Math.cos(179 * RAD)), id: `ps${hemi}`, auto: true, raster: hemi, gratCap: 90, rMax: Infinity };
|
|
1375
|
+
}
|
|
1376
|
+
const [la, lo] = PROJ_CENTRES[choice.slice(2)];
|
|
1377
|
+
const pj = { ...azimuthal(choice.startsWith('or'), la, lo), id: choice };
|
|
1378
|
+
return { ...pj, raster: pj.kind === 'stere' ? pj.hemi : null, gratCap: Math.acos(pj.cmin) * DEG };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Points along great circles at most step deg apart (footprint edges, which may cross a pole)
|
|
1382
|
+
function densifyGC(ring, step) {
|
|
1383
|
+
const v = ([lo, la]) => [Math.cos(la * RAD) * Math.cos(lo * RAD), Math.cos(la * RAD) * Math.sin(lo * RAD), Math.sin(la * RAD)];
|
|
1384
|
+
const out = [];
|
|
1385
|
+
ring.forEach((p, i) => {
|
|
1386
|
+
const a = v(p), b = v(ring[(i + 1) % ring.length]);
|
|
1387
|
+
const d = Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]))), s = Math.sin(d);
|
|
1388
|
+
out.push(p);
|
|
1389
|
+
for (let j = 1, n = Math.ceil((d * DEG) / step); j < n && s > 1e-9; j++) {
|
|
1390
|
+
const f0 = Math.sin(d * (1 - j / n)) / s, f1 = Math.sin((d * j) / n) / s;
|
|
1391
|
+
const [x, y, z] = [0, 1, 2].map((c) => f0 * a[c] + f1 * b[c]);
|
|
1392
|
+
out.push([Math.atan2(y, x) * DEG, Math.asin(Math.max(-1, Math.min(1, z))) * DEG]);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
return out;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Lon/lat ring -> data coordinates: edges densified, parts beyond the horizon cut along it
|
|
1399
|
+
function projectRing(pj, ring) {
|
|
1400
|
+
if (ring.length < 3) return ring.map(([lo, la]) => pj.fwd(lo, la)).filter(Boolean);
|
|
1401
|
+
const pts = densifyGC(ring, 0.5), n = pts.length;
|
|
1402
|
+
const cs = pts.map(([lo, la]) => pj.cosc(lo, la)), vis = cs.map((c) => c >= pj.cmin);
|
|
1403
|
+
const start = vis.indexOf(true);
|
|
1404
|
+
if (start < 0) return [];
|
|
1405
|
+
if (vis.every(Boolean)) return pts.map(([lo, la]) => pj.fwd(lo, la));
|
|
1406
|
+
const rb = pj.rMax, on = (a) => [rb * Math.cos(a), rb * Math.sin(a)], out = [];
|
|
1407
|
+
let exitA = 0;
|
|
1408
|
+
for (let k = 0; k < n; k++) {
|
|
1409
|
+
const i = (start + k) % n, j = (i + 1) % n;
|
|
1410
|
+
if (vis[i]) out.push(pj.fwd(...pts[i]));
|
|
1411
|
+
if (vis[i] === vis[j]) continue;
|
|
1412
|
+
const t = (pj.cmin - cs[i]) / (cs[j] - cs[i]);
|
|
1413
|
+
const [rx, ry] = pj.raw(pts[i][0] + wrap180(pts[j][0] - pts[i][0]) * t, pts[i][1] + (pts[j][1] - pts[i][1]) * t);
|
|
1414
|
+
const a = Math.atan2(ry, rx);
|
|
1415
|
+
if (vis[i]) exitA = a;
|
|
1416
|
+
else {
|
|
1417
|
+
const d = mod(a - exitA + Math.PI, 2 * Math.PI) - Math.PI, m = Math.ceil(Math.abs(d) / (3 * RAD));
|
|
1418
|
+
for (let q = 1; q < m; q++) out.push(on(exitA + (d * q) / m));
|
|
1419
|
+
}
|
|
1420
|
+
out.push(on(a));
|
|
1421
|
+
}
|
|
1422
|
+
return out;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const projectPolys = (pj, polys) => polys.map((P) => P.map((r) => projectRing(pj, r))).filter((P) => P[0].length)
|
|
1426
|
+
.map((P) => [P[0], ...P.slice(1).filter((h) => h.length >= 3)]);
|
|
1427
|
+
|
|
1428
|
+
// Inverse polar stereographic (psXY)
|
|
1429
|
+
function psLonLat(x, y, hemi) {
|
|
1430
|
+
const sgn = hemi === 'S' ? 1 : -1;
|
|
1431
|
+
return [mod(Math.atan2(x, sgn * y) * DEG, 360), sgn * (2 * Math.atan(Math.hypot(x, y) / (2 * MOON_RADIUS_KM * 1000)) - Math.PI / 2) * DEG];
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1322
1434
|
function renderMap(container, { hemi, region, items }) {
|
|
1323
1435
|
container._resize?.disconnect();
|
|
1324
1436
|
container._highlight = null;
|
|
1325
1437
|
if (!items.length && !region) { container.replaceChildren(); return; }
|
|
1438
|
+
// hemi: the Auto choice from the region; generic: projected and clipped from lon/lat rings
|
|
1439
|
+
let autoHemi = hemi;
|
|
1440
|
+
const pj = mapProjection(mapPrefs.proj, hemi), generic = !pj.auto && pj.kind !== 'eqc';
|
|
1441
|
+
hemi = pj.kind === 'eqc' ? null : pj.hemi;
|
|
1326
1442
|
const NS = 'http://www.w3.org/2000/svg';
|
|
1327
1443
|
const svg = document.createElementNS(NS, 'svg');
|
|
1328
1444
|
const reset = el('button', { class: 'tool', type: 'button', title: 'Reset view (double click)' }, 'Reset view');
|
|
@@ -1331,26 +1447,30 @@
|
|
|
1331
1447
|
const cinfo = el('span', { class: 'info' });
|
|
1332
1448
|
const pick = el('select', { 'aria-label': 'Basemap', title: 'Basemap under the footprints' });
|
|
1333
1449
|
const cbox = el('input', { type: 'checkbox' });
|
|
1450
|
+
const ppick = el('select', { 'aria-label': 'Projection', title: 'Map projection' },
|
|
1451
|
+
...Object.keys(MAP_PROJECTIONS).map((v) => el('option', { value: v, title: MAP_PROJECTIONS[v] }, projMenu(v))));
|
|
1452
|
+
ppick.value = MAP_PROJECTIONS[mapPrefs.proj] ? mapPrefs.proj : 'auto';
|
|
1334
1453
|
const coord = el('div', { class: 'mapcoord' });
|
|
1335
1454
|
const tools = el('span', { class: 'tools' }, ...Object.entries(MAP_TOOLS).map(([t, [label, title]]) =>
|
|
1336
1455
|
el('button', { class: `tool${t === 'pan' ? ' on' : ''}`, type: 'button', 'data-tool': t, title }, label)));
|
|
1337
1456
|
const bar = el('div', { class: 'panehead' },
|
|
1338
1457
|
el('span', { class: 'title' }, 'Footprints'),
|
|
1339
|
-
el('span', { class: 'info' },
|
|
1458
|
+
el('span', { class: 'info' }, generic ? `${MAP_PROJECTIONS[pj.id]}, R = ${MOON_RADIUS_KM} km`
|
|
1459
|
+
: hemi ? `Polar stereographic (${hemi === 'S' ? 'south' : 'north'}), R = ${MOON_RADIUS_KM} km`
|
|
1340
1460
|
: 'Lon/lat (equirectangular)', ` | ${items.length} footprint(s) | wheel zoom, drag pan`),
|
|
1341
1461
|
cinfo, spin,
|
|
1342
|
-
el('span', { class: 'right' }, tools, pick, el('label', { class: 'toggle', title: 'Elevation contours (LOLA)' }, cbox, ' Contours'),
|
|
1462
|
+
el('span', { class: 'right' }, tools, ppick, pick, el('label', { class: 'toggle', title: 'Elevation contours (LOLA)' }, cbox, ' Contours'),
|
|
1343
1463
|
layerToggle(container, 'fp', 'Footprints'), footprintStyle(container), layerToggle(container, 'region', 'Region'), reset));
|
|
1344
1464
|
container.replaceChildren(el('div', { class: 'map' }, bar, el('div', { class: 'plotarea' }, canvas, svg, coord)));
|
|
1345
1465
|
const clipId = `clip-${container.id}`;
|
|
1346
1466
|
const M = MARGIN;
|
|
1347
1467
|
|
|
1348
1468
|
// Lon/lat: center everything on the region (or the footprints) to avoid the seam
|
|
1349
|
-
const regionGeom = region ? region.shape(hemi) : [];
|
|
1350
|
-
let geoms = items.map((it) => it.geom);
|
|
1469
|
+
const regionGeom = region && !generic ? region.shape(hemi) : [];
|
|
1470
|
+
let geoms = generic ? [] : items.map((it) => (it.hemi === hemi ? it.geom : it.geomOf(hemi)));
|
|
1351
1471
|
let kx = 1;
|
|
1352
1472
|
let recenter = (g) => g;
|
|
1353
|
-
if (
|
|
1473
|
+
if (pj.kind === 'eqc') {
|
|
1354
1474
|
const mids = [...regionGeom, ...geoms.flat()].map((P) => { const b = bboxOf([P[0]]); return (b[0] + b[1]) / 2; });
|
|
1355
1475
|
const c = region ? (region.region.span < 360 ? region.region.west + region.region.span / 2 : 180)
|
|
1356
1476
|
: Math.atan2(sumOf(mids.map((m) => Math.sin(m * RAD))), sumOf(mids.map((m) => Math.cos(m * RAD)))) * DEG;
|
|
@@ -1361,16 +1481,19 @@
|
|
|
1361
1481
|
regionGeom.splice(0, regionGeom.length, ...recenter(regionGeom));
|
|
1362
1482
|
}
|
|
1363
1483
|
// Data coordinates: x right, y down
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
const
|
|
1484
|
+
if (pj.kind === 'eqc') pj.inv = ([x, y]) => [x / kx, -y];
|
|
1485
|
+
const toXY = (g) => (generic ? projectPolys(pj, g) : g.map((P) => P.map((r) => r.map(([x, y]) => (hemi ? [x, -y] : [x * kx, -y])))));
|
|
1486
|
+
const regionXY = (next) => toXY(generic ? next.shape('LL') : hemi ? next.shape(hemi) : recenter(next.shape(hemi)));
|
|
1487
|
+
let reg = generic ? (region ? regionXY(region) : []) : toXY(regionGeom);
|
|
1488
|
+
const fps = generic ? items.map((it) => toXY(it.geomOf('LL'))) : geoms.map(toXY);
|
|
1367
1489
|
const all = [...reg, ...fps.flat()].flat();
|
|
1368
|
-
|
|
1369
|
-
|
|
1490
|
+
const R = MOON_RADIUS_KM * 1000;
|
|
1491
|
+
let [x0, x1, y0, y1] = all.length ? bboxOf(all) : generic ? [-R, R, -R, R] : [-1, 1, -1, 1];
|
|
1492
|
+
const minSpan = pj.kind === 'eqc' ? 0.05 : 2000;
|
|
1370
1493
|
if (x1 - x0 < minSpan) [x0, x1] = [(x0 + x1 - minSpan) / 2, (x0 + x1 + minSpan) / 2];
|
|
1371
1494
|
if (y1 - y0 < minSpan) [y0, y1] = [(y0 + y1 - minSpan) / 2, (y0 + y1 + minSpan) / 2];
|
|
1372
1495
|
const home = { cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, w: x1 - x0, h: y1 - y0 };
|
|
1373
|
-
const under = mapUnderlay({ container,
|
|
1496
|
+
const under = mapUnderlay({ container, pj, kx, canvas, spin, cinfo, pick, cbox, home, redraw: () => redraw() });
|
|
1374
1497
|
// SW x SH: whole svg; W x H: plot area inside the axis margins
|
|
1375
1498
|
let view = null, SW = 0, SH = 0, W = 0, H = 0, sel = -1, touched = false;
|
|
1376
1499
|
const fit = () => {
|
|
@@ -1384,10 +1507,11 @@
|
|
|
1384
1507
|
: 'M' + r.map(([x, y]) => `${sx(x).toFixed(1)} ${sy(y).toFixed(1)}`).join('L') + 'Z')).join('')).join('');
|
|
1385
1508
|
const esc = (s) => String(s).replace(/[&<>"]/g, (ch) => `&#${ch.charCodeAt(0)};`);
|
|
1386
1509
|
const sgn = hemi === 'S' ? 1 : -1;
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1510
|
+
// Polar aspects: radius of a parallel, and its inverse
|
|
1511
|
+
const rho = (la) => pj.rOfC(Math.PI / 2 + sgn * la * RAD);
|
|
1512
|
+
const latOf = (r) => sgn * (pj.cOfR(r) - Math.PI / 2) * DEG;
|
|
1513
|
+
// Data coordinates (x right, y down) to [lon, lat], null off the globe
|
|
1514
|
+
const toLonLat = (p) => pj.inv(p);
|
|
1391
1515
|
coord.style.left = `${M.l + 6}px`;
|
|
1392
1516
|
coord.style.bottom = `${M.b + 6}px`;
|
|
1393
1517
|
let tool = 'pan', sketch = null;
|
|
@@ -1408,7 +1532,16 @@
|
|
|
1408
1532
|
labels.push(`<text x="-6" y="${(y + 3.5).toFixed(1)}" text-anchor="end">${text}</text>`);
|
|
1409
1533
|
};
|
|
1410
1534
|
let xTitle, yTitle;
|
|
1411
|
-
|
|
1535
|
+
const inView = (x, y) => x >= 4 && x <= W - 30 && y >= 12 && y <= H - 4;
|
|
1536
|
+
const gl = (x, y, text) => { if (inView(x, y)) inner.push(`<text class="gl" x="${(x + 3).toFixed(1)}" y="${(y - 3).toFixed(1)}">${text}</text>`); };
|
|
1537
|
+
// Frame ticks in projected km (y up)
|
|
1538
|
+
const kmTicks = () => {
|
|
1539
|
+
const xs = step125((vx1 - vx0) / 1000, Math.max(2, W / 90)), ys = step125((vy1 - vy0) / 1000, Math.max(2, H / 50));
|
|
1540
|
+
for (let v = Math.ceil(vx0 / 1000 / xs) * xs; v <= vx1 / 1000; v += xs) xTick(sx(v * 1000), fmtDeg(v, xs));
|
|
1541
|
+
for (let v = Math.ceil(-vy1 / 1000 / ys) * ys; v <= -vy0 / 1000; v += ys) yTick(sy(-v * 1000), fmtDeg(v, ys));
|
|
1542
|
+
return ['x (km)', 'y (km)'];
|
|
1543
|
+
};
|
|
1544
|
+
if (pj.kind === 'eqc') {
|
|
1412
1545
|
const lonStep = niceStep((vx1 - vx0) / kx), latStep = niceStep(vy1 - vy0);
|
|
1413
1546
|
for (let lo = Math.ceil(vx0 / kx / lonStep) * lonStep; lo <= vx1 / kx; lo += lonStep) {
|
|
1414
1547
|
const x = sx(lo * kx);
|
|
@@ -1422,14 +1555,14 @@
|
|
|
1422
1555
|
yTick(y, fmtDeg(la, latStep));
|
|
1423
1556
|
}
|
|
1424
1557
|
[xTitle, yTitle] = ['Longitude (deg E)', 'Latitude (deg)'];
|
|
1425
|
-
} else {
|
|
1426
|
-
const inView = (x, y) => x >= 4 && x <= W - 30 && y >= 12 && y <= H - 4;
|
|
1558
|
+
} else if (hemi) {
|
|
1427
1559
|
const cx = Math.min(Math.max(0, vx0), vx1), cy = Math.min(Math.max(0, vy0), vy1);
|
|
1428
1560
|
const rMin = Math.hypot(cx, cy);
|
|
1429
|
-
const rMax = Math.max(...[[vx0, vy0], [vx0, vy1], [vx1, vy0], [vx1, vy1]].map(([x, y]) => Math.hypot(x, y)));
|
|
1561
|
+
const rMax = Math.min(pj.rMax, Math.max(...[[vx0, vy0], [vx0, vy1], [vx1, vy0], [vx1, vy1]].map(([x, y]) => Math.hypot(x, y))));
|
|
1430
1562
|
const [laIn, laOut] = [latOf(rMin), latOf(rMax)];
|
|
1431
1563
|
const latStep = niceStep(Math.abs(laOut - laIn));
|
|
1432
|
-
|
|
1564
|
+
// Auto: the polar hemisphere only; otherwise up to the horizon
|
|
1565
|
+
const poleward = (la) => 90 + sgn * la <= pj.gratCap + 1e-9;
|
|
1433
1566
|
const angSpan = rMin > 0 ? Math.min(360, (2 * Math.hypot(vx1 - vx0, vy1 - vy0) / rMin) * DEG) : 360;
|
|
1434
1567
|
const lonStep = Math.min(30, Math.max(0.5, niceStep(angSpan)));
|
|
1435
1568
|
// Latitude labels between two meridians, towards the view centre
|
|
@@ -1441,22 +1574,56 @@
|
|
|
1441
1574
|
for (const la of lats) {
|
|
1442
1575
|
const r = rho(la) * view.k;
|
|
1443
1576
|
lines.push(`M${(sx(0) - r).toFixed(1)} ${sy(0).toFixed(1)}a${r.toFixed(1)} ${r.toFixed(1)} 0 1 0 ${(2 * r).toFixed(1)} 0a${r.toFixed(1)} ${r.toFixed(1)} 0 1 0 ${(-2 * r).toFixed(1)} 0`);
|
|
1444
|
-
|
|
1445
|
-
if (inView(lx, ly)) inner.push(`<text class="gl" x="${(lx + 3).toFixed(1)}" y="${(ly - 3).toFixed(1)}">${fmtDeg(la, latStep)}</text>`);
|
|
1577
|
+
gl(sx(Math.cos(dirC) * rho(la)), sy(Math.sin(dirC) * rho(la)), fmtDeg(la, latStep));
|
|
1446
1578
|
}
|
|
1447
1579
|
const rLab = Math.max(Math.hypot(view.cx, view.cy), 0.3 * Math.min(W, H) / view.k);
|
|
1448
|
-
for (let lo = 0; lo < 360; lo += lonStep) {
|
|
1580
|
+
for (let lo = 0; lo < 360 && rMin < rMax; lo += lonStep) {
|
|
1449
1581
|
// Data coords (y down): S x = rho sin, y = -rho cos; N y = rho cos
|
|
1450
1582
|
const ux = Math.sin(lo * RAD), uy = -sgn * Math.cos(lo * RAD);
|
|
1451
1583
|
lines.push(`M${sx(ux * rMin).toFixed(1)} ${sy(uy * rMin).toFixed(1)}L${sx(ux * rMax).toFixed(1)} ${sy(uy * rMax).toFixed(1)}`);
|
|
1452
|
-
|
|
1453
|
-
if (inView(lx, ly)) inner.push(`<text class="gl" x="${(lx + 3).toFixed(1)}" y="${(ly - 3).toFixed(1)}">${fmtDeg(lo, lonStep)}E</text>`);
|
|
1584
|
+
if (rLab <= rMax) gl(sx(ux * rLab), sy(uy * rLab), `${fmtDeg(lo, lonStep)}E`);
|
|
1454
1585
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1586
|
+
[xTitle, yTitle] = kmTicks();
|
|
1587
|
+
} else {
|
|
1588
|
+
// Equatorial aspects: meridians and parallels as projected polylines, labelled at the
|
|
1589
|
+
// bottom (meridians) and left (parallels) of the view
|
|
1590
|
+
const lls = [];
|
|
1591
|
+
for (let j = 0; j <= 16; j++) {
|
|
1592
|
+
for (let i = 0; i <= 16; i++) {
|
|
1593
|
+
const ll = pj.inv([vx0 + ((vx1 - vx0) * i) / 16, vy0 + ((vy1 - vy0) * j) / 16]);
|
|
1594
|
+
if (ll) lls.push(ll);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const poleIn = (la) => { const p = pj.fwd(0, la); return p && p[0] >= vx0 && p[0] <= vx1 && p[1] >= vy0 && p[1] <= vy1; };
|
|
1598
|
+
if (lls.length) {
|
|
1599
|
+
const laMin = poleIn(-90) ? -90 : minOf(lls.map((p) => p[1])), laMax = poleIn(90) ? 90 : maxOf(lls.map((p) => p[1]));
|
|
1600
|
+
let [lw, lspan] = poleIn(-90) || poleIn(90) ? [0, 360] : coveringArc(lls.map(([lo]) => [lo, 0]));
|
|
1601
|
+
const latStep = Math.min(30, niceStep(Math.max(laMax - laMin, 1e-4))), lonStep = Math.min(30, niceStep(Math.max(lspan, 1e-4)));
|
|
1602
|
+
if (lspan < 360) [lw, lspan] = [lw - lonStep, Math.min(360, lspan + 2 * lonStep)];
|
|
1603
|
+
const [la0, la1] = [Math.max(-90, laMin - latStep), Math.min(90, laMax + latStep)];
|
|
1604
|
+
const line = (pts, text, pickLabel) => {
|
|
1605
|
+
let d = '', pen = false, best = null;
|
|
1606
|
+
for (const [lo, la] of pts) {
|
|
1607
|
+
const p = pj.fwd(lo, la);
|
|
1608
|
+
if (!p) { pen = false; continue; }
|
|
1609
|
+
const x = sx(p[0]), y = sy(p[1]);
|
|
1610
|
+
d += `${pen ? 'L' : 'M'}${x.toFixed(1)} ${y.toFixed(1)}`;
|
|
1611
|
+
pen = true;
|
|
1612
|
+
if (inView(x, y) && Math.abs(la) < 89 && (!best || pickLabel([x, y], best))) best = [x, y];
|
|
1613
|
+
}
|
|
1614
|
+
lines.push(d);
|
|
1615
|
+
if (best) gl(best[0], best[1], text);
|
|
1616
|
+
};
|
|
1617
|
+
const N = 120;
|
|
1618
|
+
for (let lo = Math.ceil(lw / lonStep) * lonStep; lo <= lw + lspan && lo < lw + 360; lo += lonStep) {
|
|
1619
|
+
line(Array.from({ length: N + 1 }, (_, i) => [lo, la0 + ((la1 - la0) * i) / N]), `${fmtDeg(mod(lo, 360), lonStep)}E`, (p, b) => p[1] > b[1]);
|
|
1620
|
+
}
|
|
1621
|
+
for (let la = Math.ceil(laMin / latStep) * latStep; la <= laMax; la += latStep) {
|
|
1622
|
+
if (Math.abs(la) >= 90) continue;
|
|
1623
|
+
line(Array.from({ length: 2 * N + 1 }, (_, i) => [lw + (lspan * i) / (2 * N), la]), fmtDeg(la, latStep), (p, b) => p[0] < b[0]);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
[xTitle, yTitle] = kmTicks();
|
|
1460
1627
|
}
|
|
1461
1628
|
labels.push(`<text class="axis" x="${(W / 2).toFixed(1)}" y="${H + 29}" text-anchor="middle">${xTitle}</text>`,
|
|
1462
1629
|
`<text class="axis" transform="translate(${-M.l + 12} ${(H / 2).toFixed(1)}) rotate(-90)" text-anchor="middle">${yTitle}</text>`);
|
|
@@ -1472,7 +1639,9 @@
|
|
|
1472
1639
|
const ds = fps.map((g) => pathD(g));
|
|
1473
1640
|
svg.innerHTML = `<defs><clipPath id="${clipId}"><rect width="${W}" height="${H}"/></clipPath></defs>`
|
|
1474
1641
|
+ `<g transform="translate(${M.l} ${M.t})"><rect class="plot" width="${W}" height="${H}"/>`
|
|
1475
|
-
+ `<g clip-path="url(#${clipId})">${a.grid}
|
|
1642
|
+
+ `<g clip-path="url(#${clipId})">${a.grid}`
|
|
1643
|
+
+ (pj.kind === 'ortho' ? `<circle class="limb" cx="${sx(0).toFixed(1)}" cy="${sy(0).toFixed(1)}" r="${(pj.rMax * view.k).toFixed(1)}"/>` : '')
|
|
1644
|
+
+ '<g class="cslot"/>'
|
|
1476
1645
|
+ `<g class="fpfill">${ds.map((d) => `<path d="${d}"/>`).join('')}</g>`
|
|
1477
1646
|
+ order.map((i) => `<path class="fp${i === sel ? ' sel' : ''}" data-i="${i}" d="${ds[i]}"><title>${esc(items[i].title)}</title></path>`).join('')
|
|
1478
1647
|
+ (reg.length ? `<path class="region" d="${pathD(reg)}"/>` : '')
|
|
@@ -1506,6 +1675,11 @@
|
|
|
1506
1675
|
const [[ax, ay], [bx, by]] = [sketch.a, sketch.b];
|
|
1507
1676
|
return `<path class="sketch" d="M${P([ax, ay])}L${P([bx, ay])}L${P([bx, by])}L${P([ax, by])}Z"/>`;
|
|
1508
1677
|
}
|
|
1678
|
+
if (sketch.tool === 'circle' && generic) {
|
|
1679
|
+
const [a, b] = [toLonLat(sketch.a), toLonLat(sketch.b)];
|
|
1680
|
+
const ring = a && b ? projectRing(pj, circleRing(a[1], a[0], greatCircleKm(a[1], a[0], b[1], b[0]))) : [];
|
|
1681
|
+
return ring.length ? `<path class="sketch" d="M${ring.map(P).join('L')}Z"/>` : '';
|
|
1682
|
+
}
|
|
1509
1683
|
if (sketch.tool === 'circle') {
|
|
1510
1684
|
const r = screenDist(sketch.a, sketch.b).toFixed(1);
|
|
1511
1685
|
return `<circle class="sketch" cx="${sx(sketch.a[0]).toFixed(1)}" cy="${sy(sketch.a[1]).toFixed(1)}" r="${r}"/>`;
|
|
@@ -1522,7 +1696,7 @@
|
|
|
1522
1696
|
};
|
|
1523
1697
|
for (const b of tools.children) b.addEventListener('click', () => setTool(b.dataset.tool));
|
|
1524
1698
|
// Densified ring in data coordinates -> lon/lat, so edges follow the map's straight lines
|
|
1525
|
-
const ringLonLat = (pts, n =
|
|
1699
|
+
const ringLonLat = (pts, n = pj.kind === 'eqc' ? 1 : 16) => pts.flatMap((p, i) => {
|
|
1526
1700
|
const q = pts[(i + 1) % pts.length];
|
|
1527
1701
|
return Array.from({ length: n }, (_, j) => toLonLat([p[0] + ((q[0] - p[0]) * j) / n, p[1] + ((q[1] - p[1]) * j) / n]));
|
|
1528
1702
|
});
|
|
@@ -1531,16 +1705,19 @@
|
|
|
1531
1705
|
setTool('pan');
|
|
1532
1706
|
if (!sk) return;
|
|
1533
1707
|
if (sk.tool === 'circle') {
|
|
1534
|
-
const [
|
|
1708
|
+
const [a, b] = [toLonLat(sk.a), toLonLat(sk.b)];
|
|
1709
|
+
if (!a || !b) return;
|
|
1710
|
+
const [[lon, lat], [lon2, lat2]] = [a, b];
|
|
1535
1711
|
setFormRegion('point', { lat, lon, radius: greatCircleKm(lat, lon, lat2, lon2) });
|
|
1536
|
-
} else if (sk.tool === 'box' &&
|
|
1712
|
+
} else if (sk.tool === 'box' && pj.kind === 'eqc') {
|
|
1537
1713
|
const [[w, n], [e, s]] = [toLonLat([Math.min(sk.a[0], sk.b[0]), Math.min(sk.a[1], sk.b[1])]),
|
|
1538
1714
|
toLonLat([Math.max(sk.a[0], sk.b[0]), Math.max(sk.a[1], sk.b[1])])];
|
|
1539
1715
|
const full = e - w >= 360;
|
|
1540
1716
|
setFormRegion('box', { west: full ? 0 : mod(w, 360), east: full ? 360 : mod(e, 360), south: s, north: n });
|
|
1541
1717
|
} else {
|
|
1542
1718
|
const pts = sk.tool === 'box' ? [sk.a, [sk.b[0], sk.a[1]], sk.b, [sk.a[0], sk.b[1]]] : sk.pts;
|
|
1543
|
-
const ring = ringLonLat(pts).map(([lon, lat]) => [mod(lon, 360), lat]);
|
|
1719
|
+
const ring = ringLonLat(pts).filter(Boolean).map(([lon, lat]) => [mod(lon, 360), lat]);
|
|
1720
|
+
if (ring.length < 3) return;
|
|
1544
1721
|
setFormRegion('aoi', { aoi: `POLYGON((${[...ring, ring[0]].map(([lon, lat]) => `${lon.toFixed(5)} ${lat.toFixed(5)}`).join(', ')}))` });
|
|
1545
1722
|
}
|
|
1546
1723
|
}
|
|
@@ -1550,9 +1727,10 @@
|
|
|
1550
1727
|
if (mx < 0 || my < 0 || mx > W || my > H) { $('cursor').textContent = coord.textContent = ''; return; }
|
|
1551
1728
|
const x = view.cx + (mx - W / 2) / view.k, y = -(view.cy + (my - H / 2) / view.k);
|
|
1552
1729
|
let lat, lon, xy = '';
|
|
1553
|
-
if (
|
|
1554
|
-
|
|
1555
|
-
|
|
1730
|
+
if (pj.kind !== 'eqc') {
|
|
1731
|
+
const ll = pj.inv([x, -y]);
|
|
1732
|
+
if (!ll) { $('cursor').textContent = coord.textContent = ''; return; }
|
|
1733
|
+
[lon, lat] = ll;
|
|
1556
1734
|
xy = ` x ${(x / 1000).toFixed(2)} km y ${(y / 1000).toFixed(2)} km`;
|
|
1557
1735
|
} else {
|
|
1558
1736
|
[lat, lon] = [y, mod(x / kx, 360)];
|
|
@@ -1611,12 +1789,20 @@
|
|
|
1611
1789
|
});
|
|
1612
1790
|
reset.addEventListener('click', home0);
|
|
1613
1791
|
container._highlight = (row) => { sel = items.findIndex((it) => it.row === row); draw(); };
|
|
1792
|
+
ppick.addEventListener('change', () => {
|
|
1793
|
+
mapPrefs.proj = ppick.value;
|
|
1794
|
+
saveMapPrefs();
|
|
1795
|
+
const row = items[sel]?.row;
|
|
1796
|
+
renderMap(container, { hemi: autoHemi, region, items });
|
|
1797
|
+
if (row) container._highlight?.(row);
|
|
1798
|
+
});
|
|
1614
1799
|
// A new search region from the form or the map, keeping the view and the results
|
|
1615
1800
|
container._setRegion = (next) => {
|
|
1616
1801
|
const h = mapHemi(next.region, []);
|
|
1617
|
-
if (h !==
|
|
1802
|
+
if (mapProjection(mapPrefs.proj, h).id !== pj.id) { renderMap(container, { hemi: h, region: next, items }); return; }
|
|
1618
1803
|
region = next;
|
|
1619
|
-
|
|
1804
|
+
autoHemi = h;
|
|
1805
|
+
reg = regionXY(next);
|
|
1620
1806
|
redraw();
|
|
1621
1807
|
};
|
|
1622
1808
|
let lastW = 0, lastH = 0;
|
|
@@ -1647,7 +1833,7 @@
|
|
|
1647
1833
|
const items = [];
|
|
1648
1834
|
for (const row of rows) {
|
|
1649
1835
|
const geom = rowFootprint(row, wktCol, hemi);
|
|
1650
|
-
if (geom) items.push({ geom, row, title: title(row), onclick: () => onclick(row) });
|
|
1836
|
+
if (geom) items.push({ geom, hemi, geomOf: (h) => rowFootprint(row, wktCol, h), row, title: title(row), onclick: () => onclick(row) });
|
|
1651
1837
|
}
|
|
1652
1838
|
return items;
|
|
1653
1839
|
}
|
|
@@ -2838,9 +3024,10 @@
|
|
|
2838
3024
|
N: ['lola_dem_north'],
|
|
2839
3025
|
};
|
|
2840
3026
|
const BM_STRETCH = { min: 0, max: 255, gamma: 1 };
|
|
3027
|
+
const POLAR_OVERVIEW_M = 609600; // half width of the bundled polar overviews
|
|
2841
3028
|
// Map preferences, remembered across searches and (when storage works) sessions
|
|
2842
3029
|
const MAP_PREFS_KEY = 'lunar-toolkit-map';
|
|
2843
|
-
const mapPrefs = { basemap: 'wac', contours: false, fpLine: 'dotted', fpFill: 'light', fpColor: 'blue' };
|
|
3030
|
+
const mapPrefs = { proj: 'auto', basemap: 'wac', contours: false, fpLine: 'dotted', fpFill: 'light', fpColor: 'blue' };
|
|
2844
3031
|
try { Object.assign(mapPrefs, JSON.parse(localStorage.getItem(MAP_PREFS_KEY) ?? '{}')); } catch { /* storage blocked */ }
|
|
2845
3032
|
const saveMapPrefs = () => { try { localStorage.setItem(MAP_PREFS_KEY, JSON.stringify(mapPrefs)); } catch { /* storage blocked */ } };
|
|
2846
3033
|
const bmOpened = new Map(); // ref -> { promise, src, pending, failed }
|
|
@@ -2886,12 +3073,15 @@
|
|
|
2886
3073
|
tileAdd(key, { canvas, w, h, bytes: 0 }, w * h * 4);
|
|
2887
3074
|
}
|
|
2888
3075
|
|
|
2889
|
-
// Marching squares over grid parts { z, w, h, x(i), y(j) }: SVG paths (minor, major every 5th)
|
|
2890
|
-
|
|
3076
|
+
// Marching squares over grid parts { z, w, h, x(i), y(j) }: SVG paths (minor, major every 5th);
|
|
3077
|
+
// P(x, y) gives the screen point, or null to drop the segment
|
|
3078
|
+
function contourPaths(parts, noData, step, P) {
|
|
2891
3079
|
const out = [[], []];
|
|
2892
3080
|
for (const { z, w, h, x, y } of parts) {
|
|
2893
|
-
const seg = (k, [i0, j0], [i1, j1]) =>
|
|
2894
|
-
|
|
3081
|
+
const seg = (k, [i0, j0], [i1, j1]) => {
|
|
3082
|
+
const a = P(x(i0), y(j0)), b = a && P(x(i1), y(j1));
|
|
3083
|
+
if (b) out[k % 5 === 0 ? 1 : 0].push(`M${a[0].toFixed(1)} ${a[1].toFixed(1)}L${b[0].toFixed(1)} ${b[1].toFixed(1)}`);
|
|
3084
|
+
};
|
|
2895
3085
|
for (let j = 0; j < h - 1; j++) {
|
|
2896
3086
|
for (let i = 0; i < w - 1; i++) {
|
|
2897
3087
|
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];
|
|
@@ -2912,10 +3102,15 @@
|
|
|
2912
3102
|
return out.map((a) => a.join(''));
|
|
2913
3103
|
}
|
|
2914
3104
|
|
|
2915
|
-
//
|
|
2916
|
-
|
|
3105
|
+
// Re-rendering the map aborts the old map's reads; geotiff.js leaves some of those rejections unobserved
|
|
3106
|
+
window.addEventListener('unhandledrejection', (e) => { if (e.reason?.name === 'AbortError') e.preventDefault(); });
|
|
3107
|
+
|
|
3108
|
+
// The basemap canvas and the contour layer of one footprint map. When the map projection is a
|
|
3109
|
+
// raster's own (pj.raster) tiles are placed affinely, else reprojected pixel by pixel.
|
|
3110
|
+
function mapUnderlay({ container, pj, kx, canvas, spin, cinfo, pick, cbox, home, redraw }) {
|
|
2917
3111
|
bmNotify?.abort?.();
|
|
2918
|
-
const
|
|
3112
|
+
const native = pj.raster, hemi = native === 'eqc' ? null : native;
|
|
3113
|
+
const proj = hemi ?? 'eqc', sc = native === 'eqc' ? kx : 1, NS = 'http://www.w3.org/2000/svg';
|
|
2919
3114
|
const ctl = new AbortController();
|
|
2920
3115
|
const st = { wanted: [], inflight: new Set(), failed: new Map(), timer: 0, ctimer: 0, ckey: '', gen: 0, cbusy: false, contour: null };
|
|
2921
3116
|
const notify = () => { if (container.isConnected) redraw(); };
|
|
@@ -2928,12 +3123,16 @@
|
|
|
2928
3123
|
contours.append(p);
|
|
2929
3124
|
return p;
|
|
2930
3125
|
});
|
|
2931
|
-
//
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3126
|
+
// A chosen polar stereographic view reaching past the polar overviews is reprojected, so the
|
|
3127
|
+
// global rasters fill it (Auto keeps the polar rasters only)
|
|
3128
|
+
const affine = (view, W, H) => native && (pj.auto || !hemi
|
|
3129
|
+
|| Math.max(Math.abs(view.cx) + W / 2 / view.k, Math.abs(view.cy) + H / 2 / view.k) <= POLAR_OVERVIEW_M);
|
|
3130
|
+
// Raster extent in map (or, reprojecting, the raster's own) data coordinates (x right, y down)
|
|
3131
|
+
const geom = (s, k = sc) => { const [x0, y0, x1, y1] = s.box; return { X0: x0 * k, X1: x1 * k, Y0: -y1, Y1: -y0 }; };
|
|
3132
|
+
// x offsets of the wrapped lon/lat copies of g overlapping [vx0, vx1] (period p, 0 for polar)
|
|
3133
|
+
const copies = (g, vx0, vx1, p = hemi ? 0 : 360 * sc) => {
|
|
3134
|
+
if (!p) return [0];
|
|
3135
|
+
const n0 = Math.ceil((vx0 - g.X1) / p), n1 = Math.floor((vx1 - g.X0) / p);
|
|
2937
3136
|
return n1 - n0 > 8 ? [] : Array.from({ length: n1 - n0 + 1 }, (_, i) => (n0 + i) * p);
|
|
2938
3137
|
};
|
|
2939
3138
|
const choices = Object.keys(BASEMAPS[proj]).filter((c) => !['shade5', 'nac'].includes(c)
|
|
@@ -2979,6 +3178,9 @@
|
|
|
2979
3178
|
const r = dpr();
|
|
2980
3179
|
Object.assign(canvas.style, { left: `${MARGIN.l}px`, top: `${MARGIN.t}px`, width: `${W}px`, height: `${H}px` });
|
|
2981
3180
|
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)];
|
|
3181
|
+
if (!affine(view, W, H)) { queue(reproject(view, W, H, r), view, W, H); return; }
|
|
3182
|
+
clearTimeout(rp.timer);
|
|
3183
|
+
rp.vkey = '';
|
|
2982
3184
|
const ctx = canvas.getContext('2d');
|
|
2983
3185
|
ctx.setTransform(r, 0, 0, r, 0, 0);
|
|
2984
3186
|
ctx.clearRect(0, 0, W, H);
|
|
@@ -3032,6 +3234,10 @@
|
|
|
3032
3234
|
}
|
|
3033
3235
|
}
|
|
3034
3236
|
}
|
|
3237
|
+
queue(want, view, W, H);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
function queue(want, view, W, H) {
|
|
3035
3241
|
st.wanted = want.sort((a, b) => a.d - b.d);
|
|
3036
3242
|
clearTimeout(st.timer);
|
|
3037
3243
|
st.timer = setTimeout(pump, 80);
|
|
@@ -3039,6 +3245,116 @@
|
|
|
3039
3245
|
status();
|
|
3040
3246
|
}
|
|
3041
3247
|
|
|
3248
|
+
// Reprojection: inverse-project a grid of screen pixels (every 3rd device pixel while the view
|
|
3249
|
+
// moves, every one once it rests) and sample the global raster, or the polar one poleward of
|
|
3250
|
+
// 72 deg, from the tiles of the level that suits the zoom, else the bundled overview
|
|
3251
|
+
const rp = { vkey: '', gkey: '', lon: null, lat: null, want: [], timer: 0, out: document.createElement('canvas') };
|
|
3252
|
+
const overviewPx = (b) => {
|
|
3253
|
+
if (!b.px) {
|
|
3254
|
+
const c = Object.assign(document.createElement('canvas'), { width: b.w0, height: b.h0 });
|
|
3255
|
+
const cx = c.getContext('2d', { willReadFrequently: true });
|
|
3256
|
+
cx.drawImage(b.pic, 0, 0);
|
|
3257
|
+
const d = cx.getImageData(0, 0, b.w0, b.h0).data;
|
|
3258
|
+
b.px = new Uint8Array(b.w0 * b.h0);
|
|
3259
|
+
for (let i = 0; i < b.px.length; i++) b.px[i] = d[4 * i];
|
|
3260
|
+
c.width = 0;
|
|
3261
|
+
}
|
|
3262
|
+
return b.px;
|
|
3263
|
+
};
|
|
3264
|
+
const tilePx = (t) => {
|
|
3265
|
+
if (!t.px) {
|
|
3266
|
+
t.px = new Uint32Array(t.canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, t.w, t.h).data.buffer);
|
|
3267
|
+
tileAdd(null, t, t.w * t.h * 4);
|
|
3268
|
+
}
|
|
3269
|
+
return t.px;
|
|
3270
|
+
};
|
|
3271
|
+
|
|
3272
|
+
function reproject(view, W, H, r) {
|
|
3273
|
+
const vkey = `${view.cx}|${view.cy}|${view.k}|${W}|${H}|${choice()}`;
|
|
3274
|
+
clearTimeout(rp.timer);
|
|
3275
|
+
if (vkey !== rp.vkey) {
|
|
3276
|
+
rp.vkey = vkey;
|
|
3277
|
+
render(view, W, H, r, 3);
|
|
3278
|
+
}
|
|
3279
|
+
rp.timer = setTimeout(() => { if (container.isConnected) queue(render(view, W, H, r, 1), view, W, H); }, 200);
|
|
3280
|
+
return rp.want;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
function render(view, W, H, r, f) {
|
|
3284
|
+
const ctx = canvas.getContext('2d');
|
|
3285
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3286
|
+
const ch = choice();
|
|
3287
|
+
if (ch === 'none') { ctx.clearRect(0, 0, canvas.width, canvas.height); return (rp.want = []); }
|
|
3288
|
+
const gw = Math.ceil((W * r) / f), gh = Math.ceil((H * r) / f), n = gw * gh;
|
|
3289
|
+
const gkey = `${view.cx}|${view.cy}|${view.k}|${gw}|${gh}`;
|
|
3290
|
+
if (gkey !== rp.gkey) {
|
|
3291
|
+
rp.gkey = gkey;
|
|
3292
|
+
[rp.lon, rp.lat] = [new Float64Array(n), new Float64Array(n)];
|
|
3293
|
+
for (let j = 0, k = 0; j < gh; j++) {
|
|
3294
|
+
const y = view.cy + (((j + 0.5) * f) / r - H / 2) / view.k;
|
|
3295
|
+
for (let i = 0; i < gw; i++, k++) {
|
|
3296
|
+
const ll = pj.inv([view.cx + (((i + 0.5) * f) / r - W / 2) / view.k, y]);
|
|
3297
|
+
[rp.lon[k], rp.lat[k]] = ll ?? [NaN, NaN];
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
const mppScreen = 1 / (view.k * r), Rm = MOON_RADIUS_KM * 1000;
|
|
3302
|
+
// Per family: overview pixels, and the streamed level when the overview is magnified
|
|
3303
|
+
const sampler = (fam) => {
|
|
3304
|
+
const stack = BASEMAPS[fam][ch], [b, c] = stack ? stack.map((ref) => bmLoad(ref).src) : [];
|
|
3305
|
+
if (!b) return null;
|
|
3306
|
+
const m = (box, w) => ((box[2] - box[0]) / w) * (fam === 'eqc' ? RAD * Rm : 1);
|
|
3307
|
+
const S = { fam, b, px: overviewPx(b), lv: null };
|
|
3308
|
+
if (c) {
|
|
3309
|
+
let li = 0;
|
|
3310
|
+
c.levels.forEach((L, i) => { if (m(c.box, L.w) <= mppScreen * 1.0001) li = i; });
|
|
3311
|
+
const L = c.levels[li];
|
|
3312
|
+
if (m(c.box, L.w) * 1.5 < m(b.box, b.w0)) S.lv = { c, li, L, ntx: Math.ceil(L.w / L.tw), tiles: new Map() };
|
|
3313
|
+
}
|
|
3314
|
+
return S;
|
|
3315
|
+
};
|
|
3316
|
+
const fams = { eqc: sampler('eqc'), S: sampler('S'), N: sampler('N') };
|
|
3317
|
+
const img = new ImageData(gw, gh), out = new Uint32Array(img.data.buffer), want = [];
|
|
3318
|
+
let lastId = -1, lastT = null, lastLv = null;
|
|
3319
|
+
for (let k = 0; k < n; k++) {
|
|
3320
|
+
const lo = rp.lon[k], la = rp.lat[k];
|
|
3321
|
+
if (lo !== lo) continue;
|
|
3322
|
+
const F = (la <= -72 && fams.S) || (la >= 72 && fams.N) || fams.eqc;
|
|
3323
|
+
if (!F) continue;
|
|
3324
|
+
let X = lo, Y = la;
|
|
3325
|
+
if (F.fam !== 'eqc') [X, Y] = psXY(la, lo, F.fam);
|
|
3326
|
+
const lv = F.lv;
|
|
3327
|
+
if (lv) {
|
|
3328
|
+
const bx = lv.c.box, u = (F.fam === 'eqc' ? mod(X - bx[0], 360) : X - bx[0]) / (bx[2] - bx[0]), v = (bx[3] - Y) / (bx[3] - bx[1]);
|
|
3329
|
+
if (u >= 0 && u < 1 && v >= 0 && v < 1) {
|
|
3330
|
+
const L = lv.L, px = Math.floor(u * L.w), py = Math.floor(v * L.h), tx = Math.floor(px / L.tw), ty = Math.floor(py / L.th);
|
|
3331
|
+
const id = ty * lv.ntx + tx;
|
|
3332
|
+
if (id !== lastId || lv !== lastLv) {
|
|
3333
|
+
[lastId, lastLv] = [id, lv];
|
|
3334
|
+
lastT = lv.tiles.get(id);
|
|
3335
|
+
if (lastT === undefined) {
|
|
3336
|
+
const key = `bm|${lv.c.key}|${lv.li}|${tx}|${ty}`, t = tileGet(key);
|
|
3337
|
+
lastT = t ? { px: tilePx(t), w: t.w } : null;
|
|
3338
|
+
lv.tiles.set(id, lastT);
|
|
3339
|
+
if (!t) want.push({ key, src: lv.c, li: lv.li, tx, ty, d: Math.hypot((k % gw) - gw / 2, Math.floor(k / gw) - gh / 2) });
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
const val = lastT ? lastT.px[(py - ty * L.th) * lastT.w + (px - tx * L.tw)] : 0;
|
|
3343
|
+
if (val >>> 24) { out[k] = val; continue; }
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
const bx = F.b.box, u = (F.fam === 'eqc' ? mod(X - bx[0], 360) : X - bx[0]) / (bx[2] - bx[0]), v = (bx[3] - Y) / (bx[3] - bx[1]);
|
|
3347
|
+
if (!(u >= 0 && u < 1 && v >= 0 && v < 1)) continue;
|
|
3348
|
+
out[k] = 0xff000000 | (F.px[Math.floor(v * F.b.h0) * F.b.w0 + Math.floor(u * F.b.w0)] * 0x010101);
|
|
3349
|
+
}
|
|
3350
|
+
if (rp.out.width !== gw || rp.out.height !== gh) [rp.out.width, rp.out.height] = [gw, gh];
|
|
3351
|
+
rp.out.getContext('2d').putImageData(img, 0, 0);
|
|
3352
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3353
|
+
ctx.imageSmoothingEnabled = true;
|
|
3354
|
+
ctx.drawImage(rp.out, 0, 0, gw * f, gh * f);
|
|
3355
|
+
return (rp.want = want);
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3042
3358
|
function placeContours(view, W, H) {
|
|
3043
3359
|
const c = st.contour;
|
|
3044
3360
|
if (c) {
|
|
@@ -3053,23 +3369,53 @@
|
|
|
3053
3369
|
st.ctimer = setTimeout(() => computeContours({ ...view }, W, H), 300);
|
|
3054
3370
|
}
|
|
3055
3371
|
|
|
3056
|
-
// DEM samples about 3 screen px apart over the
|
|
3057
|
-
|
|
3058
|
-
|
|
3372
|
+
// DEM samples about 3 screen px apart over the window, from the coarsest adequate level
|
|
3373
|
+
// (at most about 2 M samples)
|
|
3374
|
+
async function demGrid(src, g, [vx0, vx1, vy0, vy1], want, period, polar) {
|
|
3059
3375
|
let li = 0;
|
|
3060
3376
|
src.levels.forEach((L, i) => { if (((g.Y1 - g.Y0) / L.h) <= want) li = i; });
|
|
3377
|
+
const cells = (L) => (((vx1 - vx0) * L.w) / (g.X1 - g.X0)) * Math.min(L.h, ((vy1 - vy0) * L.h) / (g.Y1 - g.Y0));
|
|
3378
|
+
while (li < src.levels.length - 1 && cells(src.levels[li]) > 2e6) li++;
|
|
3061
3379
|
const L = src.levels[li], ux = (g.X1 - g.X0) / L.w, uy = (g.Y1 - g.Y0) / L.h;
|
|
3062
|
-
const vx0 = view.cx - W / 2 / view.k, vx1 = view.cx + W / 2 / view.k;
|
|
3063
|
-
const vy0 = view.cy - H / 2 / view.k, vy1 = view.cy + H / 2 / view.k;
|
|
3064
3380
|
const r0 = Math.max(0, Math.floor((vy0 - g.Y0) / uy) - 1), r1 = Math.min(L.h, Math.ceil((vy1 - g.Y0) / uy) + 1);
|
|
3065
3381
|
const parts = [];
|
|
3066
|
-
for (const dx of copies(g, vx0, vx1)) {
|
|
3382
|
+
for (const dx of copies(g, vx0, vx1, period)) {
|
|
3067
3383
|
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);
|
|
3068
3384
|
if (c1 - c0 < 2 || r1 - r0 < 2) continue;
|
|
3069
3385
|
const z = (await readWindow({ ...src, samples: [0] }, L, [c0, r0, c1, r1], ctl.signal))[0];
|
|
3070
3386
|
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 });
|
|
3071
3387
|
}
|
|
3072
|
-
return { parts, mpp:
|
|
3388
|
+
return { parts, mpp: polar ? uy : uy * RAD * MOON_RADIUS_KM * 1000 };
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// Reprojected map: the DEM family for the view (polar when it all lies poleward of 72 deg)
|
|
3392
|
+
// and the view's window in that raster's data coordinates, from a grid of view points
|
|
3393
|
+
function viewWindow(view, W, H) {
|
|
3394
|
+
const vx0 = view.cx - W / 2 / view.k, vy0 = view.cy - H / 2 / view.k, pts = [];
|
|
3395
|
+
for (let j = 0; j <= 16; j++) {
|
|
3396
|
+
for (let i = 0; i <= 16; i++) {
|
|
3397
|
+
const ll = pj.inv([vx0 + (W / view.k) * (i / 16), vy0 + (H / view.k) * (j / 16)]);
|
|
3398
|
+
if (ll) pts.push(ll);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
if (!pts.length) return null;
|
|
3402
|
+
const poleIn = (la) => {
|
|
3403
|
+
const p = pj.fwd(0, la);
|
|
3404
|
+
return p && Math.abs(p[0] - view.cx) <= W / 2 / view.k && Math.abs(p[1] - view.cy) <= H / 2 / view.k;
|
|
3405
|
+
};
|
|
3406
|
+
const lats = pts.map((p) => p[1]);
|
|
3407
|
+
const fam = lats.every((la) => la < -72) ? 'S' : lats.every((la) => la > 72) ? 'N' : 'eqc';
|
|
3408
|
+
if (fam !== 'eqc') {
|
|
3409
|
+
const xy = pts.map(([lo, la]) => psXY(la, lo, fam));
|
|
3410
|
+
if (poleIn(fam === 'S' ? -90 : 90)) xy.push([0, 0]);
|
|
3411
|
+
const [x0, x1, y0, y1] = bboxOf([xy]), pad = 0.03 * Math.max(x1 - x0, y1 - y0) + 1;
|
|
3412
|
+
return { fam, win: [x0 - pad, x1 + pad, -y1 - pad, -y0 + pad] };
|
|
3413
|
+
}
|
|
3414
|
+
let [w, span] = poleIn(-90) || poleIn(90) ? [-180, 360] : coveringArc(pts.map(([lo]) => [lo, 0]));
|
|
3415
|
+
if (span > 300) [w, span] = [-180, 360];
|
|
3416
|
+
const s0 = poleIn(-90) ? -90 : minOf(lats), n0 = poleIn(90) ? 90 : maxOf(lats);
|
|
3417
|
+
const px = 0.03 * span + 0.01, py = 0.03 * (n0 - s0) + 0.01;
|
|
3418
|
+
return { fam, win: span >= 360 ? [-180, 180, -n0 - py, -s0 + py] : [w - px, w + span + px, -n0 - py, -s0 + py] };
|
|
3073
3419
|
}
|
|
3074
3420
|
|
|
3075
3421
|
async function computeContours(view, W, H) {
|
|
@@ -3077,22 +3423,25 @@
|
|
|
3077
3423
|
st.cbusy = true;
|
|
3078
3424
|
status();
|
|
3079
3425
|
try {
|
|
3080
|
-
const
|
|
3081
|
-
const
|
|
3082
|
-
|
|
3083
|
-
const
|
|
3084
|
-
const
|
|
3085
|
-
const
|
|
3426
|
+
const nat = affine(view, W, H);
|
|
3427
|
+
const vw = nat ? { fam: proj, k: sc, win: [view.cx - W / 2 / view.k, view.cx + W / 2 / view.k, view.cy - H / 2 / view.k, view.cy + H / 2 / view.k] }
|
|
3428
|
+
: { k: 1, ...viewWindow(view, W, H) };
|
|
3429
|
+
const polar = vw.fam !== 'eqc', gm = (src) => geom(src, vw.k);
|
|
3430
|
+
const want = 3 / view.k / (nat || polar ? 1 : RAD * MOON_RADIUS_KM * 1000);
|
|
3431
|
+
const [vx0, vx1, vy0, vy1] = vw.win ?? [];
|
|
3432
|
+
const refs = vw.fam ? CONTOUR_DEMS[vw.fam].filter((ref) => ref !== PGDA_80S || relayable(ref)) : [];
|
|
3433
|
+
const bundled = refs.length ? await bmLoad(refs.at(-1)).promise : null;
|
|
3434
|
+
const res = bundled ? (gm(bundled).Y1 - gm(bundled).Y0) / bundled.h0 : Infinity;
|
|
3086
3435
|
let grid = null, name = '';
|
|
3087
3436
|
for (const ref of refs) {
|
|
3088
3437
|
const last = ref === refs.at(-1);
|
|
3089
3438
|
if (last ? !bundled : res <= want) continue;
|
|
3090
3439
|
const src = last ? bundled : await bmLoad(ref).promise;
|
|
3091
3440
|
if (gen !== st.gen) return;
|
|
3092
|
-
const g = src &&
|
|
3093
|
-
if (!src || (!last &&
|
|
3441
|
+
const g = src && gm(src);
|
|
3442
|
+
if (!src || (!last && polar && !(vx0 >= g.X0 && vx1 <= g.X1 && vy0 >= g.Y0 && vy1 <= g.Y1))) continue;
|
|
3094
3443
|
try {
|
|
3095
|
-
grid = await demGrid(src,
|
|
3444
|
+
grid = await demGrid(src, g, vw.win, want, polar ? 0 : 360 * vw.k, polar);
|
|
3096
3445
|
grid.noData = src.noData;
|
|
3097
3446
|
name = fileName(ref.startsWith('https:') ? ref : bmUrl(ref)).replace(/(_cog)?\.tif$/i, '');
|
|
3098
3447
|
break;
|
|
@@ -3106,7 +3455,11 @@
|
|
|
3106
3455
|
if (!(hi > lo)) { st.contour = null; paths.forEach((p) => p.removeAttribute('d')); cinfo.textContent = grid ? '| no relief in view' : ''; return; }
|
|
3107
3456
|
const step = step125(hi - lo, 10);
|
|
3108
3457
|
const X = (x) => W / 2 + (x - view.cx) * view.k, Y = (y) => H / 2 + (y - view.cy) * view.k;
|
|
3109
|
-
const
|
|
3458
|
+
const P = nat ? (x, y) => [X(x), Y(y)] : (x, y) => {
|
|
3459
|
+
const q = polar ? pj.fwd(...psLonLat(x, -y, vw.fam)) : pj.fwd(x, -y);
|
|
3460
|
+
return q && [X(q[0]), Y(q[1])];
|
|
3461
|
+
};
|
|
3462
|
+
const d = contourPaths(grid.parts, grid.noData, step, P);
|
|
3110
3463
|
paths.forEach((p, i) => p.setAttribute('d', d[i]));
|
|
3111
3464
|
st.contour = { ...view, W, H };
|
|
3112
3465
|
contours.removeAttribute('transform');
|
|
@@ -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.dev14
|
|
4
4
|
Summary: Find, download and process lunar orbital imagery (LROC, ShadowCam, Kaguya TC) and build stereo and shape-from-shading DEMs with ISIS + ASP
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Keywords: moon,lroc,shadowcam,kaguya,isis,ames-stereo-pipeline,shape-from-shading,dem
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lunar-toolkit"
|
|
7
|
-
version = "0.1.0.
|
|
7
|
+
version = "0.1.0.dev14"
|
|
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"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/manifest.json
RENAMED
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_global.jpg
RENAMED
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_north.jpg
RENAMED
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit/static/basemaps/wac_south.jpg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lunar_toolkit-0.1.0.dev13 → lunar_toolkit-0.1.0.dev14}/lunar_toolkit.egg-info/top_level.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
|