asp-plot 1.6.4__tar.gz → 1.7.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.
Files changed (29) hide show
  1. {asp_plot-1.6.4 → asp_plot-1.7.0}/.gitignore +1 -0
  2. {asp_plot-1.6.4 → asp_plot-1.7.0}/CHANGELOG.md +18 -0
  3. {asp_plot-1.6.4 → asp_plot-1.7.0}/PKG-INFO +4 -3
  4. {asp_plot-1.6.4 → asp_plot-1.7.0}/README.md +3 -2
  5. asp_plot-1.7.0/asp_plot/__init__.py +6 -0
  6. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/processing_parameters.py +32 -0
  7. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/report.py +21 -0
  8. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/scenes.py +14 -1
  9. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereo.py +24 -9
  10. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/utils.py +118 -47
  11. {asp_plot-1.6.4 → asp_plot-1.7.0}/pyproject.toml +1 -1
  12. asp_plot-1.6.4/asp_plot/cli/__init__.py +0 -0
  13. {asp_plot-1.6.4 → asp_plot-1.7.0}/.flake8 +0 -0
  14. {asp_plot-1.6.4 → asp_plot-1.7.0}/.github/workflows/release.yml +0 -0
  15. {asp_plot-1.6.4 → asp_plot-1.7.0}/.github/workflows/run-tests.yml +0 -0
  16. {asp_plot-1.6.4 → asp_plot-1.7.0}/.pre-commit-config.yaml +0 -0
  17. {asp_plot-1.6.4 → asp_plot-1.7.0}/LICENSE +0 -0
  18. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/alignment.py +0 -0
  19. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/altimetry.py +0 -0
  20. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/bundle_adjust.py +0 -0
  21. {asp_plot-1.6.4/asp_plot → asp_plot-1.7.0/asp_plot/cli}/__init__.py +0 -0
  22. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/asp_plot.py +0 -0
  23. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/csm_camera_plot.py +0 -0
  24. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/cli/stereo_geom.py +0 -0
  25. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/csm_camera.py +0 -0
  26. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereo_geometry.py +0 -0
  27. {asp_plot-1.6.4 → asp_plot-1.7.0}/asp_plot/stereopair_metadata_parser.py +0 -0
  28. {asp_plot-1.6.4 → asp_plot-1.7.0}/conda-forge-recipe/meta.yaml +0 -0
  29. {asp_plot-1.6.4 → asp_plot-1.7.0}/environment.yml +0 -0
@@ -136,3 +136,4 @@ scratch/
136
136
  notebooks/**/*.parquet
137
137
  notebooks/**/*.csv
138
138
  CLAUDE*
