Enhance timestamp handling in report generation by adding support for flexible date formats and local time conversion based on configuration. Refactor storm cell visualization functions to improve clarity and accuracy in displaying storm data. Update report strings for better user understanding and localization.

This commit is contained in:
erdemerikci 2026-06-10 16:57:58 +03:00
parent 29622b48fb
commit a470b417aa
6 changed files with 179 additions and 100 deletions

View File

@ -30,6 +30,7 @@ from src.reporting.docx_sections import (
build_turbine_information_table_data, build_turbine_information_table_data,
) )
from src.utils import ( from src.utils import (
filter_lightning_data_by_date_range,
format_datetime_ddmmyyyy_hhmm, format_datetime_ddmmyyyy_hhmm,
format_datetime_to_local_display, format_datetime_to_local_display,
get_utc_offset_label, get_utc_offset_label,
@ -169,6 +170,11 @@ def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None
r.font.size = Pt(size_pt) r.font.size = Pt(size_pt)
def _storm_severity_label(severity: str, s) -> str:
key = str(severity).strip().lower()
return s.storm_severity_names.get(key, str(severity).strip().title() or s.unknown)
def _build_key_findings( def _build_key_findings(
lightning_df: pd.DataFrame, lightning_df: pd.DataFrame,
turbine_df: pd.DataFrame, turbine_df: pd.DataFrame,
@ -248,7 +254,7 @@ def _build_key_findings(
if nearest_km is not None and nearest_turbine is not None: if nearest_km is not None and nearest_turbine is not None:
bullets.append(s.kf_nearest_strike.format(distance=nearest_km, name=nearest_turbine)) bullets.append(s.kf_nearest_strike.format(distance=nearest_km, name=nearest_turbine))
if turbine_count > 0: if turbine_count > 0:
bullets.append(s.kf_high_risk_turbines.format(count=high_plus_count, total=turbine_count)) bullets.append(s.kf_high_risk_turbines.format(count=high_plus_count))
if active_days > 1: if active_days > 1:
bullets.append(s.kf_active_days.format(days=active_days)) bullets.append(s.kf_active_days.format(days=active_days))
if storm_cell_count > 0 and storm_closest_to_centroid_km is not None: if storm_cell_count > 0 and storm_closest_to_centroid_km is not None:
@ -612,6 +618,9 @@ def create_docx_report(
storm_data = filter_storm_data_by_date_range(storm_data, start_date, end_date) storm_data = filter_storm_data_by_date_range(storm_data, start_date, end_date)
storm_data = filter_storm_data_by_turbine_proximity(storm_data, turbine_df) storm_data = filter_storm_data_by_turbine_proximity(storm_data, turbine_df)
if start_date or end_date:
lightning_df = filter_lightning_data_by_date_range(lightning_df, start_date, end_date)
turbine_df = calculate_turbine_risks(turbine_df, lightning_df) turbine_df = calculate_turbine_risks(turbine_df, lightning_df)
stats = calculate_lightning_statistics(lightning_df, centroid_lat, centroid_lng, start_date, end_date) stats = calculate_lightning_statistics(lightning_df, centroid_lat, centroid_lng, start_date, end_date)
histogram_figs = create_lightning_histogram_pages(lightning_df, centroid_lat, centroid_lng) histogram_figs = create_lightning_histogram_pages(lightning_df, centroid_lat, centroid_lng)
@ -977,6 +986,15 @@ def create_docx_report(
_add_paragraph(doc, s.storm_cells_overview, size_pt=11, bold=True) _add_paragraph(doc, s.storm_cells_overview, size_pt=11, bold=True)
_add_paragraph(doc, s.storm_cells_overview_1, size_pt=10) _add_paragraph(doc, s.storm_cells_overview_1, size_pt=10)
_add_paragraph(doc, s.storm_cells_overview_2, size_pt=10) _add_paragraph(doc, s.storm_cells_overview_2, size_pt=10)
_add_bullets(
doc,
[
s.storm_cells_severity_legend_low,
s.storm_cells_severity_legend_medium,
s.storm_cells_severity_legend_high,
],
size_pt=10,
)
summary = create_storm_cells_summary(storm_data) summary = create_storm_cells_summary(storm_data)
_add_paragraph(doc, s.storm_cells_summary, size_pt=11, bold=True) _add_paragraph(doc, s.storm_cells_summary, size_pt=11, bold=True)
@ -986,21 +1004,16 @@ def create_docx_report(
_add_paragraph(doc, s.severity_breakdown, size_pt=10, bold=True) _add_paragraph(doc, s.severity_breakdown, size_pt=10, bold=True)
_add_bullets( _add_bullets(
doc, doc,
[s.severity_cells.format(severity=severity, count=count) for severity, count in severity_counts.items()], [
s.severity_cells.format(
severity=_storm_severity_label(severity, s),
count=count,
)
for severity, count in severity_counts.items()
],
size_pt=10, size_pt=10,
) )
daily_breakdown: dict[str, int] = summary.get("daily_breakdown", {}) or {}
_add_paragraph(doc, s.daily_storm_breakdown, size_pt=10, bold=True)
sorted_days = sorted(daily_breakdown.keys())
daily_items: list[str] = []
for day in sorted_days[:10]:
daily_items.append(s.daily_storm_item.format(day=day, count=daily_breakdown.get(day, 0)))
if len(sorted_days) > 10:
daily_items.append(s.more_days.format(n=len(sorted_days) - 10))
if daily_items:
_add_bullets(doc, daily_items, size_pt=10)
_add_paragraph(doc, s.complete_storm_list, size_pt=10, bold=True) _add_paragraph(doc, s.complete_storm_list, size_pt=10, bold=True)
def _storm_effective_time(storm: dict[str, Any]) -> datetime: def _storm_effective_time(storm: dict[str, Any]) -> datetime:
@ -1027,15 +1040,16 @@ def create_docx_report(
effective_formatted = format_datetime_to_local_display(effective_raw, report_tz) effective_formatted = format_datetime_to_local_display(effective_raw, report_tz)
expire_formatted = format_datetime_to_local_display(expire_raw, report_tz) expire_formatted = format_datetime_to_local_display(expire_raw, report_tz)
raw_severity = storm.get("lightning_severity", s.unknown)
table_data.append( table_data.append(
[ [
str(i), str(i),
str(storm.get("lightning_severity", s.unknown)), _storm_severity_label(str(raw_severity), s),
effective_formatted, effective_formatted,
expire_formatted, expire_formatted,
] ]
) )
severity = str(storm.get("lightning_severity", s.unknown) or s.unknown).strip().lower() severity = str(raw_severity or s.unknown).strip().lower()
if severity == "high": if severity == "high":
row_colors.append("purple") row_colors.append("purple")
elif severity == "medium": elif severity == "medium":

View File

@ -11,36 +11,69 @@ from src.reporting.precompute import precompute_distances_and_rings
from src.reporting.strings import ReportLanguage, get_report_language, get_strings from src.reporting.strings import ReportLanguage, get_report_language, get_strings
def _parse_chart_times(values: pd.Series) -> pd.Series: def _parse_epoch_to_farm_local(numeric: pd.Series, tz_name: str | None) -> pd.Series:
def _to_local_naive(ts: pd.Series) -> pd.Series:
if config.timezone:
try:
ts = ts.dt.tz_convert(config.timezone)
except Exception:
pass
try:
return ts.dt.tz_localize(None)
except Exception:
return ts
numeric = pd.to_numeric(values, errors="coerce")
numeric_valid = numeric.dropna() numeric_valid = numeric.dropna()
if len(numeric_valid) == 0:
return pd.to_datetime(numeric, errors="coerce")
if len(numeric_valid) > 0 and len(numeric_valid) >= max(1, int(len(values) * 0.7)): abs_max = float(numeric_valid.abs().max())
abs_max = float(numeric_valid.abs().max()) if abs_max >= 1e17:
if abs_max >= 1e17: unit = "ns"
unit = "ns" elif abs_max >= 1e14:
elif abs_max >= 1e14: unit = "us"
unit = "us" elif abs_max >= 1e11:
elif abs_max >= 1e11: unit = "ms"
unit = "ms" else:
unit = "s"
parsed = pd.to_datetime(numeric, errors="coerce", unit=unit, utc=True)
if getattr(parsed.dt, "tz", None) is not None and tz_name:
parsed = parsed.dt.tz_convert(tz_name)
return parsed.dt.tz_localize(None) if getattr(parsed.dt, "tz", None) is not None else parsed
def _farm_local_naive_times(values: pd.Series) -> pd.Series:
"""Normalize strike timestamps to naive farm-local wall clock."""
tz_name = config.timezone
if pd.api.types.is_datetime64_any_dtype(values):
parsed = pd.to_datetime(values, errors="coerce")
elif pd.api.types.is_numeric_dtype(values):
parsed = _parse_epoch_to_farm_local(values, tz_name)
else:
numeric = pd.to_numeric(values, errors="coerce")
numeric_valid = numeric.dropna()
if len(numeric_valid) > 0 and len(numeric_valid) >= max(1, int(len(values) * 0.7)):
parsed = _parse_epoch_to_farm_local(numeric, tz_name)
else: else:
unit = "s" parsed = pd.to_datetime(values, errors="coerce")
parsed = pd.to_datetime(numeric, errors="coerce", unit=unit, utc=True)
return _to_local_naive(parsed)
parsed = pd.to_datetime(values, errors="coerce", utc=True) if getattr(parsed.dt, "tz", None) is None:
return _to_local_naive(parsed) return parsed
if tz_name:
parsed = parsed.dt.tz_convert(tz_name)
return parsed.dt.tz_localize(None)
def _chart_xaxis_range(time_values: np.ndarray) -> tuple[pd.Timestamp, pd.Timestamp]:
start_label = getattr(config, "analysis_start_date", None)
end_label = getattr(config, "analysis_end_date", None)
if start_label and end_label:
try:
return (
pd.to_datetime(str(start_label).strip(), format="%d-%m-%Y %H:%M"),
pd.to_datetime(str(end_label).strip(), format="%d-%m-%Y %H:%M"),
)
except ValueError:
pass
times = pd.to_datetime(time_values)
x_min = pd.Timestamp(times.min())
x_max = pd.Timestamp(times.max())
span = x_max - x_min
padding = max(pd.Timedelta(minutes=1), span * 0.1)
if span == pd.Timedelta(0):
padding = pd.Timedelta(minutes=2)
return x_min - padding, x_max + padding
def _build_current_vs_distance_chart( def _build_current_vs_distance_chart(
@ -67,17 +100,17 @@ def _build_current_vs_distance_chart(
distances = dists_km[combined_mask] distances = dists_km[combined_mask]
currents = subset["current"].values.astype(float) currents = subset["current"].values.astype(float)
time_series = _parse_chart_times(subset["local_time"]) display_times = _farm_local_naive_times(subset["local_time"])
valid_mask = ~time_series.isna() valid_mask = ~display_times.isna()
if valid_mask.sum() == 0: if valid_mask.sum() == 0:
return None return None
time_series = time_series.loc[valid_mask] display_times = display_times.loc[valid_mask]
distances = distances[valid_mask.values] distances = distances[valid_mask.values]
currents = currents[valid_mask.values] currents = currents[valid_mask.values]
sort_idx = np.argsort(time_series.values.astype("datetime64[ns]")) sort_idx = np.argsort(display_times.values.astype("datetime64[ns]"))
time_values = time_series.values[sort_idx] time_values = display_times.values[sort_idx]
distances = distances[sort_idx] distances = distances[sort_idx]
currents = currents[sort_idx] currents = currents[sort_idx]
@ -124,6 +157,14 @@ def _build_current_vs_distance_chart(
) )
timezone_label = config.timezone or "UTC" timezone_label = config.timezone or "UTC"
x_min, x_max = _chart_xaxis_range(time_values)
span_minutes = max(1.0, (x_max - x_min).total_seconds() / 60.0)
if span_minutes <= 15:
dtick = 60 * 1000
elif span_minutes <= 60:
dtick = 5 * 60 * 1000
else:
dtick = 10 * 60 * 1000
fig.update_layout( fig.update_layout(
font=dict(size=16), font=dict(size=16),
@ -137,8 +178,9 @@ def _build_current_vs_distance_chart(
showgrid=True, showgrid=True,
gridcolor="lightgray", gridcolor="lightgray",
zeroline=False, zeroline=False,
range=[x_min, x_max],
tickformat="%d-%m-%Y %H:%M", tickformat="%d-%m-%Y %H:%M",
nticks=6, dtick=dtick,
tickangle=-25, tickangle=-25,
tickfont=dict(size=22), tickfont=dict(size=22),
title_font=dict(size=28), title_font=dict(size=28),
@ -167,7 +209,6 @@ def _build_current_vs_distance_chart(
height=fig_height, height=fig_height,
margin=dict(l=70, r=40, t=50, b=130), margin=dict(l=70, r=40, t=50, b=130),
) )
return fig return fig

View File

@ -149,7 +149,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
+ (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available") + (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available")
+ "\n\n" + "\n\n"
"Paragraf gereksinimleri:\n" "Paragraf gereksinimleri:\n"
"- İlk cümlede analiz dönemini, toplam olay sayısını, analiz yarıçapını ve yıldırım yoğunluğunu (olay/km²) belirt.\n" "- İlk cümlede analiz dönemini \"{başlangıç} - {bitiş} arasında\" biçiminde ver; \"döneminde\" kullanma.\n"
"- İlk cümlede toplam olay sayısını (\"toplam X olay\"), yarıçapı (\"X km yarıçaplı alanda\") ve yıldırım-şimşek yoğunluğunu (olay/km²) tek cümlede birleştir; \"yıldırım olayı\" veya \"analiz alanında\" deme.\n"
"- Mesafe halkası dağılımından bir önemli çıkarım ekle; olayların hangi halkada yoğunlaştığınııkça söyle.\n" "- Mesafe halkası dağılımından bir önemli çıkarım ekle; olayların hangi halkada yoğunlaştığınııkça söyle.\n"
"- is_single_turbine_report true ise: \"{top_turbine_name} türbini için log-risk skoru ... sınıfındadır\" gibi doğal bir ifade kullan.\n" "- is_single_turbine_report true ise: \"{top_turbine_name} türbini için log-risk skoru ... sınıfındadır\" gibi doğal bir ifade kullan.\n"
"- is_single_turbine_report false ise: analiz alanında en yüksek riskli türbini ve risk sınıfını belirt.\n" "- is_single_turbine_report false ise: analiz alanında en yüksek riskli türbini ve risk sınıfını belirt.\n"
@ -161,8 +162,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
"- Ton analitik, net ve alarmist olmayan olsun.\n" "- Ton analitik, net ve alarmist olmayan olsun.\n"
"\n" "\n"
"Örnek üslup (yalnızca stil rehberi; sayıları kopyalama):\n" "Örnek üslup (yalnızca stil rehberi; sayıları kopyalama):\n"
"\"03-05-2026 10:4303-05-2026 10:43 döneminde 9,0 km yarıçaplı analiz alanında toplam 2 yıldırım olayı kaydedilmiştir. " "\"03-05-2026 10:4303-05-2026 10:43 arasında 9,0 km yarıçaplı alanda toplam 2 olay kaydedilmiştir ve yıldırım-şimşek yoğunluğu 0,008 olay/km² olarak hesaplanmıştır. "
"Yıldırım yoğunluğu 0,008 olay/km² olarak hesaplanmıştır. Yıldırım olayları 1,03,0 km halkasında (2 yıldırım, 0 şimşek) yoğunlaşmıştır. " "Yıldırım olayları 1,03,0 km halkasında (2 yıldırım, 0 şimşek) yoğunlaşmıştır. "
"T5 türbini için log-risk skoru Düşük Risk sınıfındadır. Bu raporda fırtına hücresi verisi bulunmamaktadır.\"\n" "T5 türbini için log-risk skoru Düşük Risk sınıfındadır. Bu raporda fırtına hücresi verisi bulunmamaktadır.\"\n"
"\n" "\n"
"Çıktı:\n" "Çıktı:\n"
@ -276,15 +277,15 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
if s.gemini_write_turkish: if s.gemini_write_turkish:
radius_txt = ( radius_txt = (
f"{analysis_radius_km:.1f} km yarıçaplı analiz alanında" f"{analysis_radius_km:.1f} km yarıçaplı alanda"
if isinstance(analysis_radius_km, (int, float)) if isinstance(analysis_radius_km, (int, float))
else "analiz alanında" else "alanda"
) )
events_txt = f"toplam {total_events} yıldırım olayı" if total_events is not None else "yıldırım olayı" events_txt = f"toplam {total_events} olay" if total_events is not None else "olay"
ring_sentence = _build_ring_distribution_sentence_tr(best_ring, outermost_ring, s) ring_sentence = _build_ring_distribution_sentence_tr(best_ring, outermost_ring, s)
paragraph_intro = ( paragraph_intro = (
f"{analysis_period} döneminde {radius_txt} {events_txt} tespit edilmiştir. " f"{analysis_period} arasında {radius_txt} {events_txt} kaydedilmiştir ve "
f"Yıldırım yoğunluğu {density_txt} olarak hesaplanmıştır. " f"yıldırım-şimşek yoğunluğu {density_txt} olarak hesaplanmıştır. "
f"{ring_sentence} " f"{ring_sentence} "
) )
turbine_sentence = ( turbine_sentence = (

View File

@ -82,6 +82,9 @@ class ReportStrings:
storm_cells_overview: str storm_cells_overview: str
storm_cells_overview_1: str storm_cells_overview_1: str
storm_cells_overview_2: str storm_cells_overview_2: str
storm_cells_severity_legend_low: str
storm_cells_severity_legend_medium: str
storm_cells_severity_legend_high: str
storm_cells_summary: str storm_cells_summary: str
total_storm_cells: str total_storm_cells: str
severity_breakdown: str severity_breakdown: str
@ -250,7 +253,7 @@ _STRINGS_EN = ReportStrings(
kf_peak_activity="Peak activity: {when} ({count} events in one minute)", kf_peak_activity="Peak activity: {when} ({count} events in one minute)",
kf_strongest_strike="Strongest cloud-to-ground strike: {ka:.1f} kA", kf_strongest_strike="Strongest cloud-to-ground strike: {ka:.1f} kA",
kf_nearest_strike="Nearest cloud-to-ground strike to a turbine: {distance:.1f} km ({name})", kf_nearest_strike="Nearest cloud-to-ground strike to a turbine: {distance:.1f} km ({name})",
kf_high_risk_turbines="Turbines at High risk or above: {count} of {total}", kf_high_risk_turbines="High-risk turbines: {count}",
kf_active_days="Days with recorded activity: {days}", kf_active_days="Days with recorded activity: {days}",
kf_storm_distance="Storm cells detected: {count} (closest {distance:.1f} km from the centroid)", kf_storm_distance="Storm cells detected: {count} (closest {distance:.1f} km from the centroid)",
recommendation_heading="Recommendation", recommendation_heading="Recommendation",
@ -287,8 +290,11 @@ _STRINGS_EN = ReportStrings(
frequent_lightning_periods="Frequent Lightning Activity Periods", frequent_lightning_periods="Frequent Lightning Activity Periods",
storm_cells_analysis_summary="Storm Cells Analysis Summary", storm_cells_analysis_summary="Storm Cells Analysis Summary",
storm_cells_overview="Storm Cells Overview:", storm_cells_overview="Storm Cells Overview:",
storm_cells_overview_1="The following pages show storm cell boundaries organized by day during the analysis period.", storm_cells_overview_1="The following pages show storm cell boundaries observed within the analyzed area.",
storm_cells_overview_2="Each cell represents a storm system with defined boundaries and storm severity levels.", storm_cells_overview_2="Each polygon shows the area affected by a storm system. Polygons in different colors represent storm severity.",
storm_cells_severity_legend_low="Green polygon: low severity",
storm_cells_severity_legend_medium="Orange polygon: medium severity",
storm_cells_severity_legend_high="Purple polygon: high severity",
storm_cells_summary="Storm Cells Summary:", storm_cells_summary="Storm Cells Summary:",
total_storm_cells="Total storm cells: {count}", total_storm_cells="Total storm cells: {count}",
severity_breakdown="Severity breakdown:", severity_breakdown="Severity breakdown:",
@ -446,7 +452,7 @@ _STRINGS_EN = ReportStrings(
) )
_STRINGS_TR = ReportStrings( _STRINGS_TR = ReportStrings(
cover_title="Yıldırım Aktivite Raporu", cover_title="Yıldırım-Şimşek Aktivite Raporu",
analyzed_period="Analiz Dönemi:", analyzed_period="Analiz Dönemi:",
analyzed_period_local="Analiz Dönemi (yerel saat):", analyzed_period_local="Analiz Dönemi (yerel saat):",
centroid_coordinates="Merkez Koordinatları:", centroid_coordinates="Merkez Koordinatları:",
@ -466,7 +472,7 @@ _STRINGS_TR = ReportStrings(
kf_peak_activity="En yoğun an: {when} (bir dakikada {count} olay)", kf_peak_activity="En yoğun an: {when} (bir dakikada {count} olay)",
kf_strongest_strike="En güçlü yıldırım darbesi: {ka:.1f} kA", kf_strongest_strike="En güçlü yıldırım darbesi: {ka:.1f} kA",
kf_nearest_strike="Bir türbine en yakın yıldırım: {distance:.1f} km ({name})", kf_nearest_strike="Bir türbine en yakın yıldırım: {distance:.1f} km ({name})",
kf_high_risk_turbines="Yüksek ve üzeri riskli türbin: {total} türbinden {count} adet", kf_high_risk_turbines="Yüksek riskli türbin: {count} adet",
kf_active_days="Aktivite görülen gün sayısı: {days}", kf_active_days="Aktivite görülen gün sayısı: {days}",
kf_storm_distance="Tespit edilen fırtına hücresi: {count} (merkeze en yakın {distance:.1f} km)", kf_storm_distance="Tespit edilen fırtına hücresi: {count} (merkeze en yakın {distance:.1f} km)",
recommendation_heading="Öneri", recommendation_heading="Öneri",
@ -503,8 +509,11 @@ _STRINGS_TR = ReportStrings(
frequent_lightning_periods="Sık Yıldırım Aktivite Dönemleri", frequent_lightning_periods="Sık Yıldırım Aktivite Dönemleri",
storm_cells_analysis_summary="Fırtına Hücreleri Analiz Özeti", storm_cells_analysis_summary="Fırtına Hücreleri Analiz Özeti",
storm_cells_overview="Fırtına Hücreleri Genel Bakış:", storm_cells_overview="Fırtına Hücreleri Genel Bakış:",
storm_cells_overview_1="Aşağıdaki sayfalar, analiz dönemi boyunca günlere göre düzenlenmiş fırtına hücresi sınırlarını gösterir.", storm_cells_overview_1="Aşağıdaki sayfalar analiz edilen bölgede gözlemlenen fırtına hücresi sınırlarını gösterir.",
storm_cells_overview_2="Her hücre, tanımlı sınırları ve fırtına şiddet seviyeleri olan bir fırtına sistemini temsil eder.", storm_cells_overview_2="Her poligon bir fırtına sisteminin etki ettiği alanı gösterir. Farklı renkte poligonlar ise fırtınanın şiddetini temsil eder.",
storm_cells_severity_legend_low="Yeşil poligon: düşük şiddet",
storm_cells_severity_legend_medium="Turuncu poligon: orta şiddet",
storm_cells_severity_legend_high="Mor poligon: yüksek şiddet",
storm_cells_summary="Fırtına Hücreleri Özeti:", storm_cells_summary="Fırtına Hücreleri Özeti:",
total_storm_cells="Toplam fırtına hücresi: {count}", total_storm_cells="Toplam fırtına hücresi: {count}",
severity_breakdown="Şiddet dağılımı:", severity_breakdown="Şiddet dağılımı:",
@ -516,7 +525,7 @@ _STRINGS_TR = ReportStrings(
more_days="... ve {n} gün daha", more_days="... ve {n} gün daha",
complete_storm_list="Tam Fırtına Hücreleri Listesi:", complete_storm_list="Tam Fırtına Hücreleri Listesi:",
storm_cells="Fırtına Hücreleri", storm_cells="Fırtına Hücreleri",
storm_cells_daily_viz="Günlük fırtına hücreleri görselleştirmesi.", 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_lightning_data="Ayrıntılı Yıldırım Olay Verileri",
appendix="Ek", appendix="Ek",
risk_calc_method="1. Risk Hesaplama Yöntemi", risk_calc_method="1. Risk Hesaplama Yöntemi",
@ -524,13 +533,13 @@ _STRINGS_TR = ReportStrings(
risk_exposure_1="Bir türbinin risk skoru, analiz dönemi boyunca buluttan yere düşen yıldırımlara ne ölçüde maruz kaldığını gösterir.", risk_exposure_1="Bir türbinin risk skoru, analiz dönemi boyunca buluttan yere düşen yıldırımlara ne ölçüde maruz kaldığını gösterir.",
risk_exposure_2="Her yıldırım darbesi, türbine olan mesafesine ve akım büyüklüğüne bağlı olarak bir risk katkısı sağlar; bu katkıların tümü toplanarak skor elde edilir.", risk_exposure_2="Her yıldırım darbesi, türbine olan mesafesine ve akım büyüklüğüne bağlı olarak bir risk katkısı sağlar; bu katkıların tümü toplanarak skor elde edilir.",
p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçek katsayısı", p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçek katsayısı",
current_weight_desc="current_weight: akım büyüklüğünün riski ne kadar artırdığını kontrol eder (büyük = |I| üzerinde daha fazla ağırlık)", current_weight_desc="current_weight: akım büyüklüğünün riski ne kadar artırdığını kontrol eder.",
distance_desc="Mesafe (d): türbin-darbe mesafesi (kilometre)", distance_desc="Mesafe (d): türbin-darbe mesafesi (kilometre)",
current_desc="Akım (|I|): mutlak akım büyüklüğü |I| (amper)", current_desc="Akım (|I|): mutlak akım büyüklüğü |I| (amper)",
alpha_desc="α (mesafe azalma faktörü): riskin mesafeyle nasıl azaldığını kontrol eder (küçük = daha yavaş azalma)", alpha_desc="α (mesafe azalma faktörü): riskin mesafeyle nasıl azaldığını kontrol eder.",
included_events="Dahil edilen olaylar: yalnızca buluttan yere düşen yıldırımlar (p_type = 0)", included_events="Dahil edilen olaylar: yalnızca buluttan yere düşen yıldırımlar (p_type = 0)",
per_strike_contribution="Darbe başına katkı: |I| ile artar, mesafe ile üstel olarak azalır", per_strike_contribution="Darbe başına katkı: |I| ile artar, mesafe ile üstel olarak azalır",
turbine_risk_sum="Türbin risk skoru: en dış mesafe halkası içindeki tüm yıldırım (buluttan yere) darbelerinin katkılarının toplamıdır", turbine_risk_sum="Türbin risk skoru: en dış mesafe halkası içindeki tüm yıldırım darbelerinin katkılarının toplamıdır",
log_transformed="Görselleştirme ve raporlama için log dönüşümlü skor kullanılır", log_transformed="Görselleştirme ve raporlama için log dönüşümlü skor kullanılır",
risk_interpretation="2. Risk Skoru Yorumu", risk_interpretation="2. Risk Skoru Yorumu",
understanding_risk="Risk Skoru Değerlerini Anlama:", understanding_risk="Risk Skoru Değerlerini Anlama:",
@ -558,9 +567,9 @@ _STRINGS_TR = ReportStrings(
centroid_bullet_2="Mesafe halkaları çiftlik merkezinden çizilir; en iç halkalar en yüksek yakınlık riskini temsil eder", centroid_bullet_2="Mesafe halkaları çiftlik merkezinden çizilir; en iç halkalar en yüksek yakınlık riskini temsil eder",
centroid_bullet_3="Her darbenin yakınlığı, çiftlik merkezinden Haversine formülü ile ölçülür", centroid_bullet_3="Her darbenin yakınlığı, çiftlik merkezinden Haversine formülü ile ölçülür",
centroid_bullet_4="Analiz, en dış mesafe halkası ile sınırlıdır; bu halkanın dışında kalan darbeler tüm istatistik ve grafiklerin dışında bırakılır", centroid_bullet_4="Analiz, en dış mesafe halkası ile sınırlıdır; bu halkanın dışında kalan darbeler tüm istatistik ve grafiklerin dışında bırakılır",
freq_lightning_algo="5. Sık Yıldırım Aktivite Dönemi Tespit Algoritması", freq_lightning_algo="5. Sık Yıldırım-Şimşek Aktivite Dönemi Tespit Algoritması",
how_period_timespans="Dönem Zaman Aralıkları Nasıl Belirlenir:", how_period_timespans="Dönem Zaman Aralıkları Nasıl Belirlenir:",
gap_based_algo="Algoritma, yoğun yıldırım aktivitesi dönemlerini belirlemek için olaylar arasındaki zaman boşluklarına dayalı bir yaklaşım kullanır.", gap_based_algo="Algoritma, yoğun yıldırım-şimşek aktivitesi dönemlerini belirlemek için olaylar arasındaki zaman boşluklarına dayalı bir yaklaşım kullanır.",
step_by_step="Adım Adım Süreç:", step_by_step="Adım Adım Süreç:",
algo_chronological="Kronolojik Sıralama: Tüm yıldırım olayları zaman damgasına (local_time) göre sıralanır", algo_chronological="Kronolojik Sıralama: Tüm yıldırım olayları zaman damgasına (local_time) göre sıralanır",
algo_gap="Boşluk Hesaplama: Ardışık yıldırım olayları arasındaki zaman farkları hesaplanır", algo_gap="Boşluk Hesaplama: Ardışık yıldırım olayları arasındaki zaman farkları hesaplanır",
@ -568,8 +577,8 @@ _STRINGS_TR = ReportStrings(
algo_period_validation="Dönem Doğrulama: Yalnızca ≥{min_events} yıldırım olayı içeren dönemler anlamlı kabul edilir", algo_period_validation="Dönem Doğrulama: Yalnızca ≥{min_events} yıldırım olayı içeren dönemler anlamlı kabul edilir",
algo_timespan="Zaman Aralığı Tanımı: Başlangıç, dönemdeki ilk olay; bitiş ise son olaydır. Süre, ilk olaydan son olaya kadar geçen gerçek zamandır", algo_timespan="Zaman Aralığı Tanımı: Başlangıç, dönemdeki ilk olay; bitiş ise son olaydır. Süre, ilk olaydan son olaya kadar geçen gerçek zamandır",
algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama kullanılır; aktivite, ortalamanın 1 standart sapma üzerine çıktığında tepe dönem olarak kabul edilir ve histogramda sarı ile vurgulanır", algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama kullanılır; aktivite, ortalamanın 1 standart sapma üzerine çıktığında tepe dönem olarak kabul edilir ve histogramda sarı ile vurgulanır",
entln_title="6. EarthNetworks Toplam Yıldırımı (ENTLN)", entln_title="6. EarthNetworks Toplam Yıldırım-Şimşekı (ENTLN)",
entln_block_1="Bu rapordaki yıldırım verileri doğrudan EarthNetworks Toplam Yıldırım Ağı'ndan (ENTLN) alınmıştır; hem bulut içi (şimşek) hem buluttan yere (yıldırım) darbeleri izler. ENTLN, küresel ölçekte konuşlandırılan ilk şimşek ve yıldırım tespit ağıdır. 1.500'den fazla sensör içerir.", entln_block_1="Bu rapordaki yıldırım-şimşek verileri doğrudan EarthNetworks Toplam Yıldırım-Şimşekı'ndan (ENTLN) alınmıştır; hem bulut içi (şimşek) hem buluttan yere (yıldırım) darbeleri izler. ENTLN, küresel ölçekte konuşlandırılan ilk şimşek ve yıldırım tespit ağıdır. 1.500'den fazla sensör içerir.",
entln_block_2="Ulusal Hava Durumu Servisi, Hava Kuvvetleri Hava Durumu Ajansı ve kamu güvenliği, acil müdahale, havaalanları ve enerji kuruluşları gibi birçok kurum ENTLN bilgisine güvenir.", entln_block_2="Ulusal Hava Durumu Servisi, Hava Kuvvetleri Hava Durumu Ajansı ve kamu güvenliği, acil müdahale, havaalanları ve enerji kuruluşları gibi birçok kurum ENTLN bilgisine güvenir.",
entln_block_3="Uzun menzilli şimşekleri yüksek verimle tespit etme yeteneği, şiddetli hava olaylarının ileri tahmini için kritiktir:", entln_block_3="Uzun menzilli şimşekleri yüksek verimle tespit etme yeteneği, şiddetli hava olaylarının ileri tahmini için kritiktir:",
entln_tornadoes="Tornado ve siklonlar", entln_tornadoes="Tornado ve siklonlar",
@ -577,7 +586,7 @@ _STRINGS_TR = ReportStrings(
entln_downburst="Downburst ve rüzgar kesmesi", entln_downburst="Downburst ve rüzgar kesmesi",
entln_cg_strikes="Yıldırım darbeleri", entln_cg_strikes="Yıldırım darbeleri",
entln_block_4="Bu potansiyel olarak ölümcül hava olayları genellikle bulut içi flaş (şimşek) başlangıcından 5 ila 30 dakika içinde oluşur. ENTLN, radar ve diğer teknolojilere kıyasla şiddetli hava uyarı sürelerini önemli ölçüde iyileştirebilir.", entln_block_4="Bu potansiyel olarak ölümcül hava olayları genellikle bulut içi flaş (şimşek) başlangıcından 5 ila 30 dakika içinde oluşur. ENTLN, radar ve diğer teknolojilere kıyasla şiddetli hava uyarı sürelerini önemli ölçüde iyileştirebilir.",
entln_block_5="Toplam yıldırımı dünyada 40'tan fazla ülkede 1.500'den fazla sensörle genişlemiştir. Bu yoğun sensör dağılımı, toplam yıldırım aktivitesinin yüksek verimle yakalanmasını sağlar.", entln_block_5="Toplam yıldırım-şimşekı dünyada 40'tan fazla ülkede 1.500'den fazla sensörle genişlemiştir. Bu yoğun sensör dağılımı, toplam yıldırım-şimşek aktivitesinin yüksek verimle tespit edilmesini sağlar.",
risk_color_legend="Risk Renk Efsanesi (Log Risk):", risk_color_legend="Risk Renk Efsanesi (Log Risk):",
risk_legend_very_low="Çok Düşük Risk (< 0.1)", risk_legend_very_low="Çok Düşük Risk (< 0.1)",
risk_legend_low="Düşük Risk (0.1 - 0.2)", risk_legend_low="Düşük Risk (0.1 - 0.2)",

View File

@ -288,6 +288,11 @@ def filter_lightning_data_by_date_range(lightning_df: pd.DataFrame, start_date:
return None return None
try: try:
if re.fullmatch(r"\d{2}-\d{2}-\d{4} \d{2}:\d{2}", value_str):
dt = datetime.strptime(value_str, '%d-%m-%Y %H:%M')
if is_end:
dt = dt.replace(second=59)
return dt
if re.fullmatch(r"\d{2}-\d{2}-\d{4}", value_str): if re.fullmatch(r"\d{2}-\d{2}-\d{4}", value_str):
dt = datetime.strptime(value_str, '%d-%m-%Y') dt = datetime.strptime(value_str, '%d-%m-%Y')
if is_end: if is_end:
@ -310,7 +315,10 @@ def filter_lightning_data_by_date_range(lightning_df: pd.DataFrame, start_date:
df['local_time'] = pd.to_datetime(df['local_time']) df['local_time'] = pd.to_datetime(df['local_time'])
if df['local_time'].dt.tz is not None: if df['local_time'].dt.tz is not None:
df['local_time'] = df['local_time'].dt.tz_localize(None) from src.config import config
tz_name = getattr(config, 'timezone', None) or 'UTC'
df['local_time'] = df['local_time'].dt.tz_convert(tz_name).dt.tz_localize(None)
start_dt = _parse_flexible_datetime(start_date, is_end=False) start_dt = _parse_flexible_datetime(start_date, is_end=False)
end_dt = _parse_flexible_datetime(end_date, is_end=True) end_dt = _parse_flexible_datetime(end_date, is_end=True)

View File

@ -121,7 +121,7 @@ def group_storm_data_by_month(storm_data: List[Dict]) -> Dict[str, List[Dict]]:
def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.DataFrame = None, center_lat: float = None, center_lon: float = None, lang: ReportLanguage | None = None) -> go.Figure: def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.DataFrame = None, center_lat: float = None, center_lon: float = None, lang: ReportLanguage | None = None) -> go.Figure:
s = get_strings(lang or get_report_language()) s = get_strings(lang or get_report_language())
""" """
Create a coordinate plane visualization showing storm cells with turbines and distance rings. Create a coordinate plane visualization showing storm cell polygons.
Args: Args:
storm_data: List of storm cell dictionaries with WKT data storm_data: List of storm cell dictionaries with WKT data
@ -130,7 +130,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
center_lon: Center longitude for map (optional) center_lon: Center longitude for map (optional)
Returns: Returns:
Plotly figure with storm cells, turbines, and distance rings in coordinate plane view Plotly figure with storm cell polygons in coordinate plane view
""" """
# Calculate center if not provided # Calculate center if not provided
if center_lat is None or center_lon is None: if center_lat is None or center_lon is None:
@ -157,23 +157,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
fig = go.Figure() fig = go.Figure()
# Add distance rings if turbine data is provided
if turbine_df is not None and not turbine_df.empty: if turbine_df is not None and not turbine_df.empty:
from src.analysis.geospatial import create_circle_points
# Add distance rings from turbine centroid
for radius, color in zip(config.distance_rings, config.ring_colors):
circle_lats, circle_lons = create_circle_points(center_lat, center_lon, radius)
fig.add_trace(go.Scatter(
x=circle_lons, # X-axis = Longitude
y=circle_lats, # Y-axis = Latitude
mode='lines',
line=dict(color=color, width=4),
opacity=0.6,
name=s.map_distance_ring.format(radius=radius / 1000),
showlegend=True
))
# Add turbines colored by risk using fixed intervals # Add turbines colored by risk using fixed intervals
if 'risk_log' in turbine_df.columns: if 'risk_log' in turbine_df.columns:
from src.utils import get_turbine_colors_by_fixed_intervals from src.utils import get_turbine_colors_by_fixed_intervals
@ -281,13 +265,35 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
hoverinfo='skip' # No hover for legend entries hoverinfo='skip' # No hover for legend entries
)) ))
# Calculate axis limits based on data and distance rings bounds_lats: list[float] = []
max_radius_deg = max(config.distance_rings) / 111000 bounds_lons: list[float] = []
for storm in storm_data:
lat_min = center_lat - max_radius_deg * 1.5 coords = parse_wkt_linestring(storm.get('cell_polygon_wkt', ''))
lat_max = center_lat + max_radius_deg * 1.5 if coords:
lon_min = center_lon - max_radius_deg * 1.5 lons, lats = zip(*coords)
lon_max = center_lon + max_radius_deg * 1.5 bounds_lats.extend(lats)
bounds_lons.extend(lons)
if turbine_df is not None and not turbine_df.empty:
bounds_lats.extend(turbine_df['lat'].tolist())
bounds_lons.extend(turbine_df['lng'].tolist())
if bounds_lats and bounds_lons:
lat_min = min(bounds_lats)
lat_max = max(bounds_lats)
lon_min = min(bounds_lons)
lon_max = max(bounds_lons)
lat_pad = max((lat_max - lat_min) * 0.15, 0.01)
lon_pad = max((lon_max - lon_min) * 0.15, 0.01)
lat_min -= lat_pad
lat_max += lat_pad
lon_min -= lon_pad
lon_max += lon_pad
else:
pad = 0.05
lat_min = center_lat - pad
lat_max = center_lat + pad
lon_min = center_lon - pad
lon_max = center_lon + pad
add_satellite_basemap(fig, lon_min, lon_max, lat_min, lat_max) add_satellite_basemap(fig, lon_min, lon_max, lat_min, lat_max)
@ -338,7 +344,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
def create_storm_cells_map(storm_data: List[Dict], turbine_df: pd.DataFrame = None, center_lat: float = None, center_lon: float = None, lang: ReportLanguage | None = None) -> go.Figure: def create_storm_cells_map(storm_data: List[Dict], turbine_df: pd.DataFrame = None, center_lat: float = None, center_lon: float = None, lang: ReportLanguage | None = None) -> go.Figure:
""" """
Create a map showing storm cells from the fırtına data with turbines and distance rings. Create a map showing storm cell polygons from the fırtına data.
Now uses coordinate plane view instead of mapbox. Now uses coordinate plane view instead of mapbox.
Args: Args:
@ -348,7 +354,7 @@ def create_storm_cells_map(storm_data: List[Dict], turbine_df: pd.DataFrame = No
center_lon: Center longitude for map (optional) center_lon: Center longitude for map (optional)
Returns: Returns:
Plotly figure with storm cells, turbines, and distance rings in coordinate plane view Plotly figure with storm cell polygons in coordinate plane view
""" """
return create_storm_cells_coordinate_plane(storm_data, turbine_df, center_lat, center_lon, lang=lang) return create_storm_cells_coordinate_plane(storm_data, turbine_df, center_lat, center_lon, lang=lang)