Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/accessiweather/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,21 @@ def update_settings(self, **kwargs) -> bool:
return self._settings.update_settings(**kwargs)

def add_location(
self, name: str, latitude: float, longitude: float, country_code: str | None = None
self,
name: str,
latitude: float,
longitude: float,
country_code: str | None = None,
marine_mode: bool = False,
) -> bool:
"""Add a new location."""
return self._locations.add_location(name, latitude, longitude, country_code)
return self._locations.add_location(
name,
latitude,
longitude,
country_code,
marine_mode=marine_mode,
)

def remove_location(self, name: str) -> bool:
"""Remove a location."""
Expand Down
2 changes: 2 additions & 0 deletions src/accessiweather/config/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def add_location(
latitude: float,
longitude: float,
country_code: str | None = None,
marine_mode: bool = False,
) -> bool:
"""Add a new location if it doesn't already exist."""
config = self._manager.get_config()
Expand All @@ -44,6 +45,7 @@ def add_location(
latitude=latitude,
longitude=longitude,
country_code=country_code,
marine_mode=marine_mode,
)
config.locations.append(new_location)

Expand Down
38 changes: 37 additions & 1 deletion src/accessiweather/display/presentation/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
from datetime import datetime, tzinfo

from ...forecast_confidence import ForecastConfidence
from ...models import AppSettings, Forecast, ForecastPeriod, HourlyForecast, Location
from ...models import (
AppSettings,
Forecast,
ForecastPeriod,
HourlyForecast,
Location,
MarineForecast,
)
from ...utils import TemperatureUnit, calculate_dewpoint
from ...utils.unit_utils import format_precipitation, format_wind_speed
from ..weather_presenter import (
Expand Down Expand Up @@ -105,6 +112,7 @@ def build_forecast(
unit_pref: TemperatureUnit,
settings: AppSettings | None = None,
*,
marine: MarineForecast | None = None,
confidence: ForecastConfidence | None = None,
) -> ForecastPresentation:
"""Create a structured forecast including optional hourly highlights."""
Expand Down Expand Up @@ -277,7 +285,32 @@ def build_forecast(
hours=hourly_hours,
summary_line=hourly_summary_line,
)

marine_summary = None
marine_highlights: list[str] = []
marine_section_text = ""
if marine and marine.has_data():
marine_lines = [f"Marine conditions for {location.name}:"]
if marine.zone_name:
zone_label = marine.zone_name
if marine.zone_id:
zone_label = f"{zone_label} ({marine.zone_id})"
marine_lines.append(f"Marine zone: {zone_label}")
marine_summary = marine.forecast_summary
if marine_summary:
marine_lines.append(f"Summary: {marine_summary}")
marine_highlights = marine.highlights[:4]
if marine_highlights:
marine_lines.append("Wind and wave highlights:")
marine_lines.extend(f" • {highlight}" for highlight in marine_highlights)
for period in marine.periods[:3]:
if period.summary:
marine_lines.append(f"{period.name}: {wrap_text(period.summary, 80)}")
marine_section_text = "\n".join(marine_lines).rstrip()

fallback_sections = [daily_section_text]
if marine_section_text:
fallback_sections.append(marine_section_text)
if hourly_section_text:
fallback_sections.append(hourly_section_text)
fallback_text = "\n\n".join(section for section in fallback_sections if section).rstrip()
Expand All @@ -291,6 +324,9 @@ def build_forecast(
fallback_text=fallback_text,
daily_section_text=daily_section_text,
hourly_section_text=hourly_section_text,
marine_section_text=marine_section_text,
marine_summary=marine_summary,
marine_highlights=marine_highlights,
confidence_label=confidence_label,
summary=summary_line,
)
Expand Down
66 changes: 66 additions & 0 deletions src/accessiweather/display/presentation/html_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@
color: #888;
font-style: italic;
}
.marine-section {
margin-bottom: 16px;
padding: 8px;
background-color: #f3f8ff;
border-radius: 4px;
border: 1px solid #d7e7ff;
}
.marine-section ul {
margin: 6px 0 0 18px;
padding: 0;
}
.marine-section li {
margin-bottom: 4px;
}
.marine-period {
margin-top: 8px;
}
"""


Expand Down Expand Up @@ -259,6 +276,54 @@ def generate_forecast_html(presentation: ForecastPresentation | None) -> str:
<div class="hourly-grid" role="list">
{"".join(hourly_items)}
</div>
</section>"""