139
+ .claude/**
@@ -5,6 +5,24 @@ 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
+ ## [1.7.0] - 2026-02-24
9
+
10
+ ### Added
11
+ - ASP version and asp_plot version displayed on report title page
12
+ - Copyright overlay ("© Vantor {year}") on WorldView satellite imagery in scene, match point, and detailed hillshade plots
13
+ - `detect_vantor_satellite()` utility to identify WorldView imagery from XML SATID tags
14
+ - `add_copyright_overlay()` utility for matplotlib axes
15
+ - `ProcessingParameters.get_asp_version()` method to extract ASP version from log files
16
+ - `Raster._mask_nodata()` private helper to consolidate nodata/invalid value masking
17
+ - `Raster._load_and_diff_rasters_da()` private static method returning xarray DataArray for raster differencing
18
+
19
+ ### Changed
20
+ - `Raster.get_bounds()` now uses `self.ds.bounds` (rasterio) instead of opening a redundant rioxarray dataset
21
+ - `Raster.compute_difference()` uses `rio.to_raster()` for saving when `save=True`, avoiding manual profile construction
22
+ - `StereoPlotter.plot_detailed_hillshade()` reuses existing `raster.ds.transform` instead of reopening the DEM file
23
+ - Consolidated duplicated nodata masking logic into `Raster._mask_nodata()`
24
+ - Updated ASTER and WorldView example notebooks
25
+
8
26
  ## [1.6.4] - 2026-02-17
9
27
 
10
28
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asp_plot
3
- Version: 1.6.4
3
+ Version: 1.7.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: Issues, https://github.com/uw-cryo/asp_plot/issues
@@ -31,12 +31,13 @@ Description-Content-Type: text/markdown
31
31
 
32
32
  # asp_plot
33
33
 
34
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14263122.svg)](https://doi.org/10.5281/zenodo.14263122)
34
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14263121.svg)](https://doi.org/10.5281/zenodo.14263121)
35
35
 
36
36
  Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
37
37
 
38
38
  > [!IMPORTANT]
39
- > [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
39
+ > [View an example WorldView report here](https://github.com/uw-cryo/asp_plot/blob/main/notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
40
+ > [View example modular plotting usage here](https://github.com/uw-cryo/asp_plot/tree/main/notebooks)
40
41
  >
41
42
 
42
43
  ## Motivation
@@ -1,11 +1,12 @@
1
1
  # asp_plot
2
2
 
3
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14263122.svg)](https://doi.org/10.5281/zenodo.14263122)
3
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14263121.svg)](https://doi.org/10.5281/zenodo.14263121)
4
4
 
5
5
  Scripts and notebooks to visualize output from the [NASA Ames Stereo Pipeline (ASP)](https://github.com/NeoGeographyToolkit/StereoPipeline).
6
6
 
7
7
  > [!IMPORTANT]
8
- > [View an example WorldView report here](notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
8
+ > [View an example WorldView report here](https://github.com/uw-cryo/asp_plot/blob/main/notebooks/WorldView/asp_plot_report_atlanta_tile.pdf).
9
+ > [View example modular plotting usage here](https://github.com/uw-cryo/asp_plot/tree/main/notebooks)
9
10
  >
10
11
 
11
12
  ## Motivation
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("asp_plot")
5
+ except PackageNotFoundError:
6
+ __version__ = "unknown"
@@ -103,6 +103,37 @@ class ProcessingParameters:
103
103
  except:
104
104
  self.point2dem_log = None
105
105
 
106
+ def get_asp_version(self):
107
+ """Extract the ASP version string from the first available log file.
108
+
109
+ Returns
110
+ -------
111
+ str
112
+ ASP version string (e.g., "3.4.0-alpha"), or "N/A" if not found.
113
+ """
114
+ log_candidates = []
115
+ if self.bundle_adjust_log:
116
+ log_candidates.append(self.bundle_adjust_log)
117
+ if self.stereo_logs:
118
+ pprc = next(
119
+ (log for log in self.stereo_logs if "log-stereo_pprc" in log),
120
+ None,
121
+ )
122
+ if pprc:
123
+ log_candidates.append(pprc)
124
+ if self.point2dem_log:
125
+ log_candidates.append(self.point2dem_log)
126
+
127
+ for log_file in log_candidates:
128
+ try:
129
+ with open(log_file, "r") as f:
130
+ first_line = f.readline().strip()
131
+ if first_line.startswith("ASP "):
132
+ return first_line[4:]
133
+ except Exception:
134
+ continue
135
+ return "N/A"
136
+
106
137
  def from_log_files(self):
107
138
  """
108
139
  Extract processing parameters from log files.
@@ -157,6 +188,7 @@ class ProcessingParameters:
157
188
  point2dem_params = "point2dem " + point2dem_params.split(maxsplit=1)[1]
158
189
 
159
190
  self.processing_parameters_dict = {
191
+ "asp_version": self.get_asp_version(),
160
192
  "processing_timestamp": processing_timestamp,
161
193
  "reference_dem": reference_dem,
162
194
  "bundle_adjust": bundle_adjust_params,
@@ -152,6 +152,27 @@ def compile_report(
152
152
  new_x="LMARGIN",
153
153
  new_y="NEXT",
154
154
  )
155
+ asp_version = processing_parameters_dict.get("asp_version", "")
156
+ if asp_version:
157
+ pdf.cell(
158
+ 0,
159
+ 8,
160
+ f"ASP version: {asp_version}",
161
+ align="C",
162
+ new_x="LMARGIN",
163
+ new_y="NEXT",
164
+ )
165
+
166
+ from asp_plot import __version__
167
+
168
+ pdf.cell(
169
+ 0,
170
+ 8,
171
+ f"asp_plot version: {__version__}",
172
+ align="C",
173
+ new_x="LMARGIN",
174
+ new_y="NEXT",
175
+ )
155
176
  pdf.ln(10)
156
177
 
157
178
  if report_metadata is not None:
@@ -3,7 +3,14 @@ import os
3
3
 
4
4
  import matplotlib.pyplot as plt
5
5
 
6
- from asp_plot.utils import Plotter, Raster, glob_file, save_figure
6
+ from asp_plot.utils import (
7
+ Plotter,
8
+ Raster,
9
+ add_copyright_overlay,
10
+ detect_vantor_satellite,
11
+ glob_file,
12
+ save_figure,
13
+ )
7
14
 
8
15
  logging.basicConfig(level=logging.WARNING)
9
16
  logger = logging.getLogger(__name__)
@@ -64,6 +71,8 @@ class ScenePlotter(Plotter):
64
71
  self.stereo_directory = stereo_directory
65
72
  self.full_stereo_directory = os.path.join(directory, stereo_directory)
66
73
 
74
+ self.is_vantor = detect_vantor_satellite(self.directory)
75
+
67
76
  self.left_scene_sub_fn = glob_file(self.full_stereo_directory, "*-L_sub.tif")
68
77
  self.right_scene_sub_fn = glob_file(self.full_stereo_directory, "*-R_sub.tif")
69
78
 
@@ -110,10 +119,14 @@ class ScenePlotter(Plotter):
110
119
  left_scene_ma = left_scene.read_array()
111
120
  self.plot_array(ax=axa[0], array=left_scene_ma, cmap="gray", add_cbar=False)
112
121
  axa[0].set_title(f"Left\n{os.path.basename(self.left_scene_sub_fn)}", size=8)
122
+ if self.is_vantor:
123
+ add_copyright_overlay(axa[0])
113
124
 
114
125
  right_scene_ma = Raster(self.right_scene_sub_fn).read_array()
115
126
  self.plot_array(ax=axa[1], array=right_scene_ma, cmap="gray", add_cbar=False)
116
127
  axa[1].set_title(f"Right\n{os.path.basename(self.right_scene_sub_fn)}", size=8)
128
+ if self.is_vantor:
129
+ add_copyright_overlay(axa[1])
117
130
 
118
131
  fig.tight_layout()
119
132
  if save_dir and fig_fn:
@@ -9,7 +9,15 @@ import rasterio as rio
9
9
  from matplotlib_scalebar.scalebar import ScaleBar
10
10
 
11
11
  from asp_plot.processing_parameters import ProcessingParameters
12
- from asp_plot.utils import ColorBar, Plotter, Raster, glob_file, save_figure
12
+ from asp_plot.utils import (
13
+ ColorBar,
14
+ Plotter,
15
+ Raster,
16
+ add_copyright_overlay,
17
+ detect_vantor_satellite,
18
+ glob_file,
19
+ save_figure,
20
+ )
13
21
 
14
22
  logging.basicConfig(level=logging.WARNING)
15
23
  logger = logging.getLogger(__name__)
@@ -90,6 +98,7 @@ class StereoPlotter(Plotter):
90
98
  super().__init__(**kwargs)
91
99
  self.directory = directory
92
100
  self.stereo_directory = stereo_directory
101
+ self.is_vantor = detect_vantor_satellite(self.directory)
93
102
 
94
103
  if reference_dem:
95
104
  self.reference_dem = reference_dem
@@ -331,6 +340,10 @@ class StereoPlotter(Plotter):
331
340
  s=1,
332
341
  )
333
342
  axa[1].set_aspect("equal")
343
+
344
+ if self.is_vantor:
345
+ add_copyright_overlay(axa[0])
346
+ add_copyright_overlay(axa[1])
334
347
  else:
335
348
  axa[0].text(
336
349
  0.5,
@@ -647,14 +660,14 @@ class StereoPlotter(Plotter):
647
660
  ]
648
661
  self._plot_hillshade_with_overlay(ax_hs, dem_subset, hs_subset, gsd)
649
662
 
650
- with rio.open(self.dem_fn) as src:
651
- transform = src.transform
652
- ul_x, ul_y = rio.transform.xy(
653
- transform, idx[0] * subset_size, idx[1] * subset_size
654
- )
655
- lr_x, lr_y = rio.transform.xy(
656
- transform, (idx[0] + 1) * subset_size, (idx[1] + 1) * subset_size
657
- )
663
+ ul_x, ul_y = rio.transform.xy(
664
+ raster.ds.transform, idx[0] * subset_size, idx[1] * subset_size
665
+ )
666
+ lr_x, lr_y = rio.transform.xy(
667
+ raster.ds.transform,
668
+ (idx[0] + 1) * subset_size,
669
+ (idx[1] + 1) * subset_size,
670
+ )
658
671
 
659
672
  # We only show the corresponding image if it is mapprojected
660
673
  if self.orthos:
@@ -671,6 +684,8 @@ class StereoPlotter(Plotter):
671
684
  cmap="gray",
672
685
  add_cbar=False,
673
686
  )
687
+ if self.is_vantor:
688
+ add_copyright_overlay(ax_img)
674
689
  axes_to_modify = [ax_hs, ax_img]
675
690
  else:
676
691
  plt.delaxes(ax_img)
@@ -1,6 +1,7 @@
1
1
  import glob
2
2
  import logging
3
3
  import os
4
+ import re
4
5
  import subprocess
5
6
  import warnings
6
7
 
@@ -529,6 +530,27 @@ class Raster:
529
530
 
530
531
  return (height, width)
531
532
 
533
+ def _mask_nodata(self, array):
534
+ """
535
+ Apply nodata and invalid value masking to a raster array.
536
+
537
+ Handles two cases that rasterio's masked=True alone does not:
538
+ 1. Undeclared nodata values (inferred by get_ndv() from pixel data)
539
+ 2. Invalid values (inf, nan) that may exist in ASP outputs
540
+
541
+ Parameters
542
+ ----------
543
+ array : numpy.ndarray
544
+ Raw raster array (may already be partially masked from rasterio)
545
+
546
+ Returns
547
+ -------
548
+ numpy.ma.MaskedArray
549
+ Array with nodata and invalid values masked
550
+ """
551
+ ndv = self.get_ndv()
552
+ return np.ma.fix_invalid(np.ma.masked_equal(array, ndv))
553
+
532
554
  def read_array(self, b=1, extent=False):
533
555
  """
534
556
  Read raster data as a numpy masked array.
@@ -559,8 +581,7 @@ class Raster:
559
581
  else:
560
582
  a = self.ds.read(b, masked=True)
561
583
 
562
- ndv = self.get_ndv()
563
- ma = np.ma.fix_invalid(np.ma.masked_equal(a, ndv))
584
+ ma = self._mask_nodata(a)
564
585
  out = ma
565
586
  if extent:
566
587
  extent = rio.plot.plotting_extent(self.ds)
@@ -590,9 +611,7 @@ class Raster:
590
611
  """
591
612
  window = from_bounds(*bbox, self.ds.transform)
592
613
  subset = self.ds.read(b, window=window, masked=True)
593
- ndv = self.get_ndv()
594
- ma = np.ma.fix_invalid(np.ma.masked_equal(subset, ndv))
595
- return ma
614
+ return self._mask_nodata(subset)
596
615
 
597
616
  def get_ndv(self):
598
617
  """
@@ -672,8 +691,7 @@ class Raster:
672
691
  If json_format=True: list of corner coordinates as dictionaries
673
692
  If json_format=False: tuple of (min_x, min_y, max_x, max_y)
674
693
  """
675
- ds = rioxarray.open_rasterio(self.fn, masked=True).squeeze()
676
- bounds = ds.rio.bounds()
694
+ bounds = self.ds.bounds
677
695
  if latlon:
678
696
  epsg = self.get_epsg_code()
679
697
  bounds = rio.warp.transform_bounds(f"EPSG:{epsg}", "EPSG:4326", *bounds)
@@ -736,13 +754,9 @@ class Raster:
736
754
  Aligns rasters to the grid of the second raster before differencing.
737
755
  If save=True, saves the difference raster with "_diff.tif" suffix.
738
756
  """
739
- # Load and align rasters, with second raster as reference (cropped to intersection)
740
- diff_data, dst_transform, dst_crs, nodata = self.load_and_diff_rasters(
741
- self.fn, second_fn
742
- )
743
-
744
- # Optionally save difference raster
745
757
  if save:
758
+ diff_da, nodata = self._load_and_diff_rasters_da(self.fn, second_fn)
759
+
746
760
  outdir = os.path.dirname(os.path.abspath(self.fn))
747
761
  outprefix = (
748
762
  os.path.splitext(os.path.split(self.fn)[1])[0]
@@ -751,24 +765,13 @@ class Raster:
751
765
  )
752
766
  dst_fn = os.path.join(outdir, outprefix + "_diff.tif")
753
767
 
754
- # Save with the intersection's grid parameters
755
- height, width = diff_data.shape
756
- profile = {
757
- "driver": "GTiff",
758
- "dtype": rio.float32,
759
- "width": width,
760
- "height": height,
761
- "count": 1,
762
- "crs": dst_crs,
763
- "transform": dst_transform,
764
- "nodata": nodata,
765
- "compress": "lzw",
766
- }
767
-
768
- with rio.open(dst_fn, "w", **profile) as dst:
769
- dst.write(diff_data.filled(nodata).astype(rio.float32), 1)
770
-
771
- return diff_data
768
+ diff_da.rio.write_nodata(nodata, inplace=True)
769
+ diff_da.rio.to_raster(dst_fn, dtype="float32", compress="lzw")
770
+
771
+ return np.ma.masked_invalid(diff_da.values)
772
+ else:
773
+ diff_data, _, _, _ = self.load_and_diff_rasters(self.fn, second_fn)
774
+ return diff_data
772
775
 
773
776
  @staticmethod
774
777
  def save_raster(data, output_fn, reference_fn, dtype=None, nodata=None):
@@ -819,9 +822,9 @@ class Raster:
819
822
  dst.write(data.astype(profile["dtype"]), 1)
820
823
 
821
824
  @staticmethod
822
- def load_and_diff_rasters(first_fn, second_fn):
825
+ def _load_and_diff_rasters_da(first_fn, second_fn):
823
826
  """
824
- Load two rasters, align them, and compute their difference.
827
+ Load two rasters, align them, and compute their difference as a DataArray.
825
828
 
826
829
  Parameters
827
830
  ----------
@@ -833,7 +836,7 @@ class Raster:
833
836
  Returns
834
837
  -------
835
838
  tuple
836
- (difference array (second_raster - first_raster), transform, CRS, nodata)
839
+ (difference xarray.DataArray, nodata value)
837
840
 
838
841
  Notes
839
842
  -----
@@ -841,35 +844,49 @@ class Raster:
841
844
  grid before differencing. Both rasters are cropped to their intersection first
842
845
  (matching geoutils behavior). Uses rioxarray for efficient reprojection.
843
846
  """
844
- # Load rasters with rioxarray
845
847
  first = rioxarray.open_rasterio(first_fn, masked=True).squeeze()
846
848
  second = rioxarray.open_rasterio(second_fn, masked=True).squeeze()
847
849
 
848
- # Get first raster's bounds in second raster's CRS
849
850
  first_bounds_in_ref_crs = first.rio.transform_bounds(second.rio.crs)
850
-
851
- # Clip second raster to first raster's bounds (intersection)
852
851
  second_clipped = second.rio.clip_box(*first_bounds_in_ref_crs)
853
-
854
- # Reproject first raster to match clipped second raster's grid
855
852
  first_reproj = first.rio.reproject_match(second_clipped)
856
853
 
857
- # Compute difference: second - first
858
854
  diff = second_clipped - first_reproj
859
855
 
860
- # Extract metadata
861
- dst_transform = second_clipped.rio.transform()
862
- dst_crs = second_clipped.rio.crs
863
856
  nodata = (
864
857
  second.rio.encoded_nodata
865
858
  if second.rio.encoded_nodata is not None
866
859
  else -9999
867
860
  )
868
861
 
869
- # Convert to numpy masked array
870
- diff_array = np.ma.masked_invalid(diff.values)
862
+ return diff, nodata
871
863
 
872
- return diff_array, dst_transform, dst_crs, nodata
864
+ @staticmethod
865
+ def load_and_diff_rasters(first_fn, second_fn):
866
+ """
867
+ Load two rasters, align them, and compute their difference.
868
+
869
+ Parameters
870
+ ----------
871
+ first_fn : str
872
+ Path to the first raster file
873
+ second_fn : str
874
+ Path to the second raster file (used as reference grid)
875
+
876
+ Returns
877
+ -------
878
+ tuple
879
+ (difference array (second_raster - first_raster), transform, CRS, nodata)
880
+
881
+ Notes
882
+ -----
883
+ The first raster is reprojected and resampled to match the second raster's
884
+ grid before differencing. Both rasters are cropped to their intersection first
885
+ (matching geoutils behavior). Uses rioxarray for efficient reprojection.
886
+ """
887
+ diff, nodata = Raster._load_and_diff_rasters_da(first_fn, second_fn)
888
+ diff_array = np.ma.masked_invalid(diff.values)
889
+ return diff_array, diff.rio.transform(), diff.rio.crs, nodata
873
890
 
874
891
 
875
892
  class Plotter:
@@ -1045,3 +1062,57 @@ class Plotter:
1045
1062
 
1046
1063
  if ctx_kwargs:
1047
1064
  ctx.add_basemap(ax=ax, **ctx_kwargs)
1065
+
1066
+
1067
+ def detect_vantor_satellite(directory):
1068
+ """Check if XML files in directory indicate a Vantor (WorldView) satellite.
1069
+
1070
+ Parameters
1071
+ ----------
1072
+ directory : str
1073
+ Path to directory containing XML camera model files.
1074
+
1075
+ Returns
1076
+ -------
1077
+ bool
1078
+ True if any XML file contains a WorldView SATID (WV01, WV02, WV03, etc.).
1079
+ """
1080
+ try:
1081
+ xml_files = glob_file(directory, "*.[Xx][Mm][Ll]", all_files=True)
1082
+ except Exception:
1083
+ return False
1084
+ if not xml_files:
1085
+ return False
1086
+ xml_files = [f for f in xml_files if not re.search(r".*ortho.*\.xml", f)]
1087
+ for xml_file in xml_files:
1088
+ try:
1089
+ satid = get_xml_tag(xml_file, "SATID")
1090
+ if satid.startswith("WV"):
1091
+ return True
1092
+ except (ValueError, Exception):
1093
+ continue
1094
+ return False
1095
+
1096
+
1097
+ def add_copyright_overlay(ax):
1098
+ """Add Vantor copyright text overlay to the bottom-right of a matplotlib axes.
1099
+
1100
+ Parameters
1101
+ ----------
1102
+ ax : matplotlib.axes.Axes
1103
+ The axes to add the copyright overlay to.
1104
+ """
1105
+ from datetime import datetime
1106
+
1107
+ year = datetime.now().year
1108
+ ax.text(
1109
+ 0.98,
1110
+ 0.02,
1111
+ f"\u00a9 Vantor {year}",
1112
+ transform=ax.transAxes,
1113
+ fontsize=7,
1114
+ color="white",
1115
+ ha="right",
1116
+ va="bottom",
1117
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="black", alpha=0.5),
1118
+ )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "asp_plot"
7
- version = "1.6.4"
7
+ version = "1.7.0"
8
8
  license = {text = "BSD-3-Clause"}
9
9
  authors = [
10
10
  { name="Ben Purinton", email="purinton@uw.edu" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes