Remove Lightning_Report_Automatic.json file, eliminating the associated n8n workflow and its components for report generation and token management. Update Lightning_Report_Manual.json to reflect changes in node positions and IDs, ensuring consistency across workflows. Enhance report generation logic by adding detailed strike list sections for cloud-to-ground and intercloud lightning events, improving clarity and user experience in report outputs.

This commit is contained in:
erdemerikci 2026-06-18 16:34:52 +03:00
parent 7fd27ffed8
commit 8152e76d05
10 changed files with 898 additions and 3568 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
name,latitude,longitude,farm_name
WTG-01,39.854321,32.765432,Sample Wind Farm
WTG-02,39.855100,32.766200,Sample Wind Farm
WTG-03,39.855880,32.766968,Sample Wind Farm
1 name latitude longitude farm_name
2 WTG-01 39.854321 32.765432 Sample Wind Farm
3 WTG-02 39.855100 32.766200 Sample Wind Farm
4 WTG-03 39.855880 32.766968 Sample Wind Farm

View File

@ -31,6 +31,7 @@ _LIGHTNING_COLUMN_ALIASES: dict[str, list[str]] = {
"time",
"timestamp",
],
"ic_height": ["ic_height", "inCloudHeight", "in_cloud_height", "InCloudHeight"],
}
_TURBINE_COLUMN_ALIASES: dict[str, list[str]] = {
@ -158,6 +159,9 @@ def _build_lightning_df(
if "current_abs" not in df.columns:
df["current_abs"] = df["current"].abs()
if "ic_height" in df.columns:
df["ic_height"] = pd.to_numeric(df["ic_height"], errors="coerce")
return df

View File

@ -4,7 +4,7 @@ import io
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Iterable
from typing import Any, Iterable, Literal
import pandas as pd
import matplotlib
@ -163,6 +163,24 @@ def _add_paragraph(doc: Document, text: str, size_pt: int = 11, align: WD_ALIGN_
r.font.size = Pt(size_pt)
def _add_multiline_paragraph(
doc: Document,
text: str,
size_pt: int = 11,
align: WD_ALIGN_PARAGRAPH = WD_ALIGN_PARAGRAPH.LEFT,
) -> None:
lines = [line.strip() for line in text.splitlines() if line.strip()]
if not lines:
return
p = doc.add_paragraph()
p.alignment = align
for index, line in enumerate(lines):
if index > 0:
p.add_run().add_break()
run = p.add_run(line)
run.font.size = Pt(size_pt)
def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None:
for item in items:
p = doc.add_paragraph(style="List Bullet")
@ -499,6 +517,56 @@ def _fit_one_line_table_layout(
return font_pt, [w * scale for w in required]
def _add_detailed_strike_list_section(
doc: Document,
title: str,
description: str,
empty_message: str,
centroid_lat: float,
centroid_lng: float,
lightning_df: pd.DataFrame,
strike_type: Literal["cg", "ic"],
content_width: float,
lang,
) -> None:
_add_title(doc, title, size_pt=14)
_add_multiline_paragraph(doc, description, size_pt=10)
table_data, row_colors = build_lightning_event_table_data(
centroid_lat,
centroid_lng,
lightning_df,
lang,
strike_type=strike_type,
)
if len(table_data) <= 1:
_add_paragraph(doc, empty_message, size_pt=10)
return
table_data[0] = [
str(h).replace(" ", "\u00A0", 1) if " " in str(h) else str(h) for h in table_data[0]
]
available_width_cm = float(content_width) * 2.54
if strike_type == "ic":
min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.4, 1.8]
else:
min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.8]
font_pt, col_widths_cm = _fit_one_line_table_layout(
table_data=table_data,
available_width_cm=available_width_cm,
min_col_widths_cm=min_col_widths_cm,
max_font_pt=15,
min_font_pt=8,
)
_add_table(
doc,
table_data,
row_colors=row_colors,
column_widths_cm=col_widths_cm,
font_size_pt=float(font_pt),
autofit=False,
)
def _add_table(
doc: Document,
table_data: list[list[str]],
@ -813,7 +881,7 @@ def create_docx_report(
doc.add_page_break()
# Farm-wide maps + charts + risk table, anchored at the n8n-supplied centroid.
# Risk table, then farm-wide maps and charts anchored at the n8n-supplied centroid.
centroid_row = pd.Series({
"lat": centroid_lat,
"lng": centroid_lng,
@ -825,6 +893,13 @@ def create_docx_report(
has_cg = bool(((type_values == "0") & mask_within).any())
has_ic = bool(((type_values != "0") & mask_within).any())
risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang)
if risk_table_data and len(risk_table_data) > 1:
_add_title(doc, s.turbine_risk_assessment, size_pt=14)
_add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10)
_add_table(doc, risk_table_data, row_colors=risk_row_colors)
doc.add_page_break()
if has_cg:
_add_title(doc, s.cloud_to_ground_lightnings, size_pt=14)
if start_date and end_date:
@ -849,6 +924,18 @@ def create_docx_report(
)
if cg_chart is not None:
_add_image_from_bytes(doc, cg_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
_add_detailed_strike_list_section(
doc,
s.detailed_cg_events,
s.detailed_cg_events_desc,
s.detailed_cg_events_empty,
centroid_lat,
centroid_lng,
lightning_df,
"cg",
content_width,
lang,
)
doc.add_page_break()
if has_ic:
@ -875,13 +962,18 @@ def create_docx_report(
)
if ic_chart is not None:
_add_image_from_bytes(doc, ic_chart.to_image(format="png", width=1200, height=900, scale=2, engine="kaleido"), content_width)
doc.add_page_break()
risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang)
if risk_table_data and len(risk_table_data) > 1:
_add_title(doc, s.turbine_risk_assessment, size_pt=14)
_add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10)
_add_table(doc, risk_table_data, row_colors=risk_row_colors)
_add_detailed_strike_list_section(
doc,
s.detailed_ic_events,
s.detailed_ic_events_desc,
s.detailed_ic_events_empty,
centroid_lat,
centroid_lng,
lightning_df,
"ic",
content_width,
lang,
)
doc.add_page_break()
# Daily breakdown
@ -1081,30 +1173,6 @@ def create_docx_report(
)
doc.add_page_break()
# Farm-wide lightning event table, anchored at the n8n-supplied centroid.
_add_title(doc, s.detailed_lightning_data, size_pt=14)
table_data, row_colors = build_lightning_event_table_data(centroid_lat, centroid_lng, lightning_df, lang)
if table_data and table_data[0]:
table_data[0] = [str(h).replace(" ", "\u00A0", 1) if " " in str(h) else str(h) for h in table_data[0]]
available_width_cm = float(content_width) * 2.54
min_col_widths_cm = [1, 3.2, 1.6, 1.6, 1.9, 1.4, 2.0, 1.8]
font_pt, col_widths_cm = _fit_one_line_table_layout(
table_data=table_data,
available_width_cm=available_width_cm,
min_col_widths_cm=min_col_widths_cm,
max_font_pt=15,
min_font_pt=8,
)
_add_table(
doc,
table_data,
row_colors=row_colors,
column_widths_cm=col_widths_cm,
font_size_pt=float(font_pt),
autofit=False,
)
doc.add_page_break()
# Appendix
_add_title(doc, s.appendix, size_pt=16)
_add_title(doc, s.risk_calc_method, size_pt=14)

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Callable
from typing import Any, Callable, Literal
import numpy as np
import pandas as pd
@ -280,17 +280,45 @@ def build_lightning_event_table_data(
centroid_lng: float,
lightning_df: pd.DataFrame,
lang: ReportLanguage | None = None,
strike_type: Literal["cg", "ic", "all"] = "all",
) -> tuple[list[list[str]], list[str]]:
s = get_strings(lang or get_report_language())
include_type_column = strike_type == "all"
include_height_column = strike_type != "cg"
if strike_type == "cg":
source_df = lightning_df[lightning_df["p_type"].astype(str) == "0"].copy()
elif strike_type == "ic":
source_df = lightning_df[lightning_df["p_type"].astype(str) != "0"].copy()
else:
source_df = lightning_df.copy()
header = [
s.col_no,
s.col_time_local,
s.col_lat,
s.col_lng,
s.col_current_amps,
]
if include_height_column:
header.append(s.col_height_m)
if include_type_column:
header.append(s.col_lightning_type)
header.append(s.col_proximity_km)
if source_df.empty:
return [header], ["lightgrey"]
pre = precompute_distances_and_rings(
centroid_lat, centroid_lng, lightning_df, config.distance_rings
centroid_lat, centroid_lng, source_df, config.distance_rings
)
rows: list[list[str]] = []
row_colors: list[str] = []
outermost_km = max(config.distance_rings) / 1000.0
rings_km = [r / 1000.0 for r in config.distance_rings]
proximity_index = len(header) - 1
for i, rec in enumerate(lightning_df.itertuples(index=False)):
for i, rec in enumerate(source_df.itertuples(index=False)):
proximity = float(pre["dists_km"][i])
if proximity > outermost_km:
continue
@ -310,48 +338,43 @@ def build_lightning_event_table_data(
except Exception:
local_time = str(getattr(rec, "local_time", ""))[:19]
lightning_type = s.lightning_type_cg if str(rec.p_type) == "0" else s.lightning_type_ic
height_val = getattr(rec, "ic_height", "")
if height_val == "":
height_val = getattr(rec, "height", "")
rows.append(
[
row = [
"",
local_time,
f"{rec.lat:.5f}",
f"{rec.lng:.5f}",
str(rec.current),
str(height_val),
lightning_type,
f"{proximity:.2f}",
]
)
if include_height_column:
height_val = getattr(rec, "ic_height", "")
if height_val == "" or pd.isna(height_val):
height_val = getattr(rec, "inCloudHeight", "")
row.append("" if height_val == "" or pd.isna(height_val) else str(height_val))
if include_type_column:
lightning_type = s.lightning_type_cg if str(rec.p_type) == "0" else s.lightning_type_ic
row.append(lightning_type)
row.append(f"{proximity:.2f}")
rows.append(row)
row_colors.append(color)
if rows:
if include_type_column:
type_index = proximity_index - 1
sorted_data = sorted(
zip(rows, row_colors),
key=lambda x: (0 if x[0][6] == s.lightning_type_cg else 1, float(x[0][7])),
key=lambda x: (0 if x[0][type_index] == s.lightning_type_cg else 1, float(x[0][proximity_index])),
)
else:
sorted_data = sorted(
zip(rows, row_colors),
key=lambda x: float(x[0][proximity_index]),
)
if sorted_data:
rows, row_colors = zip(*sorted_data)
rows = list(rows)
row_colors = list(row_colors)
for idx, row in enumerate(rows):
row[0] = str(idx + 1)
else:
rows, row_colors = [], []
header = [
s.col_no,
s.col_time_local,
s.col_lat,
s.col_lng,
s.col_current_amps,
s.col_height_m,
s.col_lightning_type,
s.col_proximity_km,
]
return [header] + rows, ["lightgrey"] + row_colors

View File

@ -98,6 +98,12 @@ class ReportStrings:
storm_cells: str
storm_cells_daily_viz: str
detailed_lightning_data: str
detailed_cg_events: str
detailed_ic_events: str
detailed_cg_events_desc: str
detailed_ic_events_desc: str
detailed_cg_events_empty: str
detailed_ic_events_empty: str
appendix: str
risk_calc_method: str
how_risk_determined: str
@ -308,6 +314,19 @@ _STRINGS_EN = ReportStrings(
storm_cells="Storm Cells",
storm_cells_daily_viz="Daily storm cells visualization.",
detailed_lightning_data="Detailed Lightning Event Data",
detailed_cg_events="Detailed Cloud-to-Ground Lightning List",
detailed_ic_events="Detailed Intercloud Lightning List",
detailed_cg_events_desc=(
"Current (amps): peak current of the cloud-to-ground strike.\n"
"Proximity (km): air distance from the strike to the wind farm centroid."
),
detailed_ic_events_desc=(
"Current (amps): peak current of the in-cloud strike.\n"
"Height (m): in-cloud flash height.\n"
"Proximity (km): air distance from the strike to the wind farm centroid."
),
detailed_cg_events_empty="No cloud-to-ground lightning events were recorded within the analysis area during this period.",
detailed_ic_events_empty="No intercloud lightning events were recorded within the analysis area during this period.",
appendix="Appendix",
risk_calc_method="1. Risk Calculation Method",
how_risk_determined="How Risk Scores Are Determined:",
@ -328,14 +347,14 @@ _STRINGS_EN = ReportStrings(
max_risk_score="Maximum Risk Score: ~2.500 (close distance, high current)",
typical_range="Typical Range: 0.010 - 1.000 for most lightning events",
risk_categories="Risk Score Categories (Fixed Color Intervals):",
risk_cat_very_low="Very Low Risk (<0.1): Blue - Distant lightning with low current",
risk_cat_low="Low Risk (0.1-0.2): Teal - Moderate distance lightning",
risk_cat_med_low="Med-Low Risk (0.2-0.4): Green - Closer lightning",
risk_cat_medium="Medium Risk (0.4-0.6): Yellow - Moderate risk lightning",
risk_cat_med_high="Med-High Risk (0.6-0.8): Orange - High risk lightning",
risk_cat_high="High Risk (0.8-1.0): Dark Orange - Very high risk lightning",
risk_cat_very_high="Very High Risk (1.0-1.2): Red - Extreme risk lightning",
risk_cat_critical="Critical Risk (>1.2): Dark Red - Critical risk lightning",
risk_cat_very_low="Very Low Risk (<0.1): Blue",
risk_cat_low="Low Risk (0.1-0.2): Teal",
risk_cat_med_low="Med-Low Risk (0.2-0.4): Green",
risk_cat_medium="Medium Risk (0.4-0.6): Yellow",
risk_cat_med_high="Med-High Risk (0.6-0.8): Orange",
risk_cat_high="High Risk (0.8-1.0): Dark Orange",
risk_cat_very_high="Very High Risk (1.0-1.2): Red",
risk_cat_critical="Critical Risk (>1.2): Dark Red",
risk_chart="3. Risk Score Calculation Chart",
chart_reference="Chart Reference Guide:",
risk_chart_desc_1="The following chart shows how distance and current magnitude affect risk scores.",
@ -527,6 +546,19 @@ _STRINGS_TR = ReportStrings(
storm_cells="Fırtına Hücreleri",
storm_cells_daily_viz="Fırtına hücrelerinin dağılımı",
detailed_lightning_data="Ayrıntılı Yıldırım Olay Verileri",
detailed_cg_events="Ayrıntılı Yıldırım Listesi",
detailed_ic_events="Ayrıntılı Şimşek Listesi",
detailed_cg_events_desc=(
"Akım (amper): yıldırımın akım büyüklüğü.\n"
"Yakınlık (km): yıldırımın rüzgar çiftliğinin merkez noktasına (centroid) olan kuş uçuşu mesafesi."
),
detailed_ic_events_desc=(
"Akım (amper): şimşeğin akım büyüklüğü.\n"
"Yükseklik (m): şimşeğin oluştuğu yükseklik.\n"
"Yakınlık (km): şimşeğin rüzgar çiftliğinin merkez noktasına olan kuş uçuşu mesafesi."
),
detailed_cg_events_empty="Bu dönemde analiz alanında yıldırım olayı kaydedilmemiştir.",
detailed_ic_events_empty="Bu dönemde analiz alanında şimşek olayı kaydedilmemiştir.",
appendix="Ek",
risk_calc_method="1. Risk Hesaplama Yöntemi",
how_risk_determined="Risk Skorları Nasıl Belirlenir:",
@ -547,14 +579,14 @@ _STRINGS_TR = ReportStrings(
max_risk_score="Maksimum Risk Skoru: ~2.500 (yakın mesafe, yüksek akım)",
typical_range="Tipik Aralık: çoğu yıldırım olayı için 0.010 - 1.000",
risk_categories="Risk Skoru Kategorileri (Sabit Renk Aralıkları):",
risk_cat_very_low="Çok Düşük Risk (<0.1): Mavi - Uzak, düşük akımlı yıldırım",
risk_cat_low="Düşük Risk (0.1-0.2): Turkuaz - Orta mesafeli yıldırım",
risk_cat_med_low="Orta-Düşük Risk (0.2-0.4): Yeşil - Daha yakın yıldırım",
risk_cat_medium="Orta Risk (0.4-0.6): Sarı - Orta riskli yıldırım",
risk_cat_med_high="Orta-Yüksek Risk (0.6-0.8): Turuncu - Yüksek riskli yıldırım",
risk_cat_high="Yüksek Risk (0.8-1.0): Koyu Turuncu - Çok yüksek riskli yıldırım",
risk_cat_very_high="Çok Yüksek Risk (1.0-1.2): Kırmızı - Aşırı riskli yıldırım",
risk_cat_critical="Kritik Risk (>1.2): Koyu Kırmızı - Kritik riskli yıldırım",
risk_cat_very_low="Çok düşük risk (<0.1): Mavi",
risk_cat_low="Düşük Risk (0.1-0.2): Turkuaz",
risk_cat_med_low="Orta-Düşük Risk (0.2-0.4): Yeşil",
risk_cat_medium="Orta Risk (0.4-0.6): Sarı",
risk_cat_med_high="Orta-Yüksek Risk (0.6-0.8): Turuncu",
risk_cat_high="Yüksek Risk (0.8-1.0): Koyu Turuncu",
risk_cat_very_high="Çok Yüksek Risk (1.0-1.2): Kırmızı",
risk_cat_critical="Kritik Risk (>1.2): Koyu Kırmızı",
risk_chart="3. Risk Skoru Hesaplama Grafiği",
chart_reference="Grafik Referans Kılavuzu:",
risk_chart_desc_1="Aşağıdaki grafik, mesafe ve akım büyüklüğünün risk skorlarını nasıl etkilediğini gösterir.",

View File

@ -11,6 +11,8 @@ COORDINATE_PLANE_RING_LINE_WIDTH = 4
COORDINATE_PLANE_LIGHTNING_SIZE_MIN = 10
COORDINATE_PLANE_LIGHTNING_SIZE_MAX = 24
COORDINATE_PLANE_LIGHTNING_CURRENT_SCALE = 800
COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE = 9
COORDINATE_PLANE_TURBINE_TEXT_POSITION = 'middle center'
def plot_turbine_map(turbine_row: pd.Series, lightning_df: pd.DataFrame, turbine_df: pd.DataFrame) -> go.Figure:
turbine_lat = turbine_row['lat']
@ -371,8 +373,8 @@ def plot_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.DataFrame, tu
line=dict(color='black', width=1)
),
text=turbine_df['name'].tolist(),
textfont=dict(size=18, color='black'), # turbine name label font size
textposition='middle center',
textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'),
textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION,
name='Wind Turbines',
showlegend=True
))
@ -394,7 +396,7 @@ def plot_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.DataFrame, tu
line=dict(color='black', width=1)
),
text=['S'] * len(storm_df),
textfont=dict(size=18, color='white'), # storm "S" label font size
textfont=dict(size=18, color='white'),
textposition='middle center',
name='Storm Cells',
showlegend=True
@ -521,8 +523,8 @@ def plot_intercloud_coordinate_plane(turbine_row: pd.Series, lightning_df: pd.Da
line=dict(color='black', width=1)
),
text=turbine_df['name'].tolist(),
textfont=dict(size=24, color='black'),
textposition='middle center',
textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'),
textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION,
name=s.map_wind_turbines,
showlegend=True
))
@ -694,8 +696,8 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
line=dict(color='black', width=1)
),
text=turbine_df['name'].tolist(),
textfont=dict(size=18, color='black'), # turbine name label font size
textposition='middle center',
textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'),
textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION,
name=s.map_wind_turbines,
showlegend=True
))
@ -717,7 +719,7 @@ def plot_cloud_to_ground_coordinate_plane(turbine_row: pd.Series, lightning_df:
line=dict(color='black', width=1)
),
text=['S'] * len(storm_df),
textfont=dict(size=18, color='white'), # storm "S" label font size
textfont=dict(size=18, color='white'),
textposition='middle center',
name=s.map_storm_cells,
showlegend=True

View File

@ -13,6 +13,10 @@ from src.config import config
from src.reporting.strings import ReportLanguage, get_report_language, get_strings
from src.utils import parse_period_string_to_datetime
from src.visualization.basemap import add_satellite_basemap
from src.visualization.maps import (
COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE,
COORDINATE_PLANE_TURBINE_TEXT_POSITION,
)
def format_datetime_for_display(datetime_str: str) -> str:
"""
@ -177,8 +181,8 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
line=dict(color='black', width=1)
),
text=turbine_df['name'].tolist(),
textfont=dict(size=12, color='black'),
textposition='middle center',
textfont=dict(size=COORDINATE_PLANE_TURBINE_NAME_FONT_SIZE, color='black'),
textposition=COORDINATE_PLANE_TURBINE_TEXT_POSITION,
name=s.map_wind_turbines,
showlegend=True,
hovertemplate=(