# Build marine section if available
marine_html = ""
if presentation.marine_section_text:
marine_summary_html = ""
if presentation.marine_summary:
marine_summary_html = (
f'<p class="description">{_escape_html(presentation.marine_summary)}</p>'
)

marine_highlights_html = ""
if presentation.marine_highlights:
marine_items = "".join(
f"<li>{_escape_html(highlight)}</li>"
for highlight in presentation.marine_highlights
)
marine_highlights_html = f"""
<h3>Marine Highlights</h3>
<ul>
{marine_items}
</ul>"""

period_lines = [
line.strip()
for line in presentation.marine_section_text.splitlines()
if line.strip()
and ":" in line
and not line.startswith(
(
"Marine conditions",
"Marine zone:",
"Summary:",
"Wind and wave highlights:",
"•",
)
)
]
marine_periods_html = "".join(
f'<p class="marine-period">{_escape_html(line)}</p>' for line in period_lines
)

marine_html = f"""
<section class="marine-section" aria-label="Marine forecast">
<h2>Marine Forecast</h2>
{marine_summary_html}
{marine_highlights_html}
{marine_periods_html}
</section>"""

# Build forecast periods
Expand Down Expand Up @@ -295,6 +360,7 @@ def generate_forecast_html(presentation: ForecastPresentation | None) -> str:
<section role="region" aria-label="{title}">
<h1>{title}</h1>
{hourly_html}
{marine_html}
{periods_html}
{generated_html}
</section>
Expand Down
15 changes: 14 additions & 1 deletion src/accessiweather/display/weather_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Forecast,
HourlyForecast,
Location,
MarineForecast,
TrendInsight,
WeatherAlerts,
WeatherData,
Expand Down Expand Up @@ -105,6 +106,9 @@ class ForecastPresentation:
fallback_text: str = ""
daily_section_text: str = ""
hourly_section_text: str = ""
marine_section_text: str = ""
marine_summary: str | None = None
marine_highlights: list[str] = field(default_factory=list)
confidence_label: str | None = None
summary: str | None = None

Expand Down Expand Up @@ -233,6 +237,7 @@ def present(self, weather_data: WeatherData) -> WeatherPresentation:
weather_data.hourly_forecast,
weather_data.location,
unit_pref,
marine=weather_data.marine,
confidence=weather_data.forecast_confidence,
)
if weather_data.forecast
Expand Down Expand Up @@ -308,13 +313,19 @@ def present_forecast(
forecast: Forecast | None,
location: Location,
hourly_forecast: HourlyForecast | None = None,
marine: MarineForecast | None = None,
confidence: ForecastConfidence | None = None,
) -> ForecastPresentation | None:
if not forecast or not forecast.has_data():
return None
unit_pref, _unit_system = self._resolve_unit_preferences(location)
return self._build_forecast(
forecast, hourly_forecast, location, unit_pref, confidence=confidence
forecast,
hourly_forecast,
location,
unit_pref,
marine=marine,
confidence=confidence,
)

def present_alerts(
Expand Down Expand Up @@ -365,6 +376,7 @@ def _build_forecast(
hourly_forecast: HourlyForecast | None,
location: Location,
unit_pref: TemperatureUnit,
marine: MarineForecast | None = None,
confidence: ForecastConfidence | None = None,
) -> ForecastPresentation:
return build_forecast(
Expand All @@ -373,6 +385,7 @@ def _build_forecast(
location,
unit_pref,
settings=self.settings,
marine=marine,
confidence=confidence,
)

Expand Down
4 changes: 4 additions & 0 deletions src/accessiweather/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
HourlyForecastPeriod,
HourlyUVIndex,
Location,
MarineForecast,
MarineForecastPeriod,
MinutelyPrecipitationForecast,
MinutelyPrecipitationPoint,
SourceAttribution,
Expand All @@ -42,6 +44,8 @@
"HourlyForecast",
"HourlyAirQuality",
"HourlyUVIndex",
"MarineForecastPeriod",
"MarineForecast",
"MinutelyPrecipitationPoint",
"MinutelyPrecipitationForecast",
"TrendInsight",
Expand Down
4 changes: 4 additions & 0 deletions src/accessiweather/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ def to_dict(self) -> dict:
"latitude": loc.latitude,
"longitude": loc.longitude,
**({"country_code": loc.country_code} if loc.country_code else {}),
**({"marine_mode": True} if loc.marine_mode else {}),
}
for loc in self.locations
],
Expand All @@ -648,6 +649,7 @@ def to_dict(self) -> dict:
if self.current_location.country_code
else {}
),
**({"marine_mode": True} if self.current_location.marine_mode else {}),
}
if self.current_location
else None,
Expand All @@ -666,6 +668,7 @@ def from_dict(cls, data: dict) -> AppConfig:
latitude=loc_data["latitude"],
longitude=loc_data["longitude"],
country_code=loc_data.get("country_code"),
marine_mode=bool(loc_data.get("marine_mode", False)),
)
)

