Add Mapbox token configuration to docker-compose and implement turbine information table generation in report. Enhance visualization functions with satellite basemap integration and improve lightning size scaling in coordinate plane plots.
This commit is contained in:
parent
64b99eed06
commit
17c9aa865a
@ -28,6 +28,9 @@ services:
|
|||||||
# service to call Gemini itself.
|
# service to call Gemini itself.
|
||||||
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
||||||
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-1.5-flash}
|
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-1.5-flash}
|
||||||
|
# Mapbox token for the satellite basemap behind the coordinate-plane charts.
|
||||||
|
# Charts fall back to a plain background when unset.
|
||||||
|
- MAPBOX_TOKEN=${MAPBOX_TOKEN:-}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -24,6 +24,7 @@ from src.reporting.docx_sections import (
|
|||||||
_build_current_vs_distance_chart,
|
_build_current_vs_distance_chart,
|
||||||
build_lightning_event_table_data,
|
build_lightning_event_table_data,
|
||||||
build_risk_table_data,
|
build_risk_table_data,
|
||||||
|
build_turbine_information_table_data,
|
||||||
)
|
)
|
||||||
from src.utils import (
|
from src.utils import (
|
||||||
format_datetime_ddmmyyyy_hhmm,
|
format_datetime_ddmmyyyy_hhmm,
|
||||||
@ -514,21 +515,7 @@ def create_docx_report(
|
|||||||
# Turbine information
|
# Turbine information
|
||||||
_add_title(doc, "Turbine Information", size_pt=16, align=WD_ALIGN_PARAGRAPH.LEFT)
|
_add_title(doc, "Turbine Information", size_pt=16, align=WD_ALIGN_PARAGRAPH.LEFT)
|
||||||
_add_paragraph(doc, "This table contains detailed information about all turbines in the wind farm.", size_pt=10)
|
_add_paragraph(doc, "This table contains detailed information about all turbines in the wind farm.", size_pt=10)
|
||||||
turb_rows: list[list[str]] = [[
|
_add_table(doc, build_turbine_information_table_data(turbine_df))
|
||||||
"Turbine", "Lat", "Lng", "Unit Power (MWm)", "Unit Power (MWe)", "Tower Height (m)", "Rotor Diameter (m)", "Altitude (m)",
|
|
||||||
]]
|
|
||||||
for _, t in turbine_df.iterrows():
|
|
||||||
turb_rows.append([
|
|
||||||
str(t.get("name", "N/A")),
|
|
||||||
f"{t.get('lat', 0):.4f}",
|
|
||||||
f"{t.get('lng', 0):.4f}",
|
|
||||||
str(t.get("unit_power_mwm", "N/A")),
|
|
||||||
str(t.get("unit_power_mwe", "N/A")),
|
|
||||||
str(t.get("tower_height_m", "N/A")),
|
|
||||||
str(t.get("turbine_rotor_blade_diameter", "N/A")),
|
|
||||||
str(t.get("altitude", "N/A")),
|
|
||||||
])
|
|
||||||
_add_table(doc, turb_rows)
|
|
||||||
|
|
||||||
# Gemini commentary (single API call per report run; falls back deterministically if Gemini is unavailable)
|
# Gemini commentary (single API call per report run; falls back deterministically if Gemini is unavailable)
|
||||||
analysis_radius_km = float(get_analysis_radius_m()) / 1000.0 if get_analysis_radius_m() > 0 else float(max(config.distance_rings) / 1000.0)
|
analysis_radius_km = float(get_analysis_radius_m()) / 1000.0 if get_analysis_radius_m() > 0 else float(max(config.distance_rings) / 1000.0)
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
@ -166,6 +168,55 @@ def _build_current_vs_distance_chart(
|
|||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
|
def _turbine_cell_value_is_available(value: Any) -> bool:
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if pd.isna(value):
|
||||||
|
return False
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
if text.lower() in {"n/a", "na", "nan", "none", "-", "--"}:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def build_turbine_information_table_data(
|
||||||
|
turbine_df: pd.DataFrame,
|
||||||
|
) -> list[list[str]]:
|
||||||
|
column_specs: list[tuple[str, str, Callable[[pd.Series], str], bool]] = [
|
||||||
|
("Turbine", "name", lambda t: str(t.get("name", "N/A")), True),
|
||||||
|
("Lat", "lat", lambda t: f"{t.get('lat', 0):.4f}", True),
|
||||||
|
("Lng", "lng", lambda t: f"{t.get('lng', 0):.4f}", True),
|
||||||
|
("Unit Power (MWm)", "unit_power_mwm", lambda t: str(t.get("unit_power_mwm", "N/A")), False),
|
||||||
|
("Unit Power (MWe)", "unit_power_mwe", lambda t: str(t.get("unit_power_mwe", "N/A")), False),
|
||||||
|
("Tower Height (m)", "tower_height_m", lambda t: str(t.get("tower_height_m", "N/A")), False),
|
||||||
|
(
|
||||||
|
"Rotor Diameter (m)",
|
||||||
|
"turbine_rotor_blade_diameter",
|
||||||
|
lambda t: str(t.get("turbine_rotor_blade_diameter", "N/A")),
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
("Altitude (m)", "altitude", lambda t: str(t.get("altitude", "N/A")), False),
|
||||||
|
]
|
||||||
|
|
||||||
|
active_columns: list[tuple[str, Callable[[pd.Series], str]]] = []
|
||||||
|
for header, field, formatter, always_include in column_specs:
|
||||||
|
if always_include:
|
||||||
|
active_columns.append((header, formatter))
|
||||||
|
continue
|
||||||
|
if any(_turbine_cell_value_is_available(row.get(field)) for _, row in turbine_df.iterrows()):
|
||||||
|
active_columns.append((header, formatter))
|
||||||
|
|
||||||
|
rows: list[list[str]] = [[header for header, _ in active_columns]]
|
||||||
|
for _, turbine in turbine_df.iterrows():
|
||||||
|
rows.append([formatter(turbine) for _, formatter in active_columns])
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def build_lightning_event_table_data(
|
def build_lightning_event_table_data(
|
||||||
centroid_lat: float, centroid_lng: float, lightning_df: pd.DataFrame
|
centroid_lat: float, centroid_lng: float, lightning_df: pd.DataFrame
|
||||||
) -> tuple[list[list[str]], list[str]]:
|
) -> tuple[list[list[str]], list[str]]:
|
||||||
|
|||||||
149
src/visualization/basemap.py
Normal file
149
src/visualization/basemap.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
"""Satellite basemap helper for coordinate-plane charts.
|
||||||
|
|
||||||
|
Fetches a Mapbox static satellite image for a longitude/latitude bounding box and
|
||||||
|
adds it behind the existing Plotly scatter layers so turbines, lightning, rings and
|
||||||
|
storm polygons render on top of real imagery. Fails gracefully (no-op) when the
|
||||||
|
token is missing or the request fails, keeping report generation robust.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MAPBOX_STATIC_URL = "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static"
|
||||||
|
MAPBOX_MAX_DIMENSION = 1280
|
||||||
|
DEFAULT_IMAGE_HEIGHT = 1000
|
||||||
|
REQUEST_TIMEOUT_SECONDS = 15
|
||||||
|
|
||||||
|
_image_cache: Dict[Tuple[float, float, float, float, int, int], str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _lonlat_to_mercator(lon: float, lat: float) -> Tuple[float, float]:
|
||||||
|
"""Project a longitude/latitude pair to Web Mercator units."""
|
||||||
|
clamped_lat = max(min(lat, 85.05112878), -85.05112878)
|
||||||
|
x = math.radians(lon)
|
||||||
|
y = math.log(math.tan(math.pi / 4 + math.radians(clamped_lat) / 2))
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_dimensions(lon_min: float, lon_max: float, lat_min: float, lat_max: float) -> Tuple[int, int]:
|
||||||
|
"""Return Mapbox image width/height matching the bbox aspect ratio in Mercator."""
|
||||||
|
mx_min, my_min = _lonlat_to_mercator(lon_min, lat_min)
|
||||||
|
mx_max, my_max = _lonlat_to_mercator(lon_max, lat_max)
|
||||||
|
|
||||||
|
mercator_width = abs(mx_max - mx_min)
|
||||||
|
mercator_height = abs(my_max - my_min)
|
||||||
|
if mercator_height <= 0:
|
||||||
|
return MAPBOX_MAX_DIMENSION, MAPBOX_MAX_DIMENSION
|
||||||
|
|
||||||
|
aspect = mercator_width / mercator_height
|
||||||
|
|
||||||
|
if aspect >= 1:
|
||||||
|
width = MAPBOX_MAX_DIMENSION
|
||||||
|
height = max(1, round(width / aspect))
|
||||||
|
else:
|
||||||
|
height = DEFAULT_IMAGE_HEIGHT
|
||||||
|
width = max(1, round(height * aspect))
|
||||||
|
|
||||||
|
width = min(width, MAPBOX_MAX_DIMENSION)
|
||||||
|
height = min(height, MAPBOX_MAX_DIMENSION)
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_satellite_image(
|
||||||
|
token: str,
|
||||||
|
lon_min: float,
|
||||||
|
lon_max: float,
|
||||||
|
lat_min: float,
|
||||||
|
lat_max: float,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Fetch the Mapbox static satellite image and return it as a base64 data URI."""
|
||||||
|
import requests
|
||||||
|
|
||||||
|
bbox = f"[{lon_min},{lat_min},{lon_max},{lat_max}]"
|
||||||
|
url = f"{MAPBOX_STATIC_URL}/{bbox}/{width}x{height}@2x"
|
||||||
|
params = {
|
||||||
|
"access_token": token,
|
||||||
|
"attribution": "false",
|
||||||
|
"logo": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT_SECONDS)
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.warning(
|
||||||
|
"Mapbox static image request failed (status %s): %s",
|
||||||
|
response.status_code,
|
||||||
|
response.text[:200],
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
encoded = base64.b64encode(response.content).decode("ascii")
|
||||||
|
return f"data:image/png;base64,{encoded}"
|
||||||
|
|
||||||
|
|
||||||
|
def add_satellite_basemap(
|
||||||
|
fig: go.Figure,
|
||||||
|
lon_min: float,
|
||||||
|
lon_max: float,
|
||||||
|
lat_min: float,
|
||||||
|
lat_max: float,
|
||||||
|
) -> go.Figure:
|
||||||
|
"""Add a satellite basemap behind the figure traces for the given bbox.
|
||||||
|
|
||||||
|
Reuses cached imagery for an identical bbox so the IC, CG and storm charts of a
|
||||||
|
single farm share one Mapbox request. Returns the figure unchanged when no token
|
||||||
|
is configured or the request fails.
|
||||||
|
"""
|
||||||
|
token = os.getenv("MAPBOX_TOKEN")
|
||||||
|
if not token:
|
||||||
|
logger.warning("MAPBOX_TOKEN is not set; rendering charts without satellite basemap")
|
||||||
|
return fig
|
||||||
|
|
||||||
|
width, height = _compute_dimensions(lon_min, lon_max, lat_min, lat_max)
|
||||||
|
cache_key = (
|
||||||
|
round(lon_min, 6),
|
||||||
|
round(lon_max, 6),
|
||||||
|
round(lat_min, 6),
|
||||||
|
round(lat_max, 6),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
|
||||||
|
image_source = _image_cache.get(cache_key)
|
||||||
|
if image_source is None:
|
||||||
|
try:
|
||||||
|
image_source = _fetch_satellite_image(
|
||||||
|
token, lon_min, lon_max, lat_min, lat_max, width, height
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to fetch Mapbox satellite basemap: %s", exc)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
if image_source is None:
|
||||||
|
return fig
|
||||||
|
|
||||||
|
_image_cache[cache_key] = image_source
|
||||||
|
|
||||||
|
fig.add_layout_image(
|
||||||
|
dict(
|
||||||
|
source=image_source,
|
||||||
|
xref="x",
|
||||||
|
yref="y",
|
||||||
|
x=lon_min,
|
||||||
|
y=lat_max,
|
||||||
|
sizex=lon_max - lon_min,
|
||||||
|
sizey=lat_max - lat_min,
|
||||||
|
xanchor="left",
|
||||||
|
yanchor="top",
|
||||||
|
sizing="stretch",
|
||||||
|
layer="below",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return fig
|
||||||
@ -3,8 +3,14 @@ import plotly.express as px
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from src.analysis.geospatial import create_circle_points, haversine_distance
|
from src.analysis.geospatial import create_circle_points, haversine_distance
|
||||||
from src.config import config
|
from src.config import config
|
||||||
|
from src.visualization.basemap import add_satellite_basemap
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
COORDINATE_PLANE_RING_LINE_WIDTH = 4
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MIN = 10
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MAX = 24
|
||||||
|
COORDINATE_PLANE_LIGHTNING_CURRENT_SCALE = 800
|
||||||
|
|
||||||
def plot_turbine_map(turbine_row: pd.Series, lightning_df: pd.DataFrame, turbine_df: pd.DataFrame) -> go.Figure:
|
def plot_turbine_map(turbine_row: pd.Series, lightning_df: pd.DataFrame, turbine_df: pd.DataFrame) -> go.Figure:
|
||||||
turbine_lat = turbine_row['lat']
|
turbine_lat = turbine_row['lat']
|
||||||
turbine_lon = turbine_row['lng']
|
turbine_lon = turbine_row['lng']
|
||||||
@ -487,7 +493,7 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
|
|||||||
x=circle_lons, # X-axis = Longitude
|
x=circle_lons, # X-axis = Longitude
|
||||||
y=circle_lats, # Y-axis = Latitude
|
y=circle_lats, # Y-axis = Latitude
|
||||||
mode='lines',
|
mode='lines',
|
||||||
line=dict(color=color, width=2),
|
line=dict(color=color, width=COORDINATE_PLANE_RING_LINE_WIDTH),
|
||||||
opacity=0.6,
|
opacity=0.6,
|
||||||
name=f'{radius/1000:.1f}km Distance Ring',
|
name=f'{radius/1000:.1f}km Distance Ring',
|
||||||
showlegend=True
|
showlegend=True
|
||||||
@ -567,7 +573,11 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
|
|||||||
break
|
break
|
||||||
lightning_colors.append(color)
|
lightning_colors.append(color)
|
||||||
|
|
||||||
lightning_sizes = np.clip(ic_lightning_df['current_abs'] / 1500, 3, 12)
|
lightning_sizes = np.clip(
|
||||||
|
ic_lightning_df['current_abs'] / COORDINATE_PLANE_LIGHTNING_CURRENT_SCALE,
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MIN,
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
fig.add_trace(go.Scatter(
|
fig.add_trace(go.Scatter(
|
||||||
x=ic_lightning_df['lng'], # X-axis = Longitude
|
x=ic_lightning_df['lng'], # X-axis = Longitude
|
||||||
@ -576,9 +586,10 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
|
|||||||
marker=dict(
|
marker=dict(
|
||||||
size=lightning_sizes,
|
size=lightning_sizes,
|
||||||
color=lightning_colors,
|
color=lightning_colors,
|
||||||
opacity=0.8,
|
opacity=0.9,
|
||||||
symbol='circle',
|
symbol='circle',
|
||||||
sizemin=3
|
sizemin=COORDINATE_PLANE_LIGHTNING_SIZE_MIN,
|
||||||
|
line=dict(width=1, color='white'),
|
||||||
),
|
),
|
||||||
name='Intercloud Lightning',
|
name='Intercloud Lightning',
|
||||||
showlegend=True
|
showlegend=True
|
||||||
@ -592,13 +603,15 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
|
|||||||
lon_min = turbine_lon - max_radius_deg * 1.5
|
lon_min = turbine_lon - max_radius_deg * 1.5
|
||||||
lon_max = turbine_lon + max_radius_deg * 1.5
|
lon_max = turbine_lon + max_radius_deg * 1.5
|
||||||
|
|
||||||
|
add_satellite_basemap(fig, lon_min, lon_max, lat_min, lat_max)
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title=f'Intercloud Lightning - Coordinate Plane View - Central Turbine: {turbine_row["name"]}',
|
title=f'Intercloud Lightning - Coordinate Plane View - Central Turbine: {turbine_row["name"]}',
|
||||||
xaxis=dict(
|
xaxis=dict(
|
||||||
title=dict(text='Longitude', font=dict(size=28)), # x-axis title font size
|
title=dict(text='Longitude', font=dict(size=28)), # x-axis title font size
|
||||||
tickfont=dict(size=22), # x-axis tick label font size
|
tickfont=dict(size=22), # x-axis tick label font size
|
||||||
range=[lon_min, lon_max],
|
range=[lon_min, lon_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False
|
zeroline=False
|
||||||
@ -607,7 +620,7 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
|
|||||||
title=dict(text='Latitude', font=dict(size=28)), # y-axis title font size
|
title=dict(text='Latitude', font=dict(size=28)), # y-axis title font size
|
||||||
tickfont=dict(size=22), # y-axis tick label font size
|
tickfont=dict(size=22), # y-axis tick label font size
|
||||||
range=[lat_min, lat_max],
|
range=[lat_min, lat_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False
|
zeroline=False
|
||||||
@ -652,7 +665,7 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
|
|||||||
x=circle_lons, # X-axis = Longitude
|
x=circle_lons, # X-axis = Longitude
|
||||||
y=circle_lats, # Y-axis = Latitude
|
y=circle_lats, # Y-axis = Latitude
|
||||||
mode='lines',
|
mode='lines',
|
||||||
line=dict(color=color, width=2),
|
line=dict(color=color, width=COORDINATE_PLANE_RING_LINE_WIDTH),
|
||||||
opacity=0.6,
|
opacity=0.6,
|
||||||
name=f'{radius/1000:.1f}km Distance Ring',
|
name=f'{radius/1000:.1f}km Distance Ring',
|
||||||
showlegend=True
|
showlegend=True
|
||||||
@ -732,7 +745,11 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
|
|||||||
break
|
break
|
||||||
lightning_colors.append(color)
|
lightning_colors.append(color)
|
||||||
|
|
||||||
lightning_sizes = np.clip(cg_lightning_df['current_abs'] / 1500, 3, 12)
|
lightning_sizes = np.clip(
|
||||||
|
cg_lightning_df['current_abs'] / COORDINATE_PLANE_LIGHTNING_CURRENT_SCALE,
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MIN,
|
||||||
|
COORDINATE_PLANE_LIGHTNING_SIZE_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
fig.add_trace(go.Scatter(
|
fig.add_trace(go.Scatter(
|
||||||
x=cg_lightning_df['lng'], # X-axis = Longitude
|
x=cg_lightning_df['lng'], # X-axis = Longitude
|
||||||
@ -741,9 +758,10 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
|
|||||||
marker=dict(
|
marker=dict(
|
||||||
size=lightning_sizes,
|
size=lightning_sizes,
|
||||||
color=lightning_colors,
|
color=lightning_colors,
|
||||||
opacity=0.8,
|
opacity=0.9,
|
||||||
symbol='circle',
|
symbol='circle',
|
||||||
sizemin=3
|
sizemin=COORDINATE_PLANE_LIGHTNING_SIZE_MIN,
|
||||||
|
line=dict(width=1, color='white'),
|
||||||
),
|
),
|
||||||
name='Cloud-to-Ground Lightning',
|
name='Cloud-to-Ground Lightning',
|
||||||
showlegend=True
|
showlegend=True
|
||||||
@ -757,13 +775,15 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
|
|||||||
lon_min = turbine_lon - max_radius_deg * 1.5
|
lon_min = turbine_lon - max_radius_deg * 1.5
|
||||||
lon_max = turbine_lon + max_radius_deg * 1.5
|
lon_max = turbine_lon + max_radius_deg * 1.5
|
||||||
|
|
||||||
|
add_satellite_basemap(fig, lon_min, lon_max, lat_min, lat_max)
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title=f'Cloud-to-Ground Lightning - Coordinate Plane View - Central Turbine: {turbine_row["name"]}',
|
title=f'Cloud-to-Ground Lightning - Coordinate Plane View - Central Turbine: {turbine_row["name"]}',
|
||||||
xaxis=dict(
|
xaxis=dict(
|
||||||
title=dict(text='Longitude', font=dict(size=28)), # x-axis title font size
|
title=dict(text='Longitude', font=dict(size=28)), # x-axis title font size
|
||||||
tickfont=dict(size=22), # x-axis tick label font size
|
tickfont=dict(size=22), # x-axis tick label font size
|
||||||
range=[lon_min, lon_max],
|
range=[lon_min, lon_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False
|
zeroline=False
|
||||||
@ -772,7 +792,7 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
|
|||||||
title=dict(text='Latitude', font=dict(size=28)), # y-axis title font size
|
title=dict(text='Latitude', font=dict(size=28)), # y-axis title font size
|
||||||
tickfont=dict(size=22), # y-axis tick label font size
|
tickfont=dict(size=22), # y-axis tick label font size
|
||||||
range=[lat_min, lat_max],
|
range=[lat_min, lat_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False
|
zeroline=False
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from zoneinfo import ZoneInfo
|
|||||||
from src.analysis.geospatial import haversine_distance
|
from src.analysis.geospatial import haversine_distance
|
||||||
from src.config import config
|
from src.config import config
|
||||||
from src.utils import parse_period_string_to_datetime
|
from src.utils import parse_period_string_to_datetime
|
||||||
|
from src.visualization.basemap import add_satellite_basemap
|
||||||
|
|
||||||
def format_datetime_for_display(datetime_str: str) -> str:
|
def format_datetime_for_display(datetime_str: str) -> str:
|
||||||
"""
|
"""
|
||||||
@ -165,7 +166,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
|||||||
x=circle_lons, # X-axis = Longitude
|
x=circle_lons, # X-axis = Longitude
|
||||||
y=circle_lats, # Y-axis = Latitude
|
y=circle_lats, # Y-axis = Latitude
|
||||||
mode='lines',
|
mode='lines',
|
||||||
line=dict(color=color, width=2),
|
line=dict(color=color, width=4),
|
||||||
opacity=0.6,
|
opacity=0.6,
|
||||||
name=f'{radius/1000:.0f}km Ring',
|
name=f'{radius/1000:.0f}km Ring',
|
||||||
showlegend=True
|
showlegend=True
|
||||||
@ -284,6 +285,8 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
|||||||
lon_min = center_lon - max_radius_deg * 1.5
|
lon_min = center_lon - max_radius_deg * 1.5
|
||||||
lon_max = center_lon + max_radius_deg * 1.5
|
lon_max = center_lon + max_radius_deg * 1.5
|
||||||
|
|
||||||
|
add_satellite_basemap(fig, lon_min, lon_max, lat_min, lat_max)
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
font=dict(size=18),
|
font=dict(size=18),
|
||||||
title=dict(text='Storm Cells - Coordinate Plane View', font=dict(size=28)),
|
title=dict(text='Storm Cells - Coordinate Plane View', font=dict(size=28)),
|
||||||
@ -291,7 +294,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
|||||||
yaxis_title='Latitude',
|
yaxis_title='Latitude',
|
||||||
xaxis=dict(
|
xaxis=dict(
|
||||||
range=[lon_min, lon_max],
|
range=[lon_min, lon_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False,
|
zeroline=False,
|
||||||
@ -300,7 +303,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
|||||||
),
|
),
|
||||||
yaxis=dict(
|
yaxis=dict(
|
||||||
range=[lat_min, lat_max],
|
range=[lat_min, lat_max],
|
||||||
showgrid=True,
|
showgrid=False,
|
||||||
gridwidth=1,
|
gridwidth=1,
|
||||||
gridcolor='lightgray',
|
gridcolor='lightgray',
|
||||||
zeroline=False,
|
zeroline=False,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user