asp-plot 2.2.0__tar.gz → 3.0.0__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.
- {asp_plot-2.2.0 → asp_plot-3.0.0}/AGENTS.md +1 -1
- {asp_plot-2.2.0 → asp_plot-3.0.0}/ARCHITECTURE.md +24 -25
- {asp_plot-2.2.0 → asp_plot-3.0.0}/CHANGELOG.md +16 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/PKG-INFO +2 -2
- {asp_plot-2.2.0 → asp_plot-3.0.0}/README.md +1 -1
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/bundle_adjust.py +14 -5
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/asp_report.py +43 -48
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/csm_camera_plot.py +31 -31
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/gallery.py +9 -8
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/request_planetary_altimetry.py +4 -4
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/stereo_geom.py +9 -9
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/processing_parameters.py +15 -2
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/report_pipeline.py +37 -34
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/utils.py +36 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/pyproject.toml +1 -1
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.flake8 +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/ISSUE_TEMPLATE/problem-report.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/workflows/claude-code-review.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/workflows/claude.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/workflows/release.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.github/workflows/run-tests.yml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.gitignore +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.pre-commit-config.yaml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/.readthedocs.yaml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/CLAUDE.md +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/LICENSE +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/__init__.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/alignment.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/altimetry.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/altimetry_plots.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/altimetry_source.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/asp_log.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/bodies.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/cli/__init__.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/csm_analysis.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/csm_camera.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/csm_io.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/gallery.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/icesat2_source.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/mapproject.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/planetary_source.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/report.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/report_captions.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/scenes.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/selections.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/__init__.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/aster.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/base.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/dimap.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/dimap_v1.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/rpc.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/sensors/worldview.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/stereo.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/stereo_geometry.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/asp_plot/stereopair_metadata_parser.py +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/conda-forge-recipe/meta.yaml +0 -0
- {asp_plot-2.2.0 → asp_plot-3.0.0}/environment.yml +0 -0
|
@@ -54,7 +54,7 @@ sphinx-autobuild docs docs/_build/html --open-browser # or sphinx-build for a
|
|
|
54
54
|
## External Data Sources
|
|
55
55
|
|
|
56
56
|
- **ICESat-2 ATL06-SR** (Earth): requested through the SlideRule API; results cached as parquet next to the report.
|
|
57
|
-
- **LOLA (Moon) / MOLA (Mars)**: async queries to the ODE GDS REST API via the `request_planetary_altimetry` CLI — the user gets a download link by email, then passes the CSV to `asp_report --
|
|
57
|
+
- **LOLA (Moon) / MOLA (Mars)**: async queries to the ODE GDS REST API via the `request_planetary_altimetry` CLI — the user gets a download link by email, then passes the CSV to `asp_report --altimetry-csv`. Coordinates are east-positive 0–360 longitude, planetocentric latitude.
|
|
58
58
|
|
|
59
59
|
## Testing
|
|
60
60
|
|
|
@@ -42,7 +42,7 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
42
42
|
- `pixel_window_to_bbox()` / `bbox_to_pixel_offset()`: convert a detailed-hillshade clip between a DEM-CRS map bbox and a top-left pixel offset. `bbox_to_pixel_offset` uses `rowcol(..., op=round)` (round-to-nearest, **not** floor) so a replayed clip doesn't drift by a pixel from floating-point error
|
|
43
43
|
- `reproject_bbox(bbox, src_crs, dst_crs)`: reprojects a clip bbox between CRSs (via `rasterio.warp.transform_bounds`) so clip reuse works across stereo variants in different projections (e.g. MOC non-mapproj Stereographic vs mapproj Sinusoidal). `plot_detailed_hillshade(clip_windows_crs=...)` passes the manifest's `dem_crs`; reprojection is a no-op when CRSs match
|
|
44
44
|
- Deliberately imports nothing from `report.py` / `fpdf`, so it is safe to use from notebooks
|
|
45
|
-
- The `asp_report` CLI always writes `<report_stem>_figure_selections.yml` next to the report; `--
|
|
45
|
+
- The `asp_report` CLI always writes `<report_stem>_figure_selections.yml` next to the report; `--reuse-selections PATH` replays a prior run's choices
|
|
46
46
|
|
|
47
47
|
**`report.py`** - PDF report generation using fpdf2
|
|
48
48
|
- `ReportSection`: Dataclass representing a report figure (title, image path, caption)
|
|
@@ -53,13 +53,13 @@ The package is organized by functionality, with each module focused on a specifi
|
|
|
53
53
|
- `_add_processing_parameters_page()`: helper that renders the runtime summary table plus the bundle_adjust / stereo / point2dem / report commands on page 2 (moved from the trailing page as of v1.13.0). Also renders the reconstructed `mapproject` command(s) (from the optional `mapproject` list key; see `mapproject.py`) with a "reconstructed from output metadata" note.
|
|
54
54
|
- `_fmt_sig()`: formats a number compactly — 2 decimals for |x| < 10, 1 decimal for 10 ≤ |x| < 100, 0 decimals above, "n/a" for non-finite. Used for alignment stats.
|
|
55
55
|
- Title page displays: processing date, ASP version (from logs), asp_plot version (from package metadata)
|
|
56
|
-
- Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if
|
|
56
|
+
- Page order: title + DEM summary → Processing Parameters → diagnostic figures → (if the `pc_align` step ran) alignment report page + aligned-DEM figures.
|
|
57
57
|
- `report.py` is fed declaratively by `report_pipeline.py`; it was **not** rewritten in #128.
|
|
58
58
|
|
|
59
59
|
**`report_pipeline.py`** - Declarative report pipeline behind the `asp_report` CLI (issue #128)
|
|
60
60
|
- `ReportConfig`: dataclass packing the ~18 CLI options into one Click-free object (field names/defaults mirror the options one-for-one, guarded by a test)
|
|
61
61
|
- `run_report(config)`: importable/callable from notebooks and tests with no Click context; returns the written PDF path
|
|
62
|
-
- A declarative section registry (`REPORT_SECTIONS`) of `ReportSpec`s replaces the old inline plot-and-append wall: each spec pairs an `enabled(ctx)` predicate with a `build(ctx)` function returning the sections to append. `--
|
|
62
|
+
- A declarative section registry (`REPORT_SECTIONS`) of `ReportSpec`s replaces the old inline plot-and-append wall: each spec pairs an `enabled(ctx)` predicate with a `build(ctx)` function returning the sections to append. `--no-geometry` / `--no-altimetry` / `--no-pc-align` gating are predicates; figure numbering comes from a per-run counter on the shared `ReportContext`, so section order and numbering are data, not source-line position. The alignment "Page B/C/D" follow-ups are one spec emitting several sections
|
|
63
63
|
- Section builders: `_build_input_scenes`, `_build_stereo_geometry`, `_build_match_points`, `_build_bundle_adjust`, `_build_disparity`, `_build_dem_results`, `_build_detailed_hillshade`, `_build_altimetry` (→ `_build_altimetry_earth` / `_build_altimetry_planetary`)
|
|
64
64
|
- `_numbered_sections()`: shared helper turning a plotter's saved-filename list into one `ReportSection` per figure (first gets the caption, continuations are titled "... (continued)") — used by the scenes/geometry/match/disparity builders, whose plotters save one figure per pair on multi-view runs (issue #160)
|
|
65
65
|
|
|
@@ -218,28 +218,27 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
|
|
|
218
218
|
**`asp_report.py`** - Main CLI tool (`asp_report` command)
|
|
219
219
|
- Generates comprehensive PDF reports of ASP processing
|
|
220
220
|
- A **thin Click wrapper** (issue #128): parses the ~18 options into a `ReportConfig` and calls `report_pipeline.run_report(config)`. All orchestration lives in `report_pipeline.py` (see the module structure section); the CLI file itself is now just option definitions + the `ReportConfig` build
|
|
221
|
-
- Accepts
|
|
221
|
+
- Accepts a directory for stereo outputs and a directory or ASP-style prefix for bundle_adjust outputs; trailing slashes are tolerated (issue #60)
|
|
222
222
|
- Options for reference DEMs, ICESat-2 comparisons, basemaps
|
|
223
223
|
- Report section order: Input Scenes → Stereo Geometry → Match Points → Bundle Adjust panels (Log/Linear Residuals, Map-Projected Residuals, Geodiff) → Disparity → DEM Results → Detailed Hillshade → Altimetry panels. Disparity follows Bundle Adjust (not Hillshade) and DEM Results precedes Hillshade, so the title page → inputs → per-step diagnostics → final DEM products narrative reads top-down.
|
|
224
224
|
- Title-page `ReportMetadata` is populated here; `get_acquisition_dates()` is called with the main directory plus stereo/BA subdirs as `extra_dirs` so the Acquisition Date(s) row appears on both raw-input layouts (WV XMLs at top level) and layouts where XMLs live one level deep.
|
|
225
225
|
- `--directory`: Root ASP processing directory (default: `./`)
|
|
226
|
-
- `--
|
|
227
|
-
- `--
|
|
228
|
-
- `--
|
|
229
|
-
- `--
|
|
230
|
-
- `--
|
|
231
|
-
- `--
|
|
232
|
-
- `--
|
|
233
|
-
- `--
|
|
234
|
-
- `--
|
|
235
|
-
- `--
|
|
236
|
-
- `--
|
|
237
|
-
- `--
|
|
238
|
-
- `--
|
|
239
|
-
- `--
|
|
240
|
-
- `--
|
|
241
|
-
- `--
|
|
242
|
-
- `--report_title`: Custom report title (default: directory name)
|
|
226
|
+
- `--stereo-directory`: Stereo output subdirectory (default: `stereo`)
|
|
227
|
+
- `--bundle-adjust-prefix`: Optional BA output location — either a directory (`ba`) or an ASP-style output prefix (`ba/run`, as passed to stereo/mapproject); a prefix narrows file globs to that run's outputs
|
|
228
|
+
- `--dem-filename`: Custom DEM filename (default: auto-detect `*-DEM.tif`)
|
|
229
|
+
- `--dem-gsd`: Custom DEM ground sample distance
|
|
230
|
+
- `--map-crs`: Projection as `EPSG:XXXX` (default: auto-detect from DEM, fallback `EPSG:4326`)
|
|
231
|
+
- `--reference-dem`: Reference DEM path (auto-detected from logs if not supplied)
|
|
232
|
+
- `--no-basemap`: Skip the Esri WorldImagery basemaps added by default (which require internet)
|
|
233
|
+
- `--no-altimetry`: Skip the altimetry comparisons plotted by default. Auto-detects planetary body from DEM CRS: Earth → ICESat-2 (requires internet), Moon → LOLA, Mars → MOLA. For planetary DEMs, requires `--altimetry-csv`.
|
|
234
|
+
- `--altimetry-csv`: Path to a LOLA/MOLA CSV from the ODE GDS API. **Mars: must be the `*_pts_csv.csv` (not `*_topo_csv.csv`) — the loader requires the `PLANET_RAD` column to avoid the oblate-areoid offset.** Moon accepts either the `*_topo_simple_csv.csv` (results=u) or the `*_pts_csv.csv` (results=p). Obtained via the `request_planetary_altimetry` CLI tool.
|
|
235
|
+
- `--no-pc-align`: Skips the `pc_align` step; by default (when altimetry is on) the report runs `pc_align` against the reference altimetry (ICESat-2 for Earth, MOLA for Mars, LOLA for Moon) after the existing altimetry plots and appends an alignment report. **Earth success path**: adds four pages (alignment report page, pre/post landcover histogram, aligned profile, aligned best/worst segments). **Planetary success path**: adds three pages (alignment report page, pre/post mapview, pre/post histogram). `insufficient_points` and `no_improvement` outcomes emit a single alignment report page on either branch. Skipped automatically when `--no-altimetry` is set.
|
|
236
|
+
- `--no-geometry`: Skip the stereo geometry plots drawn by default (used for planetary missions)
|
|
237
|
+
- `--subset-km`: Hillshade subset size in km (default: 1.0)
|
|
238
|
+
- `--atl06sr-time-range`: Time range for ICESat-2 ATL06-SR requests. `"all"` (default) for full mission, `"auto"` for scene metadata ±1 year, `"START,END"` for a custom range, or a single date (buffered by ±1 year).
|
|
239
|
+
- `--reuse-selections`: Path to a `*_figure_selections.yml` from a prior run. Replays that run's ICESat-2 points (parquet), profile track, best/worst segments, and detailed-hillshade clips so re-processing runs (e.g. mapproj vs non-mapproj) are directly comparable (issue #121). Every run always writes `<report_stem>_figure_selections.yml` next to the report (the `regenerate_reports.sh` paired variants use this to reuse each other). Generated sidecars are gitignored (they hardcode absolute local paths); a sanitized example is in `docs/cli/asp_report.md`.
|
|
240
|
+
- `--report-filename`: PDF report filename or path. A bare filename saves in the stereo directory; a path (e.g. `reports/report.pdf`) is used as-is. Default is auto-generated as `asp_report_<report_title>_<UTC timestamp>.pdf`
|
|
241
|
+
- `--report-title`: Custom report title (default: directory name)
|
|
243
242
|
|
|
244
243
|
**`csm_camera_plot.py`** - CSM camera comparison tool (`csm_camera_plot` command)
|
|
245
244
|
- Wrapper for `csm_camera.py` functions
|
|
@@ -253,7 +252,7 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
|
|
|
253
252
|
- `--email`: Email for notification when query finishes (required)
|
|
254
253
|
- `--channels`: LOLA detector channels (Moon only, default `tffff` = channel 1 only)
|
|
255
254
|
- Saves request metadata as `altimetry_request_info.yml` alongside the DEM
|
|
256
|
-
- Workflow: submit query → receive email → download/unzip → pass `*_pts_csv.csv` (Mars) or `*_topo_simple_csv.csv`/`*_pts_csv.csv` (Moon) to `asp_report --
|
|
255
|
+
- Workflow: submit query → receive email → download/unzip → pass `*_pts_csv.csv` (Mars) or `*_topo_simple_csv.csv`/`*_pts_csv.csv` (Moon) to `asp_report --altimetry-csv`
|
|
257
256
|
|
|
258
257
|
**`stereo_geom.py`** - Stereo geometry visualization tool (`stereo_geom` command)
|
|
259
258
|
- Wrapper for `StereoGeometryPlotter`
|
|
@@ -265,7 +264,7 @@ All CLI tools are in `asp_plot/cli/` and use Click for argument parsing:
|
|
|
265
264
|
**`gallery.py`** - DEM gallery tool (`gallery` command)
|
|
266
265
|
- Wrapper for `GalleryPlotter`; lays out many DEMs as a grid sharing one color scale
|
|
267
266
|
- `--directory` + `--pattern` (supports recursive `**` for subdirectories) or an explicit list of `FILES` (files take precedence)
|
|
268
|
-
- `--
|
|
267
|
+
- `--no-hillshade`, `--cmap`, `--downsample`, `--max-filesize-mb`, `--title`, `--output-directory/--output-filename`
|
|
269
268
|
- Saves `<dirname>_gallery.png` into the input directory by default
|
|
270
269
|
|
|
271
270
|
## Documentation Website
|
|
@@ -307,7 +306,7 @@ docs/
|
|
|
307
306
|
- **`html_extra_path`** is used to serve `notebooks/figures/` at the correct relative path for notebook `<img src>` references.
|
|
308
307
|
- **`docs/` is excluded from sdist** in `pyproject.toml` so docs never ship in the PyPI/conda package.
|
|
309
308
|
- **Changelog** uses `{include} ../CHANGELOG.md` so there's one source of truth.
|
|
310
|
-
- **Selective notebook exclusion**: `.readthedocs.yaml`'s `pre_build` copies every `notebooks/**/*.ipynb` into `docs/examples/notebooks/`, but specific notebooks can be dropped from the build by listing them in `exclude_patterns` in `docs/conf.py` (e.g., `worldview_utqiagvik_stereo.ipynb`). Inter-notebook links in the WorldView examples use fully-qualified `https://asp-plot.readthedocs.io/en/latest/...` URLs so they resolve both on RTD and in raw notebook previews. Report-link convention: each notebook's "Full Report Generation" section ends with a `#### See the resulting [report](https://asp-plot.readthedocs.io/en/latest/_static/reports/<filename>.pdf).` line that links directly to the PDF served from `_static/reports/`. The `<filename>` must match what the cell's `!asp_report --
|
|
309
|
+
- **Selective notebook exclusion**: `.readthedocs.yaml`'s `pre_build` copies every `notebooks/**/*.ipynb` into `docs/examples/notebooks/`, but specific notebooks can be dropped from the build by listing them in `exclude_patterns` in `docs/conf.py` (e.g., `worldview_utqiagvik_stereo.ipynb`). Inter-notebook links in the WorldView examples use fully-qualified `https://asp-plot.readthedocs.io/en/latest/...` URLs so they resolve both on RTD and in raw notebook previews. Report-link convention: each notebook's "Full Report Generation" section ends with a `#### See the resulting [report](https://asp-plot.readthedocs.io/en/latest/_static/reports/<filename>.pdf).` line that links directly to the PDF served from `_static/reports/`. The `<filename>` must match what the cell's `!asp_report --report-filename` writes (so the URL on RTD actually resolves).
|
|
311
310
|
|
|
312
311
|
### Dependencies
|
|
313
312
|
|
|
@@ -337,7 +336,7 @@ Docs dependencies are in `pyproject.toml` under `[project.optional-dependencies]
|
|
|
337
336
|
|
|
338
337
|
**ODE GDS REST API**: Base URL `https://oderest.rsl.wustl.edu/livegds`. Queries are submitted via `gds_query_async()` in async mode. The `request_planetary_altimetry` CLI submits the query and the user downloads results via email link. Coordinates use east-positive 0-360 longitude and planetocentric latitude.
|
|
339
338
|
|
|
340
|
-
**Basemaps**: Uses `contextily` to fetch Esri WorldImagery tiles (requires internet). Can be disabled with `--
|
|
339
|
+
**Basemaps**: Uses `contextily` to fetch Esri WorldImagery tiles (requires internet). Can be disabled with `--no-basemap`. Automatically skipped for planetary (non-Earth) DEMs.
|
|
341
340
|
|
|
342
341
|
## Example Notebooks
|
|
343
342
|
|
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.0] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
A breaking standardization of the command-line interfaces ([#60](https://github.com/uw-cryo/asp_plot/issues/60)), done deliberately as a clean break — no aliases, no deprecation period — while the user base is small. Every multi-word option across the five CLIs (`asp_report`, `stereo_geom`, `csm_camera_plot`, `gallery`, `request_planetary_altimetry`) moves from underscores to the hyphenated style ASP itself uses (`--stereo_directory` → `--stereo-directory`), booleans become single switches for the non-default behavior (`--add_basemap False` → `--no-basemap`), and `--bundle_adjust_directory` becomes `--bundle-adjust-prefix`, matching both the name and the semantics of ASP's own option. The Python API is unchanged.
|
|
11
|
+
|
|
12
|
+
A major version because every existing `asp_report`/`stereo_geom`/`csm_camera_plot`/`gallery` invocation with a multi-word or boolean option needs editing. **Upgrade:** re-spell the flags per the entries below — the `--help` of each command lists the new names, and the committed example reports and notebooks show them in use. No new dependencies and no entry-point changes.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **All CLI options are hyphenated** (issue [#60](https://github.com/uw-cryo/asp_plot/issues/60)). One-for-one renames: `--stereo-directory`, `--dem-filename`, `--dem-gsd`, `--map-crs`, `--reference-dem`, `--altimetry-csv`, `--subset-km`, `--atl06sr-time-range`, `--reuse-selections`, `--report-filename`, `--report-title` (asp_report); `--output-directory`, `--output-filename` (stereo_geom, gallery); `--max-filesize-mb` (gallery); `--original-cameras`, `--optimized-cameras`, `--map-crs`, `--upper-magnitude-percentile` (csm_camera_plot). Single-word options are untouched. The command recorded on the report's final page is emitted with the new spellings, so it stays re-runnable.
|
|
16
|
+
- **Boolean options are now single switches for the non-default behavior**, in the Unix idiom: where the default is on, only the negative switch exists — `--no-basemap`, `--no-altimetry`, `--no-pc-align`, `--no-geometry` (asp_report), `--no-basemap` (stereo_geom), `--no-trim` (csm_camera_plot), `--no-hillshade` (gallery, replacing the `--hillshade/--no-hillshade` pair) — and where the default is off, only the positive one — `--shared-scales`, `--log-scale-positions`, `--log-scale-angles`, `--add-basemap` (csm_camera_plot). Where you passed `--add_basemap False`, pass `--no-basemap`; the verb-y `plot`/`add` prefixes are dropped from the switch names.
|
|
17
|
+
- **`--bundle_adjust_directory` is now `--bundle-adjust-prefix`**, and accepts either the containing directory (`ba`, the previous behavior) or the same ASP-style output prefix passed to `stereo`/`mapproject` (`ba/run`). A prefix narrows the residuals/log file search to that run's outputs, so several bundle-adjust runs can share a directory. Internally, `ReportConfig.bundle_adjust_directory` is renamed to `bundle_adjust_prefix`.
|
|
18
|
+
- **`csm_camera_plot` options are harmonized with the other CLIs and ASP's `orbit_plot.py`**: `--save_dir` → `--output-directory`, `--fig_fn` → `--output-filename`, `--figsize` → `--figure-size`. The `csm_camera_summary_plot()` Python keyword arguments are unchanged.
|
|
19
|
+
- **Trailing slashes on directory options are stripped centrally** (`--directory`, `--stereo-directory`, `--bundle-adjust-prefix`), closing the last thread of issue [#60](https://github.com/uw-cryo/asp_plot/issues/60); the dg_mosaic `r100` concern from the same issue was resolved earlier by the sensor readers, which treat `*.r100.xml`/`*.r50.xml` as regenerable intermediates.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- **The deprecated `--plot_icesat` alias** (deprecated in favor of `--plot_altimetry` in 1.10.0) is gone; altimetry is on by default and `--no-altimetry` disables it.
|
|
23
|
+
|
|
8
24
|
## [2.2.0] - 2026-08-18
|
|
9
25
|
|
|
10
26
|
A correctness release for the CSM camera comparison, and a new diagnostic layer under the match points.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asp_plot
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Package for plotting outputs Ames Stereo Pipeline processing
|
|
5
5
|
Project-URL: Homepage, https://github.com/uw-cryo/asp_plot
|
|
6
6
|
Project-URL: Documentation, https://asp-plot.readthedocs.io
|
|
@@ -74,7 +74,7 @@ See the [installation guide](https://asp-plot.readthedocs.io/en/latest/installat
|
|
|
74
74
|
Generate a PDF report from an ASP processing directory:
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
asp_report --directory ./ --
|
|
77
|
+
asp_report --directory ./ --stereo-directory stereo
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
|
|
@@ -30,7 +30,7 @@ See the [installation guide](https://asp-plot.readthedocs.io/en/latest/installat
|
|
|
30
30
|
Generate a PDF report from an ASP processing directory:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
asp_report --directory ./ --
|
|
33
|
+
asp_report --directory ./ --stereo-directory stereo
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
See the [CLI documentation](https://asp-plot.readthedocs.io/en/latest/cli/index.html) for all options and additional tools (`stereo_geom`, `csm_camera_plot`).
|
|
@@ -41,7 +41,7 @@ class ReadBundleAdjustFiles:
|
|
|
41
41
|
>>> mapproj_gdf = ba_reader.get_mapproj_residuals_gdf()
|
|
42
42
|
"""
|
|
43
43
|
|
|
44
|
-
def __init__(self, directory, bundle_adjust_directory):
|
|
44
|
+
def __init__(self, directory, bundle_adjust_directory, stem=None):
|
|
45
45
|
"""
|
|
46
46
|
Initialize the ReadBundleAdjustFiles object.
|
|
47
47
|
|
|
@@ -51,10 +51,15 @@ class ReadBundleAdjustFiles:
|
|
|
51
51
|
Root directory of ASP processing
|
|
52
52
|
bundle_adjust_directory : str
|
|
53
53
|
Subdirectory containing bundle adjustment outputs
|
|
54
|
+
stem : str, optional
|
|
55
|
+
Run stem of an ASP output prefix (the ``run`` in ``ba/run``).
|
|
56
|
+
When given, file globs are narrowed to that run's outputs;
|
|
57
|
+
otherwise any run in the directory matches.
|
|
54
58
|
"""
|
|
55
59
|
self.directory = os.path.expanduser(directory)
|
|
56
60
|
self.bundle_adjust_directory = bundle_adjust_directory
|
|
57
61
|
self.full_directory = os.path.join(self.directory, bundle_adjust_directory)
|
|
62
|
+
self.stem_glob = stem if stem else "*"
|
|
58
63
|
|
|
59
64
|
def get_csv_paths(self, geodiff_files=False):
|
|
60
65
|
"""
|
|
@@ -86,8 +91,8 @@ class ReadBundleAdjustFiles:
|
|
|
86
91
|
will attempt to generate them using the geodiff ASP tool.
|
|
87
92
|
"""
|
|
88
93
|
filenames = [
|
|
89
|
-
"
|
|
90
|
-
"
|
|
94
|
+
f"{self.stem_glob}-initial_residuals_pointmap.csv",
|
|
95
|
+
f"{self.stem_glob}-final_residuals_pointmap.csv",
|
|
91
96
|
]
|
|
92
97
|
|
|
93
98
|
if geodiff_files:
|
|
@@ -371,7 +376,9 @@ class ReadBundleAdjustFiles:
|
|
|
371
376
|
the distance between matched interest points in the map-projected
|
|
372
377
|
images after bundle adjustment.
|
|
373
378
|
"""
|
|
374
|
-
path = glob_file(
|
|
379
|
+
path = glob_file(
|
|
380
|
+
self.full_directory, f"{self.stem_glob}-mapproj_match_offsets.txt"
|
|
381
|
+
)
|
|
375
382
|
if path is None:
|
|
376
383
|
raise ValueError("\n\nMapProj Residuals TXT file not found.\n\n")
|
|
377
384
|
|
|
@@ -411,7 +418,9 @@ class ReadBundleAdjustFiles:
|
|
|
411
418
|
the bundle adjustment residuals propagate to uncertainty in
|
|
412
419
|
triangulation. This is an estimate of the final DEM error.
|
|
413
420
|
"""
|
|
414
|
-
path = glob_file(
|
|
421
|
+
path = glob_file(
|
|
422
|
+
self.full_directory, f"{self.stem_glob}-triangulation_uncertainty.txt"
|
|
423
|
+
)
|
|
415
424
|
if path is None:
|
|
416
425
|
raise ValueError("\n\nTriangulation Uncertainty TXT file not found.\n\n")
|
|
417
426
|
|
|
@@ -16,7 +16,10 @@ def _reconstruct_command():
|
|
|
16
16
|
for param in click_ctx.command.params:
|
|
17
17
|
val = click_ctx.params.get(param.name)
|
|
18
18
|
if val is not None and val != param.default:
|
|
19
|
-
|
|
19
|
+
if param.is_bool_flag:
|
|
20
|
+
cmd_parts.append(param.opts[0])
|
|
21
|
+
else:
|
|
22
|
+
cmd_parts.append(f"{param.opts[0]} {shlex.quote(str(val))}")
|
|
20
23
|
return " ".join(cmd_parts)
|
|
21
24
|
|
|
22
25
|
|
|
@@ -28,123 +31,116 @@ def _reconstruct_command():
|
|
|
28
31
|
help="Required directory of ASP processing with scenes and sub-directories for stereo and optionally bundle adjustment. Default: current directory.",
|
|
29
32
|
)
|
|
30
33
|
@click.option(
|
|
31
|
-
"--
|
|
34
|
+
"--bundle-adjust-prefix",
|
|
32
35
|
prompt=False,
|
|
33
36
|
default=None,
|
|
34
|
-
help="Optional directory
|
|
37
|
+
help="Optional bundle adjustment output location, relative to --directory. Accepts either a directory (e.g. 'ba') or an ASP-style output prefix as passed to --bundle-adjust-prefix in stereo/mapproject (e.g. 'ba/run'). If expected *residuals_pointmap.csv files are not found there, no bundle adjustment plots will be generated. Default: None.",
|
|
35
38
|
)
|
|
36
39
|
@click.option(
|
|
37
|
-
"--
|
|
40
|
+
"--stereo-directory",
|
|
38
41
|
prompt=True,
|
|
39
42
|
default="stereo",
|
|
40
43
|
help="Required directory of stereo files. Default: stereo.",
|
|
41
44
|
)
|
|
42
45
|
@click.option(
|
|
43
|
-
"--
|
|
46
|
+
"--dem-filename",
|
|
44
47
|
prompt=False,
|
|
45
48
|
default=None,
|
|
46
49
|
help="Optional DEM filename in the stereo directory. Default: None, which will search for the *-DEM.tif file in the stereo directory. Specify it as the basename with extension, e.g. my-custom-dem-name.tif.",
|
|
47
50
|
)
|
|
48
51
|
@click.option(
|
|
49
|
-
"--
|
|
52
|
+
"--dem-gsd",
|
|
50
53
|
prompt=False,
|
|
51
54
|
default=None,
|
|
52
55
|
help="Optional ground sample distance of the DEM. Default: None, which will search for the *-DEM.tif file in the stereo directory. If there is a GSD in the name of the file, specify it here as a float or integer, e.g. 1, 1.5, etc.",
|
|
53
56
|
)
|
|
54
57
|
@click.option(
|
|
55
|
-
"--
|
|
58
|
+
"--map-crs",
|
|
56
59
|
prompt=False,
|
|
57
60
|
default=None,
|
|
58
61
|
help="Projection for altimetry and bundle adjustment plots. As EPSG:XXXX. Default: None, which will use the projection of the ASP DEM, and fall back on EPSG:4326 if not found.",
|
|
59
62
|
)
|
|
60
63
|
@click.option(
|
|
61
|
-
"--
|
|
64
|
+
"--reference-dem",
|
|
62
65
|
prompt=False,
|
|
63
66
|
default=None,
|
|
64
67
|
help="Optional reference DEM used in ASP processing. No default. If not supplied, the logs will be examined to find it. If not found, no difference plots will be generated.",
|
|
65
68
|
)
|
|
66
69
|
@click.option(
|
|
67
|
-
"--
|
|
68
|
-
|
|
69
|
-
default=
|
|
70
|
-
help="
|
|
71
|
-
)
|
|
72
|
-
@click.option(
|
|
73
|
-
"--plot_altimetry",
|
|
74
|
-
prompt=False,
|
|
75
|
-
default=True,
|
|
76
|
-
help="If True, plot altimetry comparisons (ICESat-2 for Earth, LOLA for Moon, MOLA for Mars). For planetary DEMs, requires --altimetry_csv. Default: True.",
|
|
70
|
+
"--no-basemap",
|
|
71
|
+
is_flag=True,
|
|
72
|
+
default=False,
|
|
73
|
+
help="Skip the figure basemaps (basemaps are added by default, which requires an internet connection).",
|
|
77
74
|
)
|
|
78
75
|
@click.option(
|
|
79
|
-
"--
|
|
80
|
-
|
|
81
|
-
default=
|
|
82
|
-
help="
|
|
76
|
+
"--no-altimetry",
|
|
77
|
+
is_flag=True,
|
|
78
|
+
default=False,
|
|
79
|
+
help="Skip the altimetry comparisons (plotted by default: ICESat-2 for Earth, LOLA for Moon, MOLA for Mars; planetary DEMs require --altimetry-csv).",
|
|
83
80
|
)
|
|
84
81
|
@click.option(
|
|
85
|
-
"--
|
|
82
|
+
"--altimetry-csv",
|
|
86
83
|
prompt=False,
|
|
87
84
|
default=None,
|
|
88
85
|
type=click.Path(exists=True),
|
|
89
86
|
help="Path to a LOLA/MOLA *_topo_csv.csv file from the ODE GDS API. Required for planetary altimetry plots. Obtain via: request_planetary_altimetry --dem <dem> --email <email>, then download and unzip the result.",
|
|
90
87
|
)
|
|
91
88
|
@click.option(
|
|
92
|
-
"--
|
|
93
|
-
|
|
94
|
-
default=
|
|
95
|
-
help="
|
|
89
|
+
"--no-pc-align",
|
|
90
|
+
is_flag=True,
|
|
91
|
+
default=False,
|
|
92
|
+
help="Skip the pc_align step (run by default against the reference altimetry -- ICESat-2 for Earth, MOLA for Mars, LOLA for Moon -- appending the alignment-report pages; skipped automatically with --no-altimetry).",
|
|
96
93
|
)
|
|
97
94
|
@click.option(
|
|
98
|
-
"--
|
|
99
|
-
|
|
100
|
-
default=
|
|
101
|
-
help="
|
|
95
|
+
"--no-geometry",
|
|
96
|
+
is_flag=True,
|
|
97
|
+
default=False,
|
|
98
|
+
help="Skip the stereo geometry plots (plotted by default).",
|
|
102
99
|
)
|
|
103
100
|
@click.option(
|
|
104
|
-
"--
|
|
101
|
+
"--subset-km",
|
|
105
102
|
prompt=False,
|
|
106
103
|
default=1.0,
|
|
107
104
|
help="Size in km of the subset to plot for the detailed hillshade. Default: 1 km.",
|
|
108
105
|
)
|
|
109
106
|
@click.option(
|
|
110
|
-
"--
|
|
107
|
+
"--atl06sr-time-range",
|
|
111
108
|
prompt=False,
|
|
112
109
|
default="all",
|
|
113
110
|
help='Time range for ICESat-2 ATL06-SR data requests. "all" for all available data (mission start to present), or "START,END" for a custom range (e.g. "2020-01-01,2024-12-31"), or "auto" for scene metadata +/- 1 year. Default: all.',
|
|
114
111
|
)
|
|
115
112
|
@click.option(
|
|
116
|
-
"--
|
|
113
|
+
"--reuse-selections",
|
|
117
114
|
prompt=False,
|
|
118
115
|
default=None,
|
|
119
116
|
type=click.Path(exists=True),
|
|
120
117
|
help="Path to a *_figure_selections.yml written by a previous run. When supplied, replays that run's ICESat-2 points (parquet), profile track, best/worst segments, and detailed-hillshade clip boxes so figures are directly comparable across re-processing runs. Default: None.",
|
|
121
118
|
)
|
|
122
119
|
@click.option(
|
|
123
|
-
"--
|
|
120
|
+
"--report-filename",
|
|
124
121
|
prompt=False,
|
|
125
122
|
default=None,
|
|
126
123
|
help="PDF report filename or path. A bare filename (e.g. 'report.pdf') is saved in the stereo directory. A path (e.g. 'reports/report.pdf' or '/tmp/report.pdf') is used as-is. Default: auto-generated from directory name.",
|
|
127
124
|
)
|
|
128
125
|
@click.option(
|
|
129
|
-
"--
|
|
126
|
+
"--report-title",
|
|
130
127
|
prompt=False,
|
|
131
128
|
default=None,
|
|
132
129
|
help="Title for the report. Default: Directory name of ASP processing.",
|
|
133
130
|
)
|
|
134
131
|
def main(
|
|
135
132
|
directory,
|
|
136
|
-
|
|
133
|
+
bundle_adjust_prefix,
|
|
137
134
|
stereo_directory,
|
|
138
135
|
dem_filename,
|
|
139
136
|
dem_gsd,
|
|
140
137
|
map_crs,
|
|
141
138
|
reference_dem,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
plot_icesat,
|
|
139
|
+
no_basemap,
|
|
140
|
+
no_altimetry,
|
|
145
141
|
altimetry_csv,
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
no_pc_align,
|
|
143
|
+
no_geometry,
|
|
148
144
|
subset_km,
|
|
149
145
|
atl06sr_time_range,
|
|
150
146
|
reuse_selections,
|
|
@@ -160,18 +156,17 @@ def main(
|
|
|
160
156
|
"""
|
|
161
157
|
config = ReportConfig(
|
|
162
158
|
directory=directory,
|
|
163
|
-
|
|
159
|
+
bundle_adjust_prefix=bundle_adjust_prefix,
|
|
164
160
|
stereo_directory=stereo_directory,
|
|
165
161
|
dem_filename=dem_filename,
|
|
166
162
|
dem_gsd=dem_gsd,
|
|
167
163
|
map_crs=map_crs,
|
|
168
164
|
reference_dem=reference_dem,
|
|
169
|
-
add_basemap=
|
|
170
|
-
plot_altimetry=
|
|
171
|
-
plot_icesat=plot_icesat,
|
|
165
|
+
add_basemap=not no_basemap,
|
|
166
|
+
plot_altimetry=not no_altimetry,
|
|
172
167
|
altimetry_csv=altimetry_csv,
|
|
173
|
-
pc_align=
|
|
174
|
-
plot_geometry=
|
|
168
|
+
pc_align=not no_pc_align,
|
|
169
|
+
plot_geometry=not no_geometry,
|
|
175
170
|
subset_km=subset_km,
|
|
176
171
|
atl06sr_time_range=atl06sr_time_range,
|
|
177
172
|
reuse_selections=reuse_selections,
|
|
@@ -6,19 +6,19 @@ from asp_plot.csm_camera import csm_camera_summary_plot
|
|
|
6
6
|
|
|
7
7
|
@click.command()
|
|
8
8
|
@click.option(
|
|
9
|
-
"--
|
|
9
|
+
"--original-cameras",
|
|
10
10
|
prompt=True,
|
|
11
11
|
default="",
|
|
12
12
|
help="Original camera files, supplied as comma separated list 'path/to/original_camera_1,path/to/original_camera_2'. No default. Must be supplied.",
|
|
13
13
|
)
|
|
14
14
|
@click.option(
|
|
15
|
-
"--
|
|
15
|
+
"--optimized-cameras",
|
|
16
16
|
prompt=True,
|
|
17
17
|
default="",
|
|
18
18
|
help="Optimized camera files, supplied as comma separated list 'path/to/optimized_camera_1,path/to/optimized_camera_2'. No default. Must be supplied.",
|
|
19
19
|
)
|
|
20
20
|
@click.option(
|
|
21
|
-
"--
|
|
21
|
+
"--map-crs",
|
|
22
22
|
prompt=False,
|
|
23
23
|
default=None,
|
|
24
24
|
help="UTM EPSG code for map projection. As EPSG:XXXX. If not supplied, the map will be plotted in original camera coordinates of EPSG:4978 (ECEF).",
|
|
@@ -30,73 +30,73 @@ from asp_plot.csm_camera import csm_camera_summary_plot
|
|
|
30
30
|
help="Optional short title to append to figure output. Default: None.",
|
|
31
31
|
)
|
|
32
32
|
@click.option(
|
|
33
|
-
"--trim",
|
|
34
|
-
|
|
35
|
-
default=
|
|
36
|
-
help="
|
|
33
|
+
"--no-trim",
|
|
34
|
+
is_flag=True,
|
|
35
|
+
default=False,
|
|
36
|
+
help="Do not trim the plotted positions to the first and last camera image lines (trimmed by default).",
|
|
37
37
|
)
|
|
38
38
|
@click.option(
|
|
39
|
-
"--
|
|
40
|
-
|
|
39
|
+
"--shared-scales",
|
|
40
|
+
is_flag=True,
|
|
41
41
|
default=False,
|
|
42
|
-
help="
|
|
42
|
+
help="Share the position and angle difference scales between the cameras.",
|
|
43
43
|
)
|
|
44
44
|
@click.option(
|
|
45
|
-
"--
|
|
46
|
-
|
|
45
|
+
"--log-scale-positions",
|
|
46
|
+
is_flag=True,
|
|
47
47
|
default=False,
|
|
48
|
-
help="
|
|
48
|
+
help="Log-scale the position difference plots.",
|
|
49
49
|
)
|
|
50
50
|
@click.option(
|
|
51
|
-
"--
|
|
52
|
-
|
|
51
|
+
"--log-scale-angles",
|
|
52
|
+
is_flag=True,
|
|
53
53
|
default=False,
|
|
54
|
-
help="
|
|
54
|
+
help="Log-scale the angle difference plots.",
|
|
55
55
|
)
|
|
56
56
|
@click.option(
|
|
57
|
-
"--
|
|
57
|
+
"--upper-magnitude-percentile",
|
|
58
58
|
prompt=False,
|
|
59
59
|
default=95,
|
|
60
60
|
help="Percentile to use for the upper limit of the mapview colorbars. Default: 95.",
|
|
61
61
|
)
|
|
62
62
|
@click.option(
|
|
63
|
-
"--
|
|
63
|
+
"--figure-size",
|
|
64
64
|
prompt=False,
|
|
65
65
|
default="20,15",
|
|
66
66
|
help="Figure size as width,height. Default: 20,15.",
|
|
67
67
|
callback=lambda ctx, param, value: tuple(map(int, value.split(","))),
|
|
68
68
|
)
|
|
69
69
|
@click.option(
|
|
70
|
-
"--
|
|
70
|
+
"--output-directory",
|
|
71
71
|
prompt=False,
|
|
72
72
|
default=None,
|
|
73
73
|
help="Directory to save the figure. Default: None, which does not save the figure.",
|
|
74
74
|
)
|
|
75
75
|
@click.option(
|
|
76
|
-
"--
|
|
76
|
+
"--output-filename",
|
|
77
77
|
prompt=False,
|
|
78
78
|
default="csm_camera_summary_plot.png",
|
|
79
79
|
help="Figure filename. Default: csm_camera_summary_plot.png.",
|
|
80
80
|
)
|
|
81
81
|
@click.option(
|
|
82
|
-
"--
|
|
83
|
-
|
|
82
|
+
"--add-basemap",
|
|
83
|
+
is_flag=True,
|
|
84
84
|
default=False,
|
|
85
|
-
help="
|
|
85
|
+
help="Add a contextily basemap to the figure, which requires an internet connection.",
|
|
86
86
|
)
|
|
87
87
|
def main(
|
|
88
88
|
original_cameras,
|
|
89
89
|
optimized_cameras,
|
|
90
90
|
map_crs,
|
|
91
91
|
title,
|
|
92
|
-
|
|
92
|
+
no_trim,
|
|
93
93
|
shared_scales,
|
|
94
94
|
log_scale_positions,
|
|
95
95
|
log_scale_angles,
|
|
96
96
|
upper_magnitude_percentile,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
figure_size,
|
|
98
|
+
output_directory,
|
|
99
|
+
output_filename,
|
|
100
100
|
add_basemap,
|
|
101
101
|
):
|
|
102
102
|
"""
|
|
@@ -127,14 +127,14 @@ def main(
|
|
|
127
127
|
cam2_list,
|
|
128
128
|
map_crs.split(":")[-1] if map_crs else None,
|
|
129
129
|
title=title,
|
|
130
|
-
trim=
|
|
130
|
+
trim=not no_trim,
|
|
131
131
|
shared_scales=shared_scales,
|
|
132
132
|
log_scale_positions=log_scale_positions,
|
|
133
133
|
log_scale_angles=log_scale_angles,
|
|
134
134
|
upper_magnitude_percentile=upper_magnitude_percentile,
|
|
135
|
-
figsize=
|
|
136
|
-
save_dir=
|
|
137
|
-
fig_fn=
|
|
135
|
+
figsize=figure_size,
|
|
136
|
+
save_dir=output_directory,
|
|
137
|
+
fig_fn=output_filename,
|
|
138
138
|
add_basemap=add_basemap,
|
|
139
139
|
**ctx_kwargs,
|
|
140
140
|
)
|
|
@@ -20,9 +20,10 @@ from asp_plot.gallery import GalleryPlotter
|
|
|
20
20
|
)
|
|
21
21
|
@click.argument("files", nargs=-1, type=click.Path(exists=True))
|
|
22
22
|
@click.option(
|
|
23
|
-
"--
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
"--no-hillshade",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
default=False,
|
|
26
|
+
help="Skip the gray hillshade underlay drawn beneath each DEM by default.",
|
|
26
27
|
)
|
|
27
28
|
@click.option(
|
|
28
29
|
"--cmap",
|
|
@@ -37,7 +38,7 @@ from asp_plot.gallery import GalleryPlotter
|
|
|
37
38
|
help="Downsample factor for reads, or 'auto' to size thumbnails automatically. Default: auto.",
|
|
38
39
|
)
|
|
39
40
|
@click.option(
|
|
40
|
-
"--
|
|
41
|
+
"--max-filesize-mb",
|
|
41
42
|
prompt=False,
|
|
42
43
|
default=10.0,
|
|
43
44
|
type=float,
|
|
@@ -50,13 +51,13 @@ from asp_plot.gallery import GalleryPlotter
|
|
|
50
51
|
help="Figure suptitle. Default: none.",
|
|
51
52
|
)
|
|
52
53
|
@click.option(
|
|
53
|
-
"--
|
|
54
|
+
"--output-directory",
|
|
54
55
|
prompt=False,
|
|
55
56
|
default=None,
|
|
56
57
|
help="Directory to save the output plot. Default: Input directory.",
|
|
57
58
|
)
|
|
58
59
|
@click.option(
|
|
59
|
-
"--
|
|
60
|
+
"--output-filename",
|
|
60
61
|
prompt=False,
|
|
61
62
|
default=None,
|
|
62
63
|
help="Filename for the output plot. Default: Directory name with _gallery.png suffix.",
|
|
@@ -65,7 +66,7 @@ def main(
|
|
|
65
66
|
directory,
|
|
66
67
|
pattern,
|
|
67
68
|
files,
|
|
68
|
-
|
|
69
|
+
no_hillshade,
|
|
69
70
|
cmap,
|
|
70
71
|
downsample,
|
|
71
72
|
max_filesize_mb,
|
|
@@ -110,7 +111,7 @@ def main(
|
|
|
110
111
|
os.makedirs(output_directory, exist_ok=True)
|
|
111
112
|
|
|
112
113
|
plotter.plot_gallery(
|
|
113
|
-
hillshade=
|
|
114
|
+
hillshade=not no_hillshade,
|
|
114
115
|
cmap=cmap,
|
|
115
116
|
max_filesize_mb=max_filesize_mb,
|
|
116
117
|
save_dir=output_directory,
|
|
@@ -36,13 +36,13 @@ def main(dem, email, channels):
|
|
|
36
36
|
1. Run this command with your DEM and email
|
|
37
37
|
2. Wait for the email notification (may take minutes to hours)
|
|
38
38
|
3. Download and unzip the result
|
|
39
|
-
4. Pass the *_topo_csv.csv to asp_plot via --
|
|
39
|
+
4. Pass the *_topo_csv.csv to asp_plot via --altimetry-csv
|
|
40
40
|
|
|
41
41
|
\b
|
|
42
42
|
Example:
|
|
43
43
|
request_planetary_altimetry --dem stereo/output-DEM.tif --email user@example.com
|
|
44
44
|
# ... wait for email, download and unzip ...
|
|
45
|
-
asp_report --directory ./ --
|
|
45
|
+
asp_report --directory ./ --altimetry-csv /path/to/*_topo_csv.csv
|
|
46
46
|
"""
|
|
47
47
|
from asp_plot.altimetry import GDS_BASE_URL, gds_query_async
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ def main(dem, email, channels):
|
|
|
53
53
|
click.echo(
|
|
54
54
|
"\nThis DEM is an Earth DEM. ICESat-2 altimetry is fetched "
|
|
55
55
|
"automatically by asp_plot — no separate request needed.\n"
|
|
56
|
-
"Run: asp_report --directory <dir>
|
|
56
|
+
"Run: asp_report --directory <dir> (altimetry plots are on by default)\n"
|
|
57
57
|
)
|
|
58
58
|
sys.exit(0)
|
|
59
59
|
|
|
@@ -124,7 +124,7 @@ def main(dem, email, channels):
|
|
|
124
124
|
click.echo(
|
|
125
125
|
"\nYou will receive an email when the data is ready. "
|
|
126
126
|
"Download and unzip the result, then pass the *_topo_csv.csv to asp_plot:\n"
|
|
127
|
-
" asp_report --directory <dir> --
|
|
127
|
+
" asp_report --directory <dir> --altimetry-csv <*_topo_csv.csv>\n"
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
@@ -17,19 +17,19 @@ from asp_plot.stereo_geometry import StereoGeometryPlotter
|
|
|
17
17
|
),
|
|
18
18
|
)
|
|
19
19
|
@click.option(
|
|
20
|
-
"--
|
|
21
|
-
|
|
22
|
-
default=
|
|
23
|
-
help="
|
|
20
|
+
"--no-basemap",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
default=False,
|
|
23
|
+
help="Skip the figure basemaps (basemaps are added by default, which requires an internet connection).",
|
|
24
24
|
)
|
|
25
25
|
@click.option(
|
|
26
|
-
"--
|
|
26
|
+
"--output-directory",
|
|
27
27
|
prompt=False,
|
|
28
28
|
default=None,
|
|
29
29
|
help="Directory to save the output plot. Default: Input directory.",
|
|
30
30
|
)
|
|
31
31
|
@click.option(
|
|
32
|
-
"--
|
|
32
|
+
"--output-filename",
|
|
33
33
|
prompt=False,
|
|
34
34
|
default=None,
|
|
35
35
|
help=(
|
|
@@ -41,7 +41,7 @@ from asp_plot.stereo_geometry import StereoGeometryPlotter
|
|
|
41
41
|
def main(
|
|
42
42
|
inputs,
|
|
43
43
|
directory,
|
|
44
|
-
|
|
44
|
+
no_basemap,
|
|
45
45
|
output_directory,
|
|
46
46
|
output_filename,
|
|
47
47
|
):
|
|
@@ -72,14 +72,14 @@ def main(
|
|
|
72
72
|
# Positional INPUTS take precedence; otherwise fall back to --directory.
|
|
73
73
|
if inputs:
|
|
74
74
|
inputs = [os.path.expanduser(i) for i in inputs]
|
|
75
|
-
plotter = StereoGeometryPlotter(inputs=inputs, add_basemap=
|
|
75
|
+
plotter = StereoGeometryPlotter(inputs=inputs, add_basemap=not no_basemap)
|
|
76
76
|
source_desc = " ".join(inputs)
|
|
77
77
|
# Base directory for default output paths (resolved by the parser).
|
|
78
78
|
base_directory = plotter.directory
|
|
79
79
|
else:
|
|
80
80
|
base_directory = os.path.expanduser(directory or "./")
|
|
81
81
|
plotter = StereoGeometryPlotter(
|
|
82
|
-
directory=base_directory, add_basemap=
|
|
82
|
+
directory=base_directory, add_basemap=not no_basemap
|
|
83
83
|
)
|
|
84
84
|
source_desc = base_directory
|
|
85
85
|
|
|
@@ -54,7 +54,11 @@ class ProcessingParameters:
|
|
|
54
54
|
"""
|
|
55
55
|
|
|
56
56
|
def __init__(
|
|
57
|
-
self,
|
|
57
|
+
self,
|
|
58
|
+
processing_directory,
|
|
59
|
+
bundle_adjust_directory=None,
|
|
60
|
+
stereo_directory=None,
|
|
61
|
+
bundle_adjust_stem=None,
|
|
58
62
|
):
|
|
59
63
|
"""
|
|
60
64
|
Initialize the ProcessingParameters object.
|
|
@@ -67,6 +71,10 @@ class ProcessingParameters:
|
|
|
67
71
|
Subdirectory containing bundle adjustment outputs, default is None
|
|
68
72
|
stereo_directory : str, optional
|
|
69
73
|
Subdirectory containing stereo outputs, default is None
|
|
74
|
+
bundle_adjust_stem : str, optional
|
|
75
|
+
Run stem of an ASP output prefix (the ``run`` in ``ba/run``).
|
|
76
|
+
When given, the bundle adjustment log glob is narrowed to that
|
|
77
|
+
run; otherwise any run in the directory matches.
|
|
70
78
|
|
|
71
79
|
Notes
|
|
72
80
|
-----
|
|
@@ -92,8 +100,13 @@ class ProcessingParameters:
|
|
|
92
100
|
# Locate log files. A missing directory (None) means that stage was not
|
|
93
101
|
# requested -- not an error -- so we guard on it rather than catching a
|
|
94
102
|
# blanket exception that would also hide real I/O problems.
|
|
103
|
+
ba_log_pattern = (
|
|
104
|
+
f"{bundle_adjust_stem}-log-bundle_adjust*.txt"
|
|
105
|
+
if bundle_adjust_stem
|
|
106
|
+
else "*log-bundle_adjust*.txt"
|
|
107
|
+
)
|
|
95
108
|
self.bundle_adjust_log = (
|
|
96
|
-
glob_file(self.full_ba_directory,
|
|
109
|
+
glob_file(self.full_ba_directory, ba_log_pattern)
|
|
97
110
|
if self.full_ba_directory
|
|
98
111
|
else None
|
|
99
112
|
)
|
|
@@ -41,7 +41,12 @@ from asp_plot.selections import (
|
|
|
41
41
|
)
|
|
42
42
|
from asp_plot.stereo import StereoPlotter
|
|
43
43
|
from asp_plot.stereo_geometry import StereoGeometryPlotter, camera_files_from_stereo_run
|
|
44
|
-
from asp_plot.utils import
|
|
44
|
+
from asp_plot.utils import (
|
|
45
|
+
Raster,
|
|
46
|
+
detect_planetary_body,
|
|
47
|
+
get_acquisition_dates,
|
|
48
|
+
resolve_directory_or_prefix,
|
|
49
|
+
)
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
@dataclass
|
|
@@ -53,7 +58,7 @@ class ReportConfig:
|
|
|
53
58
|
"""
|
|
54
59
|
|
|
55
60
|
directory: str = "./"
|
|
56
|
-
|
|
61
|
+
bundle_adjust_prefix: Optional[str] = None
|
|
57
62
|
stereo_directory: str = "stereo"
|
|
58
63
|
dem_filename: Optional[str] = None
|
|
59
64
|
dem_gsd: Optional[float] = None
|
|
@@ -61,7 +66,6 @@ class ReportConfig:
|
|
|
61
66
|
reference_dem: Optional[str] = None
|
|
62
67
|
add_basemap: bool = True
|
|
63
68
|
plot_altimetry: bool = True
|
|
64
|
-
plot_icesat: Optional[object] = None
|
|
65
69
|
altimetry_csv: Optional[str] = None
|
|
66
70
|
pc_align: bool = True
|
|
67
71
|
plot_geometry: bool = True
|
|
@@ -74,6 +78,15 @@ class ReportConfig:
|
|
|
74
78
|
# CLI builds this from the Click context; non-CLI callers may leave it None.
|
|
75
79
|
report_command: Optional[str] = None
|
|
76
80
|
|
|
81
|
+
def __post_init__(self):
|
|
82
|
+
# Tolerate trailing slashes on directory-like inputs (issue #60) so
|
|
83
|
+
# path joins and basename-derived titles behave the same either way.
|
|
84
|
+
for attr in ("directory", "stereo_directory", "bundle_adjust_prefix"):
|
|
85
|
+
value = getattr(self, attr)
|
|
86
|
+
if value:
|
|
87
|
+
stripped = value.rstrip("/\\")
|
|
88
|
+
setattr(self, attr, stripped or value)
|
|
89
|
+
|
|
77
90
|
|
|
78
91
|
@dataclass
|
|
79
92
|
class ReportContext:
|
|
@@ -93,6 +106,9 @@ class ReportContext:
|
|
|
93
106
|
stereo_plotter: StereoPlotter
|
|
94
107
|
asp_dem: Optional[str]
|
|
95
108
|
plot_altimetry: bool
|
|
109
|
+
# --bundle-adjust-prefix resolved into a subdirectory and optional run stem
|
|
110
|
+
ba_directory: Optional[str] = None
|
|
111
|
+
ba_stem: Optional[str] = None
|
|
96
112
|
report_metadata: Optional[ReportMetadata] = None
|
|
97
113
|
reuse_clip_windows: Optional[list] = None
|
|
98
114
|
reuse_clip_windows_crs: Optional[str] = None
|
|
@@ -204,7 +220,9 @@ def _build_bundle_adjust(ctx: ReportContext) -> List[object]:
|
|
|
204
220
|
cfg = ctx.config
|
|
205
221
|
sections: List[object] = []
|
|
206
222
|
try:
|
|
207
|
-
ba_files = ReadBundleAdjustFiles(
|
|
223
|
+
ba_files = ReadBundleAdjustFiles(
|
|
224
|
+
cfg.directory, ctx.ba_directory, stem=ctx.ba_stem
|
|
225
|
+
)
|
|
208
226
|
resid_initial_gdf, resid_final_gdf = ba_files.get_initial_final_residuals_gdfs()
|
|
209
227
|
|
|
210
228
|
plotter = PlotBundleAdjustFiles(
|
|
@@ -316,7 +334,7 @@ def _build_bundle_adjust(ctx: ReportContext) -> List[object]:
|
|
|
316
334
|
|
|
317
335
|
except ValueError:
|
|
318
336
|
print(
|
|
319
|
-
f"\n\nNo bundle adjustment files found in directory {os.path.join(cfg.directory,
|
|
337
|
+
f"\n\nNo bundle adjustment files found in directory {os.path.join(cfg.directory, ctx.ba_directory):}. If you want bundle adjustment plots, make sure you run the tool and supply the correct --bundle-adjust-prefix to asp_report.\n\n"
|
|
320
338
|
)
|
|
321
339
|
return sections
|
|
322
340
|
|
|
@@ -400,7 +418,7 @@ def _build_altimetry_earth(ctx: ReportContext, ctx_kwargs_altimetry: dict):
|
|
|
400
418
|
asp_dem = ctx.asp_dem
|
|
401
419
|
sections: List[object] = []
|
|
402
420
|
|
|
403
|
-
# Parse --
|
|
421
|
+
# Parse --atl06sr-time-range into time_range/t0/t1 kwargs
|
|
404
422
|
atl06sr_time_range = cfg.atl06sr_time_range
|
|
405
423
|
atl06sr_time_kwargs = {}
|
|
406
424
|
if atl06sr_time_range.lower() == "all":
|
|
@@ -632,7 +650,7 @@ def _build_altimetry_planetary(ctx: ReportContext, body: str):
|
|
|
632
650
|
f" 1. Run: request_planetary_altimetry --dem {asp_dem} --email <your_email>\n"
|
|
633
651
|
f" 2. Wait for the email with a download link\n"
|
|
634
652
|
f" 3. Download and unzip the result\n"
|
|
635
|
-
f" 4. Re-run asp_report with: --
|
|
653
|
+
f" 4. Re-run asp_report with: --altimetry-csv <path_to_pts_csv.csv>\n"
|
|
636
654
|
f"\nSkipping {instrument} altimetry plots.\n"
|
|
637
655
|
f"{'='*60}\n"
|
|
638
656
|
)
|
|
@@ -751,7 +769,7 @@ REPORT_SECTIONS: List[ReportSpec] = [
|
|
|
751
769
|
ReportSpec("match_points", lambda ctx: True, _build_match_points),
|
|
752
770
|
ReportSpec(
|
|
753
771
|
"bundle_adjust",
|
|
754
|
-
lambda ctx: bool(ctx.config.
|
|
772
|
+
lambda ctx: bool(ctx.config.bundle_adjust_prefix),
|
|
755
773
|
_build_bundle_adjust,
|
|
756
774
|
),
|
|
757
775
|
ReportSpec("disparity", lambda ctx: True, _build_disparity),
|
|
@@ -766,29 +784,13 @@ REPORT_SECTIONS: List[ReportSpec] = [
|
|
|
766
784
|
# ---------------------------------------------------------------------------
|
|
767
785
|
|
|
768
786
|
|
|
769
|
-
def _resolve_plot_altimetry(config: ReportConfig) -> bool:
|
|
770
|
-
"""Resolve the deprecated --plot_icesat alias against --plot_altimetry."""
|
|
771
|
-
plot_altimetry = config.plot_altimetry
|
|
772
|
-
if config.plot_icesat is not None:
|
|
773
|
-
import warnings
|
|
774
|
-
|
|
775
|
-
warnings.warn(
|
|
776
|
-
"--plot_icesat is deprecated. Use --plot_altimetry instead.",
|
|
777
|
-
DeprecationWarning,
|
|
778
|
-
stacklevel=1,
|
|
779
|
-
)
|
|
780
|
-
plot_icesat = config.plot_icesat
|
|
781
|
-
# Convert Click string 'True'/'False' to bool
|
|
782
|
-
if isinstance(plot_icesat, str):
|
|
783
|
-
plot_icesat = plot_icesat.lower() not in ("false", "0", "no")
|
|
784
|
-
plot_altimetry = plot_icesat
|
|
785
|
-
return plot_altimetry
|
|
786
|
-
|
|
787
|
-
|
|
788
787
|
def _setup_context(config: ReportConfig) -> ReportContext:
|
|
789
788
|
"""Resolve paths, projection, DEM metadata, and shared plotters."""
|
|
790
789
|
directory = os.path.expanduser(config.directory)
|
|
791
790
|
config.directory = directory
|
|
791
|
+
ba_directory, ba_stem = resolve_directory_or_prefix(
|
|
792
|
+
directory, config.bundle_adjust_prefix
|
|
793
|
+
)
|
|
792
794
|
if config.reference_dem:
|
|
793
795
|
config.reference_dem = os.path.expanduser(config.reference_dem)
|
|
794
796
|
if config.altimetry_csv:
|
|
@@ -858,7 +860,7 @@ def _setup_context(config: ReportConfig) -> ReportContext:
|
|
|
858
860
|
print(f"\nUsing map projection from DEM: {map_crs}\n")
|
|
859
861
|
except Exception as e:
|
|
860
862
|
print(
|
|
861
|
-
f"\nError getting projection from DEM: {e}. Using default projection EPSG:4326. If you want a different projection, use the --
|
|
863
|
+
f"\nError getting projection from DEM: {e}. Using default projection EPSG:4326. If you want a different projection, use the --map-crs flag.\n"
|
|
862
864
|
)
|
|
863
865
|
map_crs = "EPSG:4326"
|
|
864
866
|
|
|
@@ -875,10 +877,8 @@ def _setup_context(config: ReportConfig) -> ReportContext:
|
|
|
875
877
|
(float(valid.min()), float(valid.max())) if valid.size else (0, 0)
|
|
876
878
|
)
|
|
877
879
|
acq_extra_dirs = [os.path.join(directory, config.stereo_directory)]
|
|
878
|
-
if
|
|
879
|
-
acq_extra_dirs.append(
|
|
880
|
-
os.path.join(directory, config.bundle_adjust_directory)
|
|
881
|
-
)
|
|
880
|
+
if ba_directory:
|
|
881
|
+
acq_extra_dirs.append(os.path.join(directory, ba_directory))
|
|
882
882
|
acquisition_dates = get_acquisition_dates(
|
|
883
883
|
directory, extra_dirs=acq_extra_dirs
|
|
884
884
|
)
|
|
@@ -915,7 +915,9 @@ def _setup_context(config: ReportConfig) -> ReportContext:
|
|
|
915
915
|
ctx_kwargs=ctx_kwargs,
|
|
916
916
|
stereo_plotter=stereo_plotter,
|
|
917
917
|
asp_dem=asp_dem,
|
|
918
|
-
plot_altimetry=
|
|
918
|
+
plot_altimetry=config.plot_altimetry,
|
|
919
|
+
ba_directory=ba_directory,
|
|
920
|
+
ba_stem=ba_stem,
|
|
919
921
|
report_metadata=report_metadata,
|
|
920
922
|
reuse_clip_windows=reuse_clip_windows,
|
|
921
923
|
reuse_clip_windows_crs=reuse_clip_windows_crs,
|
|
@@ -983,7 +985,8 @@ def run_report(config: ReportConfig) -> str:
|
|
|
983
985
|
# Compile report
|
|
984
986
|
processing_parameters = ProcessingParameters(
|
|
985
987
|
processing_directory=config.directory,
|
|
986
|
-
bundle_adjust_directory=
|
|
988
|
+
bundle_adjust_directory=ctx.ba_directory,
|
|
989
|
+
bundle_adjust_stem=ctx.ba_stem,
|
|
987
990
|
stereo_directory=config.stereo_directory,
|
|
988
991
|
)
|
|
989
992
|
processing_parameters_dict = processing_parameters.from_log_files()
|
|
@@ -84,6 +84,42 @@ def glob_file(directory, *patterns, all_files=False, recursive=False, quiet=Fals
|
|
|
84
84
|
return None
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def resolve_directory_or_prefix(base_directory, value):
|
|
88
|
+
"""
|
|
89
|
+
Resolve a CLI value that may name a directory or an ASP output prefix.
|
|
90
|
+
|
|
91
|
+
ASP tools take output *prefixes* (``--bundle-adjust-prefix ba/run``), while
|
|
92
|
+
asp_plot historically took the containing *directory* (``ba``). This
|
|
93
|
+
accepts both: an existing directory is returned as-is with no stem, while
|
|
94
|
+
a prefix is split into its directory and run stem so file globs can be
|
|
95
|
+
narrowed to that run (``run-final_residuals_pointmap.csv``).
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
base_directory : str
|
|
100
|
+
Directory the value is relative to (the report's ``--directory``).
|
|
101
|
+
value : str or None
|
|
102
|
+
Directory or prefix, relative to ``base_directory``. Trailing slashes
|
|
103
|
+
are stripped.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
tuple of (str or None, str or None)
|
|
108
|
+
``(subdirectory, stem)``; ``stem`` is None when ``value`` names a
|
|
109
|
+
directory. A separator-free value that is not an existing directory is
|
|
110
|
+
kept as a directory so downstream "not found" errors point at it.
|
|
111
|
+
"""
|
|
112
|
+
if value is None:
|
|
113
|
+
return None, None
|
|
114
|
+
value = value.rstrip("/\\")
|
|
115
|
+
if os.path.isdir(os.path.join(os.path.expanduser(base_directory), value)):
|
|
116
|
+
return value, None
|
|
117
|
+
subdirectory, stem = os.path.split(value)
|
|
118
|
+
if subdirectory and stem:
|
|
119
|
+
return subdirectory, stem
|
|
120
|
+
return value, None
|
|
121
|
+
|
|
122
|
+
|
|
87
123
|
def find_pair_directories(directory):
|
|
88
124
|
"""
|
|
89
125
|
Find the per-pair subdirectories of an ASP multi-view stereo run.
|
|
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
|