diff --git a/src/reporting/docx.py b/src/reporting/docx.py index 63cdc32..33c1556 100644 --- a/src/reporting/docx.py +++ b/src/reporting/docx.py @@ -30,6 +30,7 @@ from src.reporting.docx_sections import ( build_turbine_information_table_data, ) from src.utils import ( + filter_lightning_data_by_date_range, format_datetime_ddmmyyyy_hhmm, format_datetime_to_local_display, 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) +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( lightning_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: bullets.append(s.kf_nearest_strike.format(distance=nearest_km, name=nearest_turbine)) 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: bullets.append(s.kf_active_days.format(days=active_days)) 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_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) 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) @@ -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_1, 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) _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_bullets( 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, ) - 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) 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) expire_formatted = format_datetime_to_local_display(expire_raw, report_tz) + raw_severity = storm.get("lightning_severity", s.unknown) table_data.append( [ str(i), - str(storm.get("lightning_severity", s.unknown)), + _storm_severity_label(str(raw_severity), s), effective_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": row_colors.append("purple") elif severity == "medium": diff --git a/src/reporting/docx_sections.py b/src/reporting/docx_sections.py index 9273d5c..dd9f72c 100644 --- a/src/reporting/docx_sections.py +++ b/src/reporting/docx_sections.py @@ -11,36 +11,69 @@ from src.reporting.precompute import precompute_distances_and_rings from src.reporting.strings import ReportLanguage, get_report_language, get_strings -def _parse_chart_times(values: pd.Series) -> 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") +def _parse_epoch_to_farm_local(numeric: pd.Series, tz_name: str | None) -> pd.Series: 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()) - if abs_max >= 1e17: - unit = "ns" - elif abs_max >= 1e14: - unit = "us" - elif abs_max >= 1e11: - unit = "ms" + abs_max = float(numeric_valid.abs().max()) + if abs_max >= 1e17: + unit = "ns" + elif abs_max >= 1e14: + unit = "us" + elif abs_max >= 1e11: + 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: - unit = "s" - parsed = pd.to_datetime(numeric, errors="coerce", unit=unit, utc=True) - return _to_local_naive(parsed) + parsed = pd.to_datetime(values, errors="coerce") - parsed = pd.to_datetime(values, errors="coerce", utc=True) - return _to_local_naive(parsed) + if getattr(parsed.dt, "tz", None) is None: + 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( @@ -67,17 +100,17 @@ def _build_current_vs_distance_chart( distances = dists_km[combined_mask] currents = subset["current"].values.astype(float) - time_series = _parse_chart_times(subset["local_time"]) - valid_mask = ~time_series.isna() + display_times = _farm_local_naive_times(subset["local_time"]) + valid_mask = ~display_times.isna() if valid_mask.sum() == 0: return None - time_series = time_series.loc[valid_mask] + display_times = display_times.loc[valid_mask] distances = distances[valid_mask.values] currents = currents[valid_mask.values] - sort_idx = np.argsort(time_series.values.astype("datetime64[ns]")) - time_values = time_series.values[sort_idx] + sort_idx = np.argsort(display_times.values.astype("datetime64[ns]")) + time_values = display_times.values[sort_idx] distances = distances[sort_idx] currents = currents[sort_idx] @@ -124,6 +157,14 @@ def _build_current_vs_distance_chart( ) 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( font=dict(size=16), @@ -137,8 +178,9 @@ def _build_current_vs_distance_chart( showgrid=True, gridcolor="lightgray", zeroline=False, + range=[x_min, x_max], tickformat="%d-%m-%Y %H:%M", - nticks=6, + dtick=dtick, tickangle=-25, tickfont=dict(size=22), title_font=dict(size=28), @@ -167,7 +209,6 @@ def _build_current_vs_distance_chart( height=fig_height, margin=dict(l=70, r=40, t=50, b=130), ) - return fig diff --git a/src/reporting/gemini_commentary.py b/src/reporting/gemini_commentary.py index 7b6de04..b7ed0c7 100644 --- a/src/reporting/gemini_commentary.py +++ b/src/reporting/gemini_commentary.py @@ -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") + "\n\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ı açı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 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" "\n" "Örnek üslup (yalnızca stil rehberi; sayıları kopyalama):\n" - "\"03-05-2026 10:43–03-05-2026 10:43 döneminde 9,0 km yarıçaplı analiz alanında toplam 2 yıldırım olayı kaydedilmiştir. " - "Yıldırım yoğunluğu 0,008 olay/km² olarak hesaplanmıştır. Yıldırım olayları 1,0–3,0 km halkasında (2 yıldırım, 0 şimşek) yoğunlaşmıştır. " + "\"03-05-2026 10:43–03-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 olayları 1,0–3,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" "\n" "Çıktı:\n" @@ -276,15 +277,15 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None if s.gemini_write_turkish: 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)) - 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) paragraph_intro = ( - f"{analysis_period} döneminde {radius_txt} {events_txt} tespit edilmiştir. " - f"Yıldırım yoğunluğu {density_txt} olarak hesaplanmıştır. " + f"{analysis_period} arasında {radius_txt} {events_txt} kaydedilmiştir ve " + f"yıldırım-şimşek yoğunluğu {density_txt} olarak hesaplanmıştır. " f"{ring_sentence} " ) turbine_sentence = ( diff --git a/src/reporting/strings.py b/src/reporting/strings.py index 3d70520..88ed9e5 100644 --- a/src/reporting/strings.py +++ b/src/reporting/strings.py @@ -82,6 +82,9 @@ class ReportStrings: storm_cells_overview: str storm_cells_overview_1: 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 total_storm_cells: str severity_breakdown: str @@ -250,7 +253,7 @@ _STRINGS_EN = ReportStrings( kf_peak_activity="Peak activity: {when} ({count} events in one minute)", 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_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_storm_distance="Storm cells detected: {count} (closest {distance:.1f} km from the centroid)", recommendation_heading="Recommendation", @@ -287,8 +290,11 @@ _STRINGS_EN = ReportStrings( frequent_lightning_periods="Frequent Lightning Activity Periods", storm_cells_analysis_summary="Storm Cells Analysis Summary", 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_2="Each cell represents a storm system with defined boundaries and storm severity levels.", + storm_cells_overview_1="The following pages show storm cell boundaries observed within the analyzed area.", + 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:", total_storm_cells="Total storm cells: {count}", severity_breakdown="Severity breakdown:", @@ -446,7 +452,7 @@ _STRINGS_EN = 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_local="Analiz Dönemi (yerel saat):", centroid_coordinates="Merkez Koordinatları:", @@ -466,7 +472,7 @@ _STRINGS_TR = ReportStrings( 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_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_storm_distance="Tespit edilen fırtına hücresi: {count} (merkeze en yakın {distance:.1f} km)", recommendation_heading="Öneri", @@ -503,8 +509,11 @@ _STRINGS_TR = ReportStrings( 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_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_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_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 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:", total_storm_cells="Toplam fırtına hücresi: {count}", severity_breakdown="Şiddet dağılımı:", @@ -516,7 +525,7 @@ _STRINGS_TR = ReportStrings( more_days="... ve {n} gün daha", complete_storm_list="Tam Fırtına Hücreleri Listesi:", 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", appendix="Ek", 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_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ı", - 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)", 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)", 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", risk_interpretation="2. Risk Skoru Yorumu", 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_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", - 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:", - 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ç:", 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", @@ -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_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", - entln_title="6. EarthNetworks Toplam Yıldırım Ağı (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_title="6. EarthNetworks Toplam Yıldırım-Şimşek Ağı (ENTLN)", + entln_block_1="Bu rapordaki yıldırım-şimşek verileri doğrudan EarthNetworks Toplam Yıldırım-Şimşek 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_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_tornadoes="Tornado ve siklonlar", @@ -577,7 +586,7 @@ _STRINGS_TR = ReportStrings( entln_downburst="Downburst ve rüzgar kesmesi", 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_5="Toplam yıldırım ağı 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 ağı 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_legend_very_low="Çok Düşük Risk (< 0.1)", risk_legend_low="Düşük Risk (0.1 - 0.2)", diff --git a/src/utils.py b/src/utils.py index a628162..c9801db 100644 --- a/src/utils.py +++ b/src/utils.py @@ -288,6 +288,11 @@ def filter_lightning_data_by_date_range(lightning_df: pd.DataFrame, start_date: return None 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): dt = datetime.strptime(value_str, '%d-%m-%Y') 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']) 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) end_dt = _parse_flexible_datetime(end_date, is_end=True) diff --git a/src/visualization/storm_cells.py b/src/visualization/storm_cells.py index fdcf92e..177ed6b 100644 --- a/src/visualization/storm_cells.py +++ b/src/visualization/storm_cells.py @@ -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: 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: 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) 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 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() - # Add distance rings if turbine data is provided 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 if 'risk_log' in turbine_df.columns: 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 )) - # Calculate axis limits based on data and distance rings - max_radius_deg = max(config.distance_rings) / 111000 - - lat_min = center_lat - max_radius_deg * 1.5 - lat_max = center_lat + max_radius_deg * 1.5 - lon_min = center_lon - max_radius_deg * 1.5 - lon_max = center_lon + max_radius_deg * 1.5 + bounds_lats: list[float] = [] + bounds_lons: list[float] = [] + for storm in storm_data: + coords = parse_wkt_linestring(storm.get('cell_polygon_wkt', '')) + if coords: + lons, lats = zip(*coords) + 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) @@ -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: """ - 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. 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) 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)