diff --git a/.vscode/launch.json b/.vscode/launch.json
index 71250ea..c2319c2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -467,9 +467,11 @@
// Test --update_ssds_provenance with a short log_file
//"args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250128_20250131/20250131T051404/202501310514_202501310535.nc4", "--update_ssds_provenance"]
// Test ahi mission that has Backseat Planktivore data with --update_ssds_provenance
- "args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250414_20250418/20250415T040019/202504150400_202504152346.nc4", "--update_ssds_provenance"]
+ //"args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250414_20250418/20250415T040019/202504150400_202504152346.nc4", "--update_ssds_provenance"]
// Make per log file .html files to test with lrauv_deployment_plots and --update_ssds_provenance
//"args": ["-v", "1", "--auv_name", "ahi", "--start", "20251022T000000", "--end", "20251024T000000", "--update_ssds_provenance", "--clobber"]
+ // Make per log file .html files to test with lrauv_deployment_plots and --update_ssds_provenance to test dynamic map bounds
+ "args": ["-v", "1", "--auv_name", "ahi", "--start", "20260409T000000", "--end", "20260415T000000", "--update_ssds_provenance", "--clobber"]
},
{
"name": "lrauv_deployment_plots",
diff --git a/src/data/create_products.py b/src/data/create_products.py
index 55b5345..6295bcf 100755
--- a/src/data/create_products.py
+++ b/src/data/create_products.py
@@ -1062,9 +1062,27 @@ def _plot_track_map( # noqa: PLR0915
# Store original position
pos = map_ax.get_position()
- # Set fixed axis limits for Monterey Bay area (in Web Mercator) FIRST
- lon_bounds = [-122.41, -121.77]
- lat_bounds = [36.5, 37.0]
+ # Use fixed Monterey Bay bounds when the track fits within them; otherwise
+ # compute bounds dynamically from the data with a 10% margin.
+ fixed_lon_bounds = [-122.41, -121.77]
+ fixed_lat_bounds = [36.5, 37.0]
+ valid_lons = lons[~np.isnan(lons)]
+ valid_lats = lats[~np.isnan(lats)]
+ if (
+ valid_lons.size > 0
+ and valid_lats.size > 0
+ and valid_lons.min() >= fixed_lon_bounds[0]
+ and valid_lons.max() <= fixed_lon_bounds[1]
+ and valid_lats.min() >= fixed_lat_bounds[0]
+ and valid_lats.max() <= fixed_lat_bounds[1]
+ ):
+ lon_bounds = fixed_lon_bounds
+ lat_bounds = fixed_lat_bounds
+ else:
+ lon_margin = (valid_lons.max() - valid_lons.min()) * 0.1 or 0.05
+ lat_margin = (valid_lats.max() - valid_lats.min()) * 0.1 or 0.05
+ lon_bounds = [valid_lons.min() - lon_margin, valid_lons.max() + lon_margin]
+ lat_bounds = [valid_lats.min() - lat_margin, valid_lats.max() + lat_margin]
x_bounds, y_bounds = transformer.transform(lon_bounds, lat_bounds)
map_ax.set_xlim(x_bounds)
map_ax.set_ylim(y_bounds)
@@ -1072,12 +1090,20 @@ def _plot_track_map( # noqa: PLR0915
# Make the plot square by using equal aspect with explicit box adjustment
map_ax.set_aspect("equal", adjustable="box")
- # Plot the track with profile_number coloring in Web Mercator coordinates
+ # Plot the track colored by a cumulative profile number that keeps
+ # incrementing across concatenated log files (profile_number resets to
+ # zero at the start of each log file).
profile_numbers = self.ds["profile_number"].to_numpy()
+ cumulative_profile = profile_numbers.copy().astype(float)
+ offset = 0
+ for i in range(1, len(profile_numbers)):
+ if profile_numbers[i] < profile_numbers[i - 1]:
+ offset += profile_numbers[i - 1]
+ cumulative_profile[i] = profile_numbers[i] + offset
scatter = map_ax.scatter(
x_merc,
y_merc,
- c=profile_numbers,
+ c=cumulative_profile,
cmap="jet",
s=1,
alpha=0.6,
@@ -1104,7 +1130,7 @@ def _plot_track_map( # noqa: PLR0915
# Now position map aligned with left edge of reference, 50% width
# Use a square aspect ratio based on the y-dimension
map_height = pos.height
- aspect_ratio = (37.0 - 36.5) / (122.41 - 121.77) # data aspect ratio
+ aspect_ratio = (lat_bounds[1] - lat_bounds[0]) / (lon_bounds[1] - lon_bounds[0])
map_width = map_height / aspect_ratio * 0.7 # scale to fit nicely
# Align map top with the nighttime indicator top when ref axes is available.
diff --git a/src/data/lrauv_deployment_plots.py b/src/data/lrauv_deployment_plots.py
index 9370238..19321dd 100755
--- a/src/data/lrauv_deployment_plots.py
+++ b/src/data/lrauv_deployment_plots.py
@@ -159,36 +159,23 @@ def _concat_datasets(self, nc_files: list[Path | str]) -> xr.Dataset | None:
Accepts both local Paths and OPeNDAP URL strings — xr.open_dataset
handles both transparently.
- Tries xr.open_mfdataset first; falls back to manual concat on failure.
Uses join='outer' so every variable present in any log_file is retained
(absent values filled with NaN).
"""
if not nc_files:
return None
- paths = [str(p) for p in nc_files]
- try:
- self.logger.info("Concatenating %d files via open_mfdataset", len(paths))
- return xr.open_mfdataset(
- paths,
- combine="by_coords",
- join="outer",
- parallel=True,
- chunks="auto",
- )
- except Exception as exc: # noqa: BLE001
- self.logger.warning("open_mfdataset failed (%s), falling back to xr.concat", exc)
-
datasets = []
for p in nc_files:
try:
- datasets.append(xr.open_dataset(p))
+ datasets.append(xr.open_dataset(str(p)))
except OSError as e:
self.logger.warning("Skipping %s: %s", p, e)
if not datasets:
return None
+ self.logger.info("Concatenating %d dataset(s) via xr.concat", len(datasets))
return xr.concat(datasets, dim="time", join="outer")
def _deployment_has_outputs(self, deployment_dir: Path, plot_name_stem: str) -> bool:
@@ -408,9 +395,11 @@ def _send_notify_email(
std_png = None
web_url = get_web_url(str(std_html)) if std_html else ""
+ sent_on = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
plain = (
f"New LRAUV deployment plots: {deployment_name}\n\n"
- f" View this and related information on the web:\n {web_url}"
+ f" View this and related information on the web:\n {web_url}\n\n"
+ f"Sent on: {sent_on}"
)
img_tag = (
'
'
@@ -418,7 +407,9 @@ def _send_notify_email(
else ""
)
html_body = (
- f'{img_tag}
View this and related information on the web
' + f"{img_tag}" + f'View this and related information on the web
' + f"Sent on: {sent_on}
" ) outer = MIMEMultipart("related") @@ -608,6 +599,7 @@ def _write_per_png_html( # noqa: C901, PLR0913 grouped.setdefault(log_dir, []).append(url) # Collect per-row data first so we can suppress empty columns + nt = 'target="_blank" rel="noopener"' # new-tab attributes for all links rows: list[dict] = [] for log_dir in sorted(grouped): nc_urls = grouped[log_dir] @@ -616,9 +608,11 @@ def _write_per_png_html( # noqa: C901, PLR0913 rows.append( { "dir": log_dir, - "plots": "View these data in {db_label}
\n' + stoqs_line = f'View these data in {db_label}
\n' if png_file_path is not None and png_file_path.exists(): b64 = base64.b64encode(png_file_path.read_bytes()).decode("ascii") @@ -673,7 +667,7 @@ def _write_per_png_html( # noqa: C901, PLR0913 footer = ( "Created by " - f'lrauv_deployment_plots.py' + f'lrauv_deployment_plots.py' f" on {created_ts}
\n" ) html = ( diff --git a/src/data/make_permalink.py b/src/data/make_permalink.py index 2052ee0..24129d4 100755 --- a/src/data/make_permalink.py +++ b/src/data/make_permalink.py @@ -8,7 +8,7 @@ import csv import json import sys -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from pathlib import Path import lzstring @@ -96,7 +96,8 @@ def stoqs_url_from_ds(ds: xr.Dataset, base_url: str | None = None, auv_name: str # --- times --- times_np = ds.cf["time"].to_numpy() stime = pd.Timestamp(times_np[0]).to_pydatetime().replace(tzinfo=UTC) - etime = pd.Timestamp(times_np[-1]).to_pydatetime().replace(tzinfo=UTC) + # Subtract 1 second from the end time to avoid picking up the next Activity in the STOQS UI + etime = pd.Timestamp(times_np[-1]).to_pydatetime().replace(tzinfo=UTC) - timedelta(seconds=1) # --- depths --- depths_np = ds.cf["depth"].to_numpy()