Expand All @@ -677,6 +680,7 @@ def from_dict(cls, data: dict) -> AppConfig:
latitude=loc_data["latitude"],
longitude=loc_data["longitude"],
country_code=loc_data.get("country_code"),
marine_mode=bool(loc_data.get("marine_mode", False)),
)

return cls(
Expand Down
27 changes: 27 additions & 0 deletions src/accessiweather/models/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class Location:
longitude: float
timezone: str | None = None
country_code: str | None = None
marine_mode: bool = False

def __str__(self) -> str:
return self.name
Expand Down Expand Up @@ -532,6 +533,30 @@ def has_taf(self) -> bool:
)


@dataclass
class MarineForecastPeriod:
"""Single marine forecast period from an NWS marine zone product."""

name: str
summary: str


@dataclass
class MarineForecast:
"""Marine zone essentials for a coastal location."""

zone_id: str | None = None
zone_name: str | None = None
forecast_summary: str | None = None
issued_at: datetime | None = None
periods: list[MarineForecastPeriod] = field(default_factory=list)
highlights: list[str] = field(default_factory=list)

def has_data(self) -> bool:
"""Return True when any marine essentials are available."""
return bool(self.forecast_summary or self.zone_name or self.periods or self.highlights)


@dataclass
class WeatherData:
"""Complete weather data for a location."""
Expand All @@ -547,6 +572,7 @@ class WeatherData:
alerts: WeatherAlerts | None = None
environmental: EnvironmentalConditions | None = None
aviation: AviationData | None = None
marine: MarineForecast | None = None
trend_insights: list[TrendInsight] = field(default_factory=list)
stale: bool = False
stale_since: datetime | None = None
Expand Down Expand Up @@ -584,5 +610,6 @@ def has_any_data(self) -> bool:
self.alerts and self.alerts.has_alerts(),
self.environmental and self.environmental.has_data(),
self.aviation and self.aviation.has_taf(),
self.marine and self.marine.has_data(),
]
)
16 changes: 15 additions & 1 deletion src/accessiweather/ui/dialogs/location_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ def _create_ui(self):
help_text.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
name_sizer.Add(help_text, 0, wx.TOP, 5)

self.marine_mode_checkbox = wx.CheckBox(
panel,
label="Enable Marine Mode for this location (coastal essentials only)",
)
self.marine_mode_checkbox.SetToolTip(
"Adds nearby NWS marine zone summary, wind and wave highlights, and marine advisories."
)
name_sizer.Add(self.marine_mode_checkbox, 0, wx.TOP, 8)

main_sizer.Add(name_sizer, 0, wx.EXPAND | wx.ALL, 10)

# Search section
Expand Down Expand Up @@ -174,6 +183,7 @@ def _setup_accessibility(self):
self.name_input.SetName("Location name input")
self.search_input.SetName("Search for location")
self.results_list.SetName("Search results")
self.marine_mode_checkbox.SetName("Enable Marine Mode for this location")

def _on_key(self, event: wx.KeyEvent) -> None:
"""Handle key events."""
Expand Down Expand Up @@ -307,7 +317,11 @@ def _on_save(self, event):

# Add location
success = self.config_manager.add_location(
name, latitude, longitude, country_code=country_code
name,
latitude,
longitude,
country_code=country_code,
marine_mode=self.marine_mode_checkbox.GetValue(),
)

if success:
Expand Down
Loading
Loading