diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5f34be..93d61fd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,7 @@ jobs: - name: Build site run: | - uv run main.py + uv run -m wavey --resolution f env: TQDM_DISABLE: 1 diff --git a/pyproject.toml b/pyproject.toml index 90e15ca..50fac7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "matplotlib", "mpld3", "numpy", + "pillow>=9.1", "pygrib", "requests", "tqdm", diff --git a/uv.lock b/uv.lock index 12e1956..867afc1 100644 --- a/uv.lock +++ b/uv.lock @@ -1118,6 +1118,7 @@ dependencies = [ { name = "matplotlib" }, { name = "mpld3" }, { name = "numpy" }, + { name = "pillow" }, { name = "pygrib" }, { name = "requests" }, { name = "tqdm" }, @@ -1142,6 +1143,7 @@ requires-dist = [ { name = "matplotlib" }, { name = "mpld3" }, { name = "numpy" }, + { name = "pillow", specifier = ">=9.1" }, { name = "pygrib" }, { name = "requests" }, { name = "tqdm" }, diff --git a/main.py b/wavey/__main__.py similarity index 86% rename from main.py rename to wavey/__main__.py index a05c4d4..e181768 100644 --- a/main.py +++ b/wavey/__main__.py @@ -1,4 +1,5 @@ import datetime +import io import logging from pathlib import Path @@ -6,13 +7,14 @@ import matplotlib.pyplot as plt import mpld3 import numpy as np +import PIL.Image import pygrib from jinja2 import Environment, PackageLoader, select_autoescape from tqdm import tqdm from wavey.common import DATETIME_FORMAT, FEET_PER_METER, TZ_PACIFIC, TZ_UTC, setup_logging from wavey.grib import NUM_DATA_POINTS, ForecastType, read_forecast_data -from wavey.map import DEFAULT_ARROW_LENGTH, Map +from wavey.map import DEFAULT_ARROW_LENGTH, RESOLUTION, Map from wavey.nwfs import download_forecast, get_most_recent_forecast # Force non-interactive backend to keep consistency between local and github actions @@ -43,10 +45,30 @@ def utc_to_pt(dt: datetime.datetime) -> datetime.datetime: return dt.astimezone(tz=TZ_PACIFIC) +def savefig(path: Path) -> None: + """ + Save matplotlib figure to PNG file. + + We perform a bit of optimization to make the output filesize smaller + without sacrificing quality. + + Args: + path: Path to output PNG file. + """ + + bts = io.BytesIO() + plt.savefig(bts, format="png") + + with PIL.Image.open(bts) as img: + img2 = img.convert("RGB").convert("P", palette=PIL.Image.Palette.ADAPTIVE) + img2.save(path, format="png") + + def main( grib_path: Path | None = None, /, out_dir: Path = Path("_site"), + resolution: RESOLUTION = "h", ) -> None: """ Create plots for significant wave height. @@ -56,8 +78,13 @@ def main( https://nomads.ncep.noaa.gov/pub/data/nccf/com/nwps/prod/. If none, will download the most recent one to the current directory. out_dir: Path to output directory. + resolution: Resolution of the coastline map. Options are crude, low, + intermediate, high, and full. """ + if resolution != "f": + LOG.warning("Not drawing full resolution coastlines. Use the flag '--resolution f'") + out_dir.mkdir(parents=True, exist_ok=True) # Download data, if needed @@ -126,6 +153,7 @@ def main( lat_max_idx=110, lon_min_idx=20, lon_max_idx=70, + resolution=resolution, ) LOG.info("Drawing Breakwater map") @@ -140,6 +168,7 @@ def main( lat_max_idx=BREAKWATER_LAT_IDX + 3, lon_min_idx=BREAKWATER_LON_IDX - 2, lon_max_idx=BREAKWATER_LON_IDX + 3, + resolution=resolution, draw_arrows_length=DEFAULT_ARROW_LENGTH / 3, draw_arrows_stride=1, ) @@ -157,6 +186,7 @@ def main( lat_max_idx=MONASTERY_LAT_IDX + 4, lon_min_idx=MONASTERY_LON_IDX - 3, lon_max_idx=MONASTERY_LON_IDX + 2, + resolution=resolution, draw_arrows_length=DEFAULT_ARROW_LENGTH / 3, draw_arrows_stride=1, ) @@ -176,7 +206,7 @@ def main( map_mon.update(hour_i) ax_main.set_title(f"Significant wave height (ft) and wave direction\nHour {hour_i:03} -- {pacific_time_str}") - plt.savefig(plot_dir / f"{hour_i}.png") + savefig(plot_dir / f"{hour_i}.png") # Get current time diff --git a/wavey/map.py b/wavey/map.py index 1227a46..96cb0bb 100644 --- a/wavey/map.py +++ b/wavey/map.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable, NamedTuple +from typing import Iterable, Literal, NamedTuple import matplotlib import matplotlib.colors as mcolors @@ -125,6 +125,9 @@ def _update_arrows( arrow.arrow.set_data(x=arrow_start[0], y=arrow_start[1], dx=dir_vec[0], dy=dir_vec[1]) +RESOLUTION = Literal["c", "l", "i", "h", "f"] + + class Map: def __init__( self, @@ -137,6 +140,7 @@ def __init__( lat_max_idx: int, lon_min_idx: int, lon_max_idx: int, + resolution: RESOLUTION = "h", draw_arrows_length: float = DEFAULT_ARROW_LENGTH, draw_arrows_stride: int = 3, ) -> None: @@ -156,6 +160,8 @@ def __init__( lat_max_idx: For zooming-in to a smaller lat/lon bounding box. lon_min_idx: For zooming-in to a smaller lat/lon bounding box. lon_max_idx: For zooming-in to a smaller lat/lon bounding box. + resolution: Resolution of the coastline map. Options are crude, + low, intermediate, high, and full. draw_arrows_length: Length of arrows. draw_arrows_stride: Create an arrow for every multiple of `stride` indicies. @@ -187,7 +193,7 @@ def __init__( llcrnrlon=lon_min, urcrnrlat=lat_max, urcrnrlon=lon_max, - resolution="f", + resolution=resolution, ax=ax, ) map.drawcoastlines()