-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathFetch_5_3.py
More file actions
5079 lines (4531 loc) · 270 KB
/
Fetch_5_3.py
File metadata and controls
5079 lines (4531 loc) · 270 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#Python3
# fixed an issue with cell site maps not showing correct options
import simplekml #the library used to map longitudes and latitudes on google earth
import pandas #used to read spreadsheet data
import re
# import operator
import streamlit as st
import chardet # used to check file encodings
import os
import tempfile
# leafmap writes temp HTML files relative to cwd; ensure cwd is always writable
os.chdir(tempfile.gettempdir())
# ── Language Support ───────────────────────────────────────────────────────────
TRANSLATIONS = {
"en": {
"tab_review_ingest": "Review Ingest Data",
"tab_time_filter": "Time Filter",
"tab_declutter": "Declutter",
"tab_timezone": "Timezone Conversion",
"tab_geofence": "Create Geofence",
"tab_ip_mapping": "IP Address Mapping",
"tab_preview_kml": "Preview/KML Map",
"tab_analysis_maps": "Analysis Maps",
"tab_create_geofence": "Create Geofence",
"tab_advanced_analysis": "Advanced Analysis",
"tab_stop_dwell": "Stop/Dwell Detection",
"tab_colocation": "Co-Location Analysis",
"tab_coord_tools": "Coordinate Tools",
"tab_single_conv": "Single Conversion",
"tab_utm_conv": "UTM Converter",
"map_clustered": "Clustered Markers",
"map_points_trails": "Points & Trails",
"map_hotspots": "Hotspots",
"map_heatmap": "Heatmap",
"map_cell_sites": "Cell Sites",
"mode_markers": "Markers",
"mode_progression": "Show Point Progression",
"mode_vapor": "Vapour Trail",
"lbl_select_map_type": "Select Map Type",
"lbl_select_map_activity": "Select map activity",
"lbl_time_interval": "Time Interval to Display",
"lbl_conversion_method": "Conversion Method",
"lbl_output_format": "Output Format",
"lbl_hemisphere": "Hemisphere",
"lbl_datetime_column": "Date / Time Column",
"lbl_weight_column": "Weight Column",
"lbl_time_column": "Time Column",
"lbl_icon_style": "Select Map Point Icon Style",
"lbl_icon_labels": "Map Icon Labels",
"lbl_sector_color": "Sector Color",
"lbl_sector_footprint": "Sector Footprint Size",
"lbl_sector_azimuth": "Sector Azimuth",
"lbl_beam_width": "Sector Beam Width",
"lbl_radius_meters": "Radius/Footprint in Meters",
"lbl_radius_distance": "Radius/Distance-from-Point in Meters",
"lbl_tour_altitude": "Tour Altitude (Meters)",
"lbl_linger_time": "Tour Linger Time (Seconds)",
"lbl_tour_tilt": "Tour Tilt",
"lbl_camera_fly": "Camera Fly Mode",
"lbl_remove_rows": "Number of Rows to Remove from Start",
"lbl_radius_m": "Radius (m)",
"lbl_max_hotspots": "Max Hotspots",
"lbl_source_tz": "Source Timezone",
"lbl_target_tz": "Target Timezone",
"interval_daily": "Daily",
"interval_hourly": "Hourly",
"interval_10min": "10 Minutes",
"interval_1min": "1 Minute",
"conv_timezone": "Timezone Conversion",
"conv_offset": "Hourly Offset",
"northern": "Northern",
"southern": "Southern",
"fmt_kml": "KML",
"fmt_kmz": "KMZ",
"btn_search": "Search",
"btn_run_hotspot": "Run Hotspot Analysis",
"btn_clear_hotspots": "Clear Hotspots",
"btn_generate_kml": "Generate KML",
"btn_detect_stops": "Detect Stops",
"btn_run_colocation": "Run Co-Location Analysis",
"btn_convert_utm": "Convert UTM",
"btn_apply_time_filter": "Apply Multi-Source Time Filter",
"btn_preview_conversion": "Preview Conversion",
"btn_apply_conversion": "Apply Conversion",
"chk_advanced": "Advanced",
"chk_trim_chaining": "Trim Chaining (enforce radius)",
"chk_accuracy_radius": "Data has Accuracy or Radius Information",
"chk_enable_time_filter": "Enable Time Filtering",
"chk_per_source": "Analyze each source file separately",
"chk_show_points": "Show Individual Points",
"chk_show_col_info": "Show Column Information",
"chk_footprint": "Data set includes radius/area information",
"chk_path_line": "Include travel path line",
"chk_kml_tour": "Include KML Tour",
"chk_has_dates": "Data set includes date/time information",
"chk_use_weight": "Use Weight Column",
"chk_declutter": "Enable Declutter",
"hdr_advanced_analysis": "Advanced Analysis",
"hdr_stop_dwell": "Stop / Dwell Detection",
"hdr_colocation": "Co-Location / Proximity Analysis",
"hdr_coord_converter": "Coordinate Format Converter",
"hdr_hotspot_summary": "Hotspot Summary",
"hdr_hotspot_clocks": "Hotspot Tactical Clocks",
"hdr_design_kml": "Design Your KML Map",
"hdr_tour_settings": "Design Tour Settings",
"hdr_filter_results": "Filter Results",
"hdr_declutter": "\U0001f3af Declutter Settings",
"hdr_header_cleaning": "Header Cleaning",
"hdr_data_preview": "Data Preview",
"hdr_datetime_filter": "Date/Time Filtering",
"hdr_data_declutter": "Data Decluttering",
"hdr_timezone": "Timezone Conversion",
"hdr_stop_map": "Stop Locations Map",
"hdr_coloc_map": "Co-Location Map",
"hdr_custom_date": "Custom Date Range",
"msg_manage_data": "Manage Ingested Data",
"msg_import_photos": "Import Photo Locations (EXIF GPS)",
"msg_drawing_hint": "Draw a shape (polygon/rectangle) to see coordinates below the map instantly.",
"msg_tips_hotspot": "**Tips:** Increase radius if visits are a few dozen meters apart; decrease radius for tighter grouping.",
"msg_coloc_requires": "Co-location analysis requires **2 or more source files** loaded simultaneously. Upload multiple files to use this feature.",
"msg_convert_single": "Convert a single coordinate value between formats.",
"msg_convert_utm": "Convert UTM coordinates to latitude/longitude.",
"msg_stops_no_time": "No date/time columns detected. Stop/Dwell detection requires time data.",
"msg_coloc_no_time": "No date/time columns detected. Co-location analysis requires time data.",
"msg_no_valid_records": "No valid records with coordinates.",
"msg_map_name_required": "Provide map name above",
"msg_check_lat_lon": "Check that your data has Latitude and Longitude columns",
"msg_photo_success": "Extracted GPS data from {} photo(s)",
"msg_no_gps_photos": "No GPS data could be extracted from the uploaded photos.",
"hdr_multisource_time": "Multi-Source Time Configuration",
"hdr_sample_filtered": "Sample of Filtered Data",
"hdr_time_filter": "Time Filter",
"hdr_time_range_info": "Data Time Range Information",
"hdr_quick_filters": "Quick Filters",
"hdr_review_data": "Review Ingested Data",
"hdr_apply_time_filter": "Apply Time Filter",
"hdr_coord_diag": "Loaded Data Coordinate Diagnostics",
"btn_download_csv": "Download Filtered Data as CSV",
"msg_params_changed": "Parameters changed — press 'Run Hotspot Analysis' to recompute.",
"qf_all_data": "All Data",
"qf_24h": "Last 24 Hours",
"qf_week": "Last Week",
"qf_month": "Last Month",
"qf_custom": "Custom Range",
"lbl_quick_filter": "Choose a preset or custom range:",
"lbl_choose_files": "Choose files to analyze",
"lbl_search_addr_ip": "Search (Address/IP)",
"lbl_place_name": "Place Name or Address",
"lbl_enter_coord": "Enter coordinate (DMS, DDM, or Decimal)",
"lbl_time_col_optional": "Time Column (optional -select for tactical clock)",
"lbl_proximity_radius": "Proximity Radius (m)",
"lbl_time_window": "Time Window (min)",
"lbl_min_duration": "Min Duration (min)",
"lbl_zone_number": "Zone Number",
"lbl_zone_letter": "Zone Letter",
"lbl_easting": "Easting (m)",
"lbl_northing": "Northing (m)",
"lbl_date_time_col": "Date/Time Column",
"lbl_radius_col_meters": "Radius Column (meters)",
"msg_privacy": "Privacy Statement and API Use",
},
"es": {
"tab_review_ingest": "Revisar Datos",
"tab_time_filter": "Filtro de Tiempo",
"tab_declutter": "Simplificar",
"tab_timezone": "Conversión de Zona Horaria",
"tab_geofence": "Crear Geocerca",
"tab_ip_mapping": "Mapeo de Dirección IP",
"tab_preview_kml": "Vista Previa/Mapa KML",
"tab_analysis_maps": "Mapas de Análisis",
"tab_create_geofence": "Crear Geocerca",
"tab_advanced_analysis": "Análisis Avanzado",
"tab_stop_dwell": "Detección de Paradas",
"tab_colocation": "Análisis de Co-ubicación",
"tab_coord_tools": "Herramientas de Coordenadas",
"tab_single_conv": "Conversión Individual",
"tab_utm_conv": "Convertidor UTM",
"map_clustered": "Marcadores Agrupados",
"map_points_trails": "Puntos y Trayectorias",
"map_hotspots": "Zonas de Concentración",
"map_heatmap": "Mapa de Calor",
"map_cell_sites": "Torres Celulares",
"mode_markers": "Marcadores",
"mode_progression": "Mostrar Progresión de Puntos",
"mode_vapor": "Rastro de Vapor",
"lbl_select_map_type": "Seleccionar Tipo de Mapa",
"lbl_select_map_activity": "Seleccionar actividad del mapa",
"lbl_time_interval": "Intervalo de Tiempo a Mostrar",
"lbl_conversion_method": "Método de Conversión",
"lbl_output_format": "Formato de Salida",
"lbl_hemisphere": "Hemisferio",
"lbl_datetime_column": "Columna de Fecha / Hora",
"lbl_weight_column": "Columna de Peso",
"lbl_time_column": "Columna de Tiempo",
"lbl_icon_style": "Seleccionar Estilo de Icono del Mapa",
"lbl_icon_labels": "Etiquetas de Iconos del Mapa",
"lbl_sector_color": "Color del Sector",
"lbl_sector_footprint": "Tamaño del Sector",
"lbl_sector_azimuth": "Azimut del Sector",
"lbl_beam_width": "Ancho del Haz del Sector",
"lbl_radius_meters": "Radio/Huella en Metros",
"lbl_radius_distance": "Radio/Distancia desde el Punto en Metros",
"lbl_tour_altitude": "Altitud del Tour (Metros)",
"lbl_linger_time": "Tiempo de Espera del Tour (Segundos)",
"lbl_tour_tilt": "Inclinación del Tour",
"lbl_camera_fly": "Modo de Vuelo de Cámara",
"lbl_remove_rows": "Número de Filas a Eliminar del Inicio",
"lbl_radius_m": "Radio (m)",
"lbl_max_hotspots": "Máximo de Zonas",
"lbl_source_tz": "Zona Horaria de Origen",
"lbl_target_tz": "Zona Horaria de Destino",
"interval_daily": "Diario",
"interval_hourly": "Por Hora",
"interval_10min": "10 Minutos",
"interval_1min": "1 Minuto",
"conv_timezone": "Conversión de Zona Horaria",
"conv_offset": "Desplazamiento por Horas",
"northern": "Norte",
"southern": "Sur",
"fmt_kml": "KML",
"fmt_kmz": "KMZ",
"btn_search": "Buscar",
"btn_run_hotspot": "Ejecutar Análisis de Zonas",
"btn_clear_hotspots": "Limpiar Zonas",
"btn_generate_kml": "Generar KML",
"btn_detect_stops": "Detectar Paradas",
"btn_run_colocation": "Ejecutar Análisis de Co-ubicación",
"btn_convert_utm": "Convertir UTM",
"btn_apply_time_filter": "Aplicar Filtro de Tiempo",
"btn_preview_conversion": "Vista Previa de Conversión",
"btn_apply_conversion": "Aplicar Conversión",
"chk_advanced": "Avanzado",
"chk_trim_chaining": "Recortar Encadenamiento (aplicar radio)",
"chk_accuracy_radius": "El conjunto de datos incluye información de precisión o radio",
"chk_enable_time_filter": "Habilitar Filtro de Tiempo",
"chk_per_source": "Analizar cada archivo fuente por separado",
"chk_show_points": "Mostrar Puntos Individuales",
"chk_show_col_info": "Mostrar Información de Columnas",
"chk_footprint": "El conjunto de datos incluye información de radio/área",
"chk_path_line": "Incluir línea de trayectoria de viaje",
"chk_kml_tour": "Incluir Tour KML",
"chk_has_dates": "El conjunto de datos incluye información de fecha/hora",
"chk_use_weight": "Usar Columna de Peso",
"chk_declutter": "Habilitar Simplificación",
"hdr_advanced_analysis": "Análisis Avanzado",
"hdr_stop_dwell": "Detección de Paradas",
"hdr_colocation": "Análisis de Co-ubicación / Proximidad",
"hdr_coord_converter": "Convertidor de Formato de Coordenadas",
"hdr_hotspot_summary": "Resumen de Zonas de Concentración",
"hdr_hotspot_clocks": "Relojes Tácticos de Zonas",
"hdr_design_kml": "Diseña tu Mapa KML",
"hdr_tour_settings": "Configuración del Tour",
"hdr_filter_results": "Filtrar Resultados",
"hdr_declutter": "\U0001f3af Configuración de Simplificación",
"hdr_header_cleaning": "Limpieza de Encabezados",
"hdr_data_preview": "Vista Previa de Datos",
"hdr_datetime_filter": "Filtrado de Fecha/Hora",
"hdr_data_declutter": "Simplificación de Datos",
"hdr_timezone": "Conversión de Zona Horaria",
"hdr_stop_map": "Mapa de Ubicaciones de Paradas",
"hdr_coloc_map": "Mapa de Co-ubicación",
"hdr_custom_date": "Rango de Fechas Personalizado",
"msg_manage_data": "Gestionar Datos Importados",
"msg_import_photos": "Importar Ubicaciones de Fotos (EXIF GPS)",
"msg_drawing_hint": "Dibuja una forma (polígono/rectángulo) para ver las coordenadas debajo del mapa al instante.",
"msg_tips_hotspot": "**Consejos:** Aumenta el radio si las visitas están separadas varias decenas de metros; disminúyelo para agrupaciones más precisas.",
"msg_coloc_requires": "El análisis de co-ubicación requiere **2 o más archivos fuente** cargados simultáneamente.",
"msg_convert_single": "Convierte un valor de coordenada entre formatos.",
"msg_convert_utm": "Convierte coordenadas UTM a latitud/longitud.",
"msg_stops_no_time": "No se detectaron columnas de fecha/hora. La detección de paradas requiere datos de tiempo.",
"msg_coloc_no_time": "No se detectaron columnas de fecha/hora. El análisis de co-ubicación requiere datos de tiempo.",
"msg_no_valid_records": "No hay registros válidos con coordenadas.",
"msg_map_name_required": "Proporciona el nombre del mapa arriba",
"msg_check_lat_lon": "Comprueba que tus datos tienen columnas de Latitud y Longitud",
"msg_photo_success": "Datos GPS extraídos de {} foto(s)",
"msg_no_gps_photos": "No se pudieron extraer datos GPS de las fotos cargadas.",
"hdr_multisource_time": "Configuración de Tiempo Multi-Fuente",
"hdr_sample_filtered": "Muestra de Datos Filtrados",
"hdr_time_filter": "Filtro de Tiempo",
"hdr_time_range_info": "Información del Rango de Tiempo de Datos",
"hdr_quick_filters": "Filtros Rápidos",
"hdr_review_data": "Revisar Datos Importados",
"hdr_apply_time_filter": "Aplicar Filtro de Tiempo",
"hdr_coord_diag": "Diagnóstico de Coordenadas de Datos Cargados",
"btn_download_csv": "Descargar Datos Filtrados como CSV",
"msg_params_changed": "Parámetros cambiados — presiona 'Ejecutar Análisis de Zonas' para recalcular.",
"qf_all_data": "Todos los Datos",
"qf_24h": "Últimas 24 Horas",
"qf_week": "Última Semana",
"qf_month": "Último Mes",
"qf_custom": "Rango Personalizado",
"lbl_quick_filter": "Elige un rango preestablecido o personalizado:",
"lbl_choose_files": "Elegir archivos para analizar",
"lbl_search_addr_ip": "Buscar (Dirección/IP)",
"lbl_place_name": "Nombre de Lugar o Dirección",
"lbl_enter_coord": "Ingresar coordenada (GMS, GMD o Decimal)",
"lbl_time_col_optional": "Columna de Tiempo (opcional - para reloj táctico)",
"lbl_proximity_radius": "Radio de Proximidad (m)",
"lbl_time_window": "Ventana de Tiempo (min)",
"lbl_min_duration": "Duración Mínima (min)",
"lbl_zone_number": "Número de Zona",
"lbl_zone_letter": "Letra de Zona",
"lbl_easting": "Este (m)",
"lbl_northing": "Norte (m)",
"lbl_date_time_col": "Columna de Fecha/Hora",
"lbl_radius_col_meters": "Columna de Radio (metros)",
"msg_privacy": "Declaración de Privacidad y Uso de API",
},
"pt": {
"tab_review_ingest": "Revisar Dados",
"tab_time_filter": "Filtro de Tempo",
"tab_declutter": "Simplificar",
"tab_timezone": "Conversão de Fuso Horário",
"tab_geofence": "Criar Geocerca",
"tab_ip_mapping": "Mapeamento de IP",
"tab_preview_kml": "Pré-visualização/Mapa KML",
"tab_analysis_maps": "Mapas de Análise",
"tab_create_geofence": "Criar Geocerca",
"tab_advanced_analysis": "Análise Avançada",
"tab_stop_dwell": "Detecção de Paradas",
"tab_colocation": "Análise de Co-localização",
"tab_coord_tools": "Ferramentas de Coordenadas",
"tab_single_conv": "Conversão Individual",
"tab_utm_conv": "Conversor UTM",
"map_clustered": "Marcadores Agrupados",
"map_points_trails": "Pontos e Trilhas",
"map_hotspots": "Pontos de Concentração",
"map_heatmap": "Mapa de Calor",
"map_cell_sites": "Torres de Celular",
"mode_markers": "Marcadores",
"mode_progression": "Mostrar Progressão de Pontos",
"mode_vapor": "Rastro de Vapor",
"lbl_select_map_type": "Selecionar Tipo de Mapa",
"lbl_select_map_activity": "Selecionar atividade do mapa",
"lbl_time_interval": "Intervalo de Tempo a Exibir",
"lbl_conversion_method": "Método de Conversão",
"lbl_output_format": "Formato de Saída",
"lbl_hemisphere": "Hemisfério",
"lbl_datetime_column": "Coluna de Data / Hora",
"lbl_weight_column": "Coluna de Peso",
"lbl_time_column": "Coluna de Tempo",
"lbl_icon_style": "Selecionar Estilo de Ícone do Mapa",
"lbl_icon_labels": "Rótulos de Ícones do Mapa",
"lbl_sector_color": "Cor do Setor",
"lbl_sector_footprint": "Tamanho do Setor",
"lbl_sector_azimuth": "Azimute do Setor",
"lbl_beam_width": "Largura do Feixe do Setor",
"lbl_radius_meters": "Raio/Área em Metros",
"lbl_radius_distance": "Raio/Distância do Ponto em Metros",
"lbl_tour_altitude": "Altitude do Tour (Metros)",
"lbl_linger_time": "Tempo de Espera do Tour (Segundos)",
"lbl_tour_tilt": "Inclinação do Tour",
"lbl_camera_fly": "Modo de Voo da Câmera",
"lbl_remove_rows": "Número de Linhas a Remover do Início",
"lbl_radius_m": "Raio (m)",
"lbl_max_hotspots": "Máximo de Hotspots",
"lbl_source_tz": "Fuso Horário de Origem",
"lbl_target_tz": "Fuso Horário de Destino",
"interval_daily": "Diário",
"interval_hourly": "Por Hora",
"interval_10min": "10 Minutos",
"interval_1min": "1 Minuto",
"conv_timezone": "Conversão de Fuso Horário",
"conv_offset": "Deslocamento por Horas",
"northern": "Norte",
"southern": "Sul",
"fmt_kml": "KML",
"fmt_kmz": "KMZ",
"btn_search": "Buscar",
"btn_run_hotspot": "Executar Análise de Hotspots",
"btn_clear_hotspots": "Limpar Hotspots",
"btn_generate_kml": "Gerar KML",
"btn_detect_stops": "Detectar Paradas",
"btn_run_colocation": "Executar Análise de Co-localização",
"btn_convert_utm": "Converter UTM",
"btn_apply_time_filter": "Aplicar Filtro de Tempo",
"btn_preview_conversion": "Pré-visualizar Conversão",
"btn_apply_conversion": "Aplicar Conversão",
"chk_advanced": "Avançado",
"chk_trim_chaining": "Limitar Encadeamento (aplicar raio)",
"chk_accuracy_radius": "O conjunto de dados inclui informações de precisão ou raio",
"chk_enable_time_filter": "Habilitar Filtro de Tempo",
"chk_per_source": "Analisar cada arquivo-fonte separadamente",
"chk_show_points": "Mostrar Pontos Individuais",
"chk_show_col_info": "Mostrar Informações de Colunas",
"chk_footprint": "O conjunto de dados inclui informações de raio/área",
"chk_path_line": "Incluir linha de trajetória de viagem",
"chk_kml_tour": "Incluir Tour KML",
"chk_has_dates": "O conjunto de dados inclui informações de data/hora",
"chk_use_weight": "Usar Coluna de Peso",
"chk_declutter": "Habilitar Simplificação",
"hdr_advanced_analysis": "Análise Avançada",
"hdr_stop_dwell": "Detecção de Paradas",
"hdr_colocation": "Análise de Co-localização / Proximidade",
"hdr_coord_converter": "Conversor de Formato de Coordenadas",
"hdr_hotspot_summary": "Resumo de Pontos de Concentração",
"hdr_hotspot_clocks": "Relógios Táticos de Hotspots",
"hdr_design_kml": "Projete seu Mapa KML",
"hdr_tour_settings": "Configurações do Tour",
"hdr_filter_results": "Filtrar Resultados",
"hdr_declutter": "\U0001f3af Configurações de Simplificação",
"hdr_header_cleaning": "Limpeza de Cabeçalhos",
"hdr_data_preview": "Pré-visualização de Dados",
"hdr_datetime_filter": "Filtragem de Data/Hora",
"hdr_data_declutter": "Simplificação de Dados",
"hdr_timezone": "Conversão de Fuso Horário",
"hdr_stop_map": "Mapa de Locais de Paradas",
"hdr_coloc_map": "Mapa de Co-localização",
"hdr_custom_date": "Intervalo de Datas Personalizado",
"msg_manage_data": "Gerenciar Dados Importados",
"msg_import_photos": "Importar Locais de Fotos (EXIF GPS)",
"msg_drawing_hint": "Desenhe uma forma (polígono/retângulo) para ver as coordenadas abaixo do mapa imediatamente.",
"msg_tips_hotspot": "**Dicas:** Aumente o raio se as visitas estiverem separadas por algumas dezenas de metros; diminua para agrupamentos mais precisos.",
"msg_coloc_requires": "A análise de co-localização requer **2 ou mais arquivos-fonte** carregados simultaneamente.",
"msg_convert_single": "Converta um valor de coordenada entre formatos.",
"msg_convert_utm": "Converta coordenadas UTM para latitude/longitude.",
"msg_stops_no_time": "Nenhuma coluna de data/hora detectada. A detecção de paradas requer dados de tempo.",
"msg_coloc_no_time": "Nenhuma coluna de data/hora detectada. A análise de co-localização requer dados de tempo.",
"msg_no_valid_records": "Nenhum registro válido com coordenadas.",
"msg_map_name_required": "Forneça o nome do mapa acima",
"msg_check_lat_lon": "Verifique se seus dados têm colunas de Latitude e Longitude",
"msg_photo_success": "Dados GPS extraídos de {} foto(s)",
"msg_no_gps_photos": "Não foi possível extrair dados GPS das fotos carregadas.",
"hdr_multisource_time": "Configuração de Tempo Multi-Fonte",
"hdr_sample_filtered": "Amostra de Dados Filtrados",
"hdr_time_filter": "Filtro de Tempo",
"hdr_time_range_info": "Informações do Intervalo de Tempo dos Dados",
"hdr_quick_filters": "Filtros Rápidos",
"hdr_review_data": "Revisar Dados Importados",
"hdr_apply_time_filter": "Aplicar Filtro de Tempo",
"hdr_coord_diag": "Diagnóstico de Coordenadas dos Dados Carregados",
"btn_download_csv": "Baixar Dados Filtrados como CSV",
"msg_params_changed": "Parâmetros alterados — pressione 'Executar Análise de Hotspots' para recalcular.",
"qf_all_data": "Todos os Dados",
"qf_24h": "Últimas 24 Horas",
"qf_week": "Última Semana",
"qf_month": "Último Mês",
"qf_custom": "Intervalo Personalizado",
"lbl_quick_filter": "Escolha um intervalo predefinido ou personalizado:",
"lbl_choose_files": "Escolher arquivos para analisar",
"lbl_search_addr_ip": "Buscar (Endereço/IP)",
"lbl_place_name": "Nome do Local ou Endereço",
"lbl_enter_coord": "Inserir coordenada (GMS, GMD ou Decimal)",
"lbl_time_col_optional": "Coluna de Tempo (opcional - para relógio tático)",
"lbl_proximity_radius": "Raio de Proximidade (m)",
"lbl_time_window": "Janela de Tempo (min)",
"lbl_min_duration": "Duração Mínima (min)",
"lbl_zone_number": "Número da Zona",
"lbl_zone_letter": "Letra da Zona",
"lbl_easting": "Leste (m)",
"lbl_northing": "Norte (m)",
"lbl_date_time_col": "Coluna de Data/Hora",
"lbl_radius_col_meters": "Coluna de Raio (metros)",
"msg_privacy": "Declaração de Privacidade e Uso de API",
},
"de": {
"tab_review_ingest": "Daten Überprüfen",
"tab_time_filter": "Zeitfilter",
"tab_declutter": "Bereinigen",
"tab_timezone": "Zeitzonenkonvertierung",
"tab_geofence": "Geofence Erstellen",
"tab_ip_mapping": "IP-Adress-Kartierung",
"tab_preview_kml": "Vorschau/KML-Karte",
"tab_analysis_maps": "Analysekarten",
"tab_create_geofence": "Geofence Erstellen",
"tab_advanced_analysis": "Erweiterte Analyse",
"tab_stop_dwell": "Aufenthalts-Erkennung",
"tab_colocation": "Ko-Lokalisierungs-Analyse",
"tab_coord_tools": "Koordinatenwerkzeuge",
"tab_single_conv": "Einzelkonvertierung",
"tab_utm_conv": "UTM-Konverter",
"map_clustered": "Gruppierte Marker",
"map_points_trails": "Punkte & Routen",
"map_hotspots": "Hotspots",
"map_heatmap": "Heatmap",
"map_cell_sites": "Mobilfunkstandorte",
"mode_markers": "Marker",
"mode_progression": "Punktverlauf Anzeigen",
"mode_vapor": "Dampfspur",
"lbl_select_map_type": "Kartentyp Auswählen",
"lbl_select_map_activity": "Kartenaktivität Auswählen",
"lbl_time_interval": "Anzuzeigendes Zeitintervall",
"lbl_conversion_method": "Konvertierungsmethode",
"lbl_output_format": "Ausgabeformat",
"lbl_hemisphere": "Hemisphäre",
"lbl_datetime_column": "Datum / Uhrzeit Spalte",
"lbl_weight_column": "Gewichtungsspalte",
"lbl_time_column": "Zeitspalte",
"lbl_icon_style": "Kartensymbol-Stil Auswählen",
"lbl_icon_labels": "Kartensymbol-Beschriftungen",
"lbl_sector_color": "Sektorfarbe",
"lbl_sector_footprint": "Sektorgröße",
"lbl_sector_azimuth": "Sektor-Azimut",
"lbl_beam_width": "Sektorstrahlbreite",
"lbl_radius_meters": "Radius/Fußabdruck in Metern",
"lbl_radius_distance": "Radius/Abstand vom Punkt in Metern",
"lbl_tour_altitude": "Tour-Höhe (Meter)",
"lbl_linger_time": "Tour-Verweilzeit (Sekunden)",
"lbl_tour_tilt": "Tour-Neigung",
"lbl_camera_fly": "Kamera-Flugmodus",
"lbl_remove_rows": "Anzahl der zu entfernenden Anfangszeilen",
"lbl_radius_m": "Radius (m)",
"lbl_max_hotspots": "Maximale Hotspots",
"lbl_source_tz": "Quell-Zeitzone",
"lbl_target_tz": "Ziel-Zeitzone",
"interval_daily": "Täglich",
"interval_hourly": "Stündlich",
"interval_10min": "10 Minuten",
"interval_1min": "1 Minute",
"conv_timezone": "Zeitzonenkonvertierung",
"conv_offset": "Stundenversatz",
"northern": "Nördlich",
"southern": "Südlich",
"fmt_kml": "KML",
"fmt_kmz": "KMZ",
"btn_search": "Suchen",
"btn_run_hotspot": "Hotspot-Analyse Starten",
"btn_clear_hotspots": "Hotspots Löschen",
"btn_generate_kml": "KML Generieren",
"btn_detect_stops": "Aufenthalte Erkennen",
"btn_run_colocation": "Ko-Lokalisierungs-Analyse Starten",
"btn_convert_utm": "UTM Konvertieren",
"btn_apply_time_filter": "Zeitfilter Anwenden",
"btn_preview_conversion": "Konvertierung Vorschau",
"btn_apply_conversion": "Konvertierung Anwenden",
"chk_advanced": "Erweitert",
"chk_trim_chaining": "Verkettung Kürzen (Radius erzwingen)",
"chk_accuracy_radius": "Datensatz enthält Genauigkeits- oder Radiusdaten",
"chk_enable_time_filter": "Zeitfilter Aktivieren",
"chk_per_source": "Jede Quelldatei separat analysieren",
"chk_show_points": "Einzelne Punkte Anzeigen",
"chk_show_col_info": "Spalteninformationen Anzeigen",
"chk_footprint": "Datensatz enthält Radius-/Flächeninformationen",
"chk_path_line": "Reisepfadlinie einschließen",
"chk_kml_tour": "KML-Tour einschließen",
"chk_has_dates": "Datensatz enthält Datum-/Uhrzeitinformationen",
"chk_use_weight": "Gewichtungsspalte Verwenden",
"chk_declutter": "Bereinigung Aktivieren",
"hdr_advanced_analysis": "Erweiterte Analyse",
"hdr_stop_dwell": "Aufenthalts-Erkennung",
"hdr_colocation": "Ko-Lokalisierungs- / Nähe-Analyse",
"hdr_coord_converter": "Koordinatenformat-Konverter",
"hdr_hotspot_summary": "Hotspot-Zusammenfassung",
"hdr_hotspot_clocks": "Hotspot Taktische Uhren",
"hdr_design_kml": "KML-Karte Gestalten",
"hdr_tour_settings": "Tour-Einstellungen",
"hdr_filter_results": "Ergebnisse Filtern",
"hdr_declutter": "\U0001f3af Bereinigungseinstellungen",
"hdr_header_cleaning": "Kopfzeilen-Bereinigung",
"hdr_data_preview": "Datenvorschau",
"hdr_datetime_filter": "Datum/Uhrzeit-Filterung",
"hdr_data_declutter": "Datenbereinigung",
"hdr_timezone": "Zeitzonenkonvertierung",
"hdr_stop_map": "Karte der Aufenthaltsstandorte",
"hdr_coloc_map": "Ko-Lokalisierungskarte",
"hdr_custom_date": "Benutzerdefinierter Datumsbereich",
"msg_manage_data": "Importierte Daten Verwalten",
"msg_import_photos": "Fotostandorte Importieren (EXIF GPS)",
"msg_drawing_hint": "Zeichnen Sie eine Form (Polygon/Rechteck), um die Koordinaten sofort unter der Karte zu sehen.",
"msg_tips_hotspot": "**Tipps:** Radius erhöhen, wenn Besuche einige Dutzend Meter entfernt sind; verringern für engere Gruppierungen.",
"msg_coloc_requires": "Die Ko-Lokalisierungsanalyse erfordert **2 oder mehr Quelldateien** gleichzeitig.",
"msg_convert_single": "Einen Koordinatenwert zwischen Formaten konvertieren.",
"msg_convert_utm": "UTM-Koordinaten in Breiten-/Längengrad konvertieren.",
"msg_stops_no_time": "Keine Datum/Uhrzeit-Spalten erkannt. Die Aufenthaltserkennung benötigt Zeitdaten.",
"msg_coloc_no_time": "Keine Datum/Uhrzeit-Spalten erkannt. Die Ko-Lokalisierungsanalyse benötigt Zeitdaten.",
"msg_no_valid_records": "Keine gültigen Datensätze mit Koordinaten.",
"msg_map_name_required": "Kartenname oben angeben",
"msg_check_lat_lon": "Stellen Sie sicher, dass Ihre Daten Breiten- und Längengradenspalten haben",
"msg_photo_success": "GPS-Daten aus {} Foto(s) extrahiert",
"msg_no_gps_photos": "Aus den hochgeladenen Fotos konnten keine GPS-Daten extrahiert werden.",
"hdr_multisource_time": "Multi-Quellen-Zeitkonfiguration",
"hdr_sample_filtered": "Stichprobe der Gefilterten Daten",
"hdr_time_filter": "Zeitfilter",
"hdr_time_range_info": "Datenzeitbereich-Informationen",
"hdr_quick_filters": "Schnellfilter",
"hdr_review_data": "Importierte Daten Überprüfen",
"hdr_apply_time_filter": "Zeitfilter Anwenden",
"hdr_coord_diag": "Koordinatendiagnose der Geladenen Daten",
"btn_download_csv": "Gefilterte Daten als CSV Herunterladen",
"msg_params_changed": "Parameter geändert — 'Hotspot-Analyse Starten' zum Neuberechnen drücken.",
"qf_all_data": "Alle Daten",
"qf_24h": "Letzte 24 Stunden",
"qf_week": "Letzte Woche",
"qf_month": "Letzter Monat",
"qf_custom": "Benutzerdefinierter Bereich",
"lbl_quick_filter": "Voreinstellung oder benutzerdefinierten Bereich wählen:",
"lbl_choose_files": "Dateien zum Analysieren Auswählen",
"lbl_search_addr_ip": "Suchen (Adresse/IP)",
"lbl_place_name": "Ortsname oder Adresse",
"lbl_enter_coord": "Koordinate eingeben (GMS, GMG oder Dezimal)",
"lbl_time_col_optional": "Zeitspalte (optional - für taktische Uhr)",
"lbl_proximity_radius": "Näheradius (m)",
"lbl_time_window": "Zeitfenster (min)",
"lbl_min_duration": "Mindestdauer (min)",
"lbl_zone_number": "Zonennummer",
"lbl_zone_letter": "Zonenbuchstabe",
"lbl_easting": "Ostwert (m)",
"lbl_northing": "Nordwert (m)",
"lbl_date_time_col": "Datum/Uhrzeit-Spalte",
"lbl_radius_col_meters": "Radiusspalte (Meter)",
"msg_privacy": "Datenschutzerklärung und API-Nutzung",
},
"ru": {
"tab_review_ingest": "Просмотр данных",
"tab_time_filter": "Фильтр по времени",
"tab_declutter": "Упрощение",
"tab_timezone": "Конвертация часовых поясов",
"tab_geofence": "Создать геозону",
"tab_ip_mapping": "Картирование IP-адресов",
"tab_preview_kml": "Предпросмотр/KML-карта",
"tab_analysis_maps": "Аналитические карты",
"tab_create_geofence": "Создать геозону",
"tab_advanced_analysis": "Расширенный анализ",
"tab_stop_dwell": "Обнаружение остановок",
"tab_colocation": "Анализ совместного присутствия",
"tab_coord_tools": "Инструменты координат",
"tab_single_conv": "Одиночная конвертация",
"tab_utm_conv": "Конвертер UTM",
"map_clustered": "Сгруппированные маркеры",
"map_points_trails": "Точки и маршруты",
"map_hotspots": "Горячие точки",
"map_heatmap": "Тепловая карта",
"map_cell_sites": "Сотовые вышки",
"mode_markers": "Маркеры",
"mode_progression": "Показать последовательность точек",
"mode_vapor": "Дымовой след",
"lbl_select_map_type": "Выбрать тип карты",
"lbl_select_map_activity": "Выбрать действие на карте",
"lbl_time_interval": "Отображаемый интервал времени",
"lbl_conversion_method": "Метод конвертации",
"lbl_output_format": "Формат вывода",
"lbl_hemisphere": "Полушарие",
"lbl_datetime_column": "Столбец даты / времени",
"lbl_weight_column": "Столбец веса",
"lbl_time_column": "Столбец времени",
"lbl_icon_style": "Выбрать стиль значка карты",
"lbl_icon_labels": "Метки значков карты",
"lbl_sector_color": "Цвет сектора",
"lbl_sector_footprint": "Размер сектора",
"lbl_sector_azimuth": "Азимут сектора",
"lbl_beam_width": "Ширина луча сектора",
"lbl_radius_meters": "Радиус/Область в метрах",
"lbl_radius_distance": "Радиус/Расстояние от точки в метрах",
"lbl_tour_altitude": "Высота тура (метры)",
"lbl_linger_time": "Время задержки тура (секунды)",
"lbl_tour_tilt": "Наклон тура",
"lbl_camera_fly": "Режим полёта камеры",
"lbl_remove_rows": "Количество строк для удаления с начала",
"lbl_radius_m": "Радиус (м)",
"lbl_max_hotspots": "Максимум горячих точек",
"lbl_source_tz": "Исходный часовой пояс",
"lbl_target_tz": "Целевой часовой пояс",
"interval_daily": "Ежедневно",
"interval_hourly": "Ежечасно",
"interval_10min": "10 минут",
"interval_1min": "1 минута",
"conv_timezone": "Конвертация часовых поясов",
"conv_offset": "Смещение по часам",
"northern": "Северное",
"southern": "Южное",
"fmt_kml": "KML",
"fmt_kmz": "KMZ",
"btn_search": "Поиск",
"btn_run_hotspot": "Запустить анализ горячих точек",
"btn_clear_hotspots": "Очистить горячие точки",
"btn_generate_kml": "Создать KML",
"btn_detect_stops": "Обнаружить остановки",
"btn_run_colocation": "Запустить анализ совместного присутствия",
"btn_convert_utm": "Конвертировать UTM",
"btn_apply_time_filter": "Применить фильтр по времени",
"btn_preview_conversion": "Предпросмотр конвертации",
"btn_apply_conversion": "Применить конвертацию",
"chk_advanced": "Расширенный",
"chk_trim_chaining": "Ограничить цепочку (применить радиус)",
"chk_accuracy_radius": "Набор данных включает информацию о точности или радиусе",
"chk_enable_time_filter": "Включить фильтр по времени",
"chk_per_source": "Анализировать каждый исходный файл отдельно",
"chk_show_points": "Показать отдельные точки",
"chk_show_col_info": "Показать информацию о столбцах",
"chk_footprint": "Набор данных включает информацию о радиусе/площади",
"chk_path_line": "Включить линию маршрута",
"chk_kml_tour": "Включить KML-тур",
"chk_has_dates": "Набор данных включает информацию о дате/времени",
"chk_use_weight": "Использовать столбец веса",
"chk_declutter": "Включить упрощение",
"hdr_advanced_analysis": "Расширенный анализ",
"hdr_stop_dwell": "Обнаружение остановок",
"hdr_colocation": "Анализ совместного присутствия / близости",
"hdr_coord_converter": "Конвертер форматов координат",
"hdr_hotspot_summary": "Сводка по горячим точкам",
"hdr_hotspot_clocks": "Тактические часы горячих точек",
"hdr_design_kml": "Создание KML-карты",
"hdr_tour_settings": "Настройки тура",
"hdr_filter_results": "Фильтр результатов",
"hdr_declutter": "\U0001f3af Настройки упрощения",
"hdr_header_cleaning": "Очистка заголовков",
"hdr_data_preview": "Предпросмотр данных",
"hdr_datetime_filter": "Фильтрация по дате/времени",
"hdr_data_declutter": "Упрощение данных",
"hdr_timezone": "Конвертация часовых поясов",
"hdr_stop_map": "Карта местоположений остановок",
"hdr_coloc_map": "Карта совместного присутствия",
"hdr_custom_date": "Пользовательский диапазон дат",
"msg_manage_data": "Управление загруженными данными",
"msg_import_photos": "Импорт местоположений фотографий (EXIF GPS)",
"msg_drawing_hint": "Нарисуйте фигуру (полигон/прямоугольник), чтобы сразу увидеть координаты под картой.",
"msg_tips_hotspot": "**Советы:** Увеличьте радиус, если визиты разделены несколькими десятками метров; уменьшите для более тесных групп.",
"msg_coloc_requires": "Анализ совместного присутствия требует **2 или более исходных файлов**, загруженных одновременно.",
"msg_convert_single": "Конвертировать одно значение координаты между форматами.",
"msg_convert_utm": "Конвертировать координаты UTM в широту/долготу.",
"msg_stops_no_time": "Столбцы даты/времени не обнаружены. Обнаружение остановок требует данных о времени.",
"msg_coloc_no_time": "Столбцы даты/времени не обнаружены. Анализ совместного присутствия требует данных о времени.",
"msg_no_valid_records": "Нет допустимых записей с координатами.",
"msg_map_name_required": "Укажите название карты выше",
"msg_check_lat_lon": "Убедитесь, что в данных есть столбцы Широты и Долготы",
"msg_photo_success": "Данные GPS извлечены из {} фото",
"msg_no_gps_photos": "Не удалось извлечь данные GPS из загруженных фотографий.",
"hdr_multisource_time": "Конфигурация времени нескольких источников",
"hdr_sample_filtered": "Образец отфильтрованных данных",
"hdr_time_filter": "Фильтр по времени",
"hdr_time_range_info": "Информация о диапазоне времени данных",
"hdr_quick_filters": "Быстрые фильтры",
"hdr_review_data": "Просмотр загруженных данных",
"hdr_apply_time_filter": "Применить фильтр по времени",
"hdr_coord_diag": "Диагностика координат загруженных данных",
"btn_download_csv": "Скачать отфильтрованные данные в CSV",
"msg_params_changed": "Параметры изменены — нажмите 'Запустить анализ горячих точек' для пересчёта.",
"qf_all_data": "Все данные",
"qf_24h": "Последние 24 часа",
"qf_week": "Последняя неделя",
"qf_month": "Последний месяц",
"qf_custom": "Пользовательский диапазон",
"lbl_quick_filter": "Выберите предустановленный или пользовательский диапазон:",
"lbl_choose_files": "Выбрать файлы для анализа",
"lbl_search_addr_ip": "Поиск (Адрес/IP)",
"lbl_place_name": "Название места или адрес",
"lbl_enter_coord": "Введите координату (ГМС, ГМД или Десятичная)",
"lbl_time_col_optional": "Столбец времени (необязательно - для тактических часов)",
"lbl_proximity_radius": "Радиус близости (м)",
"lbl_time_window": "Временное окно (мин)",
"lbl_min_duration": "Минимальная длительность (мин)",
"lbl_zone_number": "Номер зоны",
"lbl_zone_letter": "Буква зоны",
"lbl_easting": "Восток (м)",
"lbl_northing": "Север (м)",
"lbl_date_time_col": "Столбец Даты/Времени",
"lbl_radius_col_meters": "Столбец радиуса (метры)",
"msg_privacy": "Заявление о конфиденциальности и использовании API",
},
}
def t(key: str) -> str:
"""Return the translated string for the current UI language."""
lang_code = st.session_state.get("_ui_lang_code", "en")
lang = TRANSLATIONS.get(lang_code, TRANSLATIONS["en"])
return lang.get(key, TRANSLATIONS["en"].get(key, key))
from polycircles import polycircles # creates kml polygons
import leafmap.foliumap as leafmap # maps
from leafmap.foliumap import plugins # maps
import geopandas
import folium # maps
from math import asin, atan2, cos, degrees, radians, sin # calculates shapes and polygons on sphere
from folium.plugins import Draw, Geocoder, TimestampedGeoJson, HeatMap
from streamlit_folium import st_folium # used to create geofences
import datetime
import geocoder # search bar for geofence, api calls for address and ip lookups
import gpxpy
import numpy as np
from dateutil import parser
import xml.etree.ElementTree as ET
import zipfile
from functools import lru_cache
from typing import Optional
try:
import pytz
except ImportError:
pytz = None
import hashlib
# Safe session-state helpers: using st.session_state before Streamlit initializes
# can raise runtime errors in some reload/timing scenarios. These wrappers
# catch those exceptions and provide safe fallbacks.
def safe_session_get(key, default=None):
try:
return st.session_state.get(key, default)
except Exception:
return default
def safe_session_set(key, value):
try:
st.session_state[key] = value
except Exception:
pass
# --- Persistence helpers to avoid storing large objects in session_state (Streamlit Cloud) ---
import pickle
import gzip
import tempfile
def persist_object_to_tempfile(obj) -> Optional[str]:
"""Persist object to a gzipped pickle and return filepath, or None on failure."""
try:
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.pkl.gz')
tmp.close()
with gzip.open(tmp.name, 'wb') as fh:
pickle.dump(obj, fh, protocol=pickle.HIGHEST_PROTOCOL)
return tmp.name
except Exception:
try:
if 'tmp' in locals() and os.path.exists(tmp.name):
os.unlink(tmp.name)
except Exception:
pass
return None
def load_persisted_object(path):
"""Load an object previously persisted with persist_object_to_tempfile. Return None on failure."""
try:
if not path or not isinstance(path, str):
return None
if not os.path.exists(path):
return None
with gzip.open(path, 'rb') as fh:
return pickle.load(fh)
except Exception:
return None
# -------------------------------------------------------------
# Performance/Stability Helpers (added v5 PERF)
# -------------------------------------------------------------
# Centralize expensive regex compilation so they aren't recompiled each call
IPV4_REGEX = re.compile(r'(?:\d{1,3}\.){3}\d{1,3}\b')
IPV6_REGEX = re.compile(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))')
def filter_valid_coordinates(df: pandas.DataFrame, lat_col: str = 'LATITUDE', lon_col: str = 'LONGITUDE'):
"""Return a cleaned copy of df with only valid numeric finite coordinates.
This consolidates previously duplicated logic (numeric coercion, NaN/inf removal)
and returns (clean_df, skipped_count).
"""
if df is None or df.empty:
return pandas.DataFrame(columns=df.columns if df is not None else []), 0
working = df.copy()
if lat_col not in working.columns or lon_col not in working.columns:
return pandas.DataFrame(columns=working.columns), len(working)
original = len(working)
# Drop obvious nulls first
working = working.dropna(subset=[lat_col, lon_col])
# Coerce numeric
working[lat_col] = pandas.to_numeric(working[lat_col], errors='coerce')
working[lon_col] = pandas.to_numeric(working[lon_col], errors='coerce')
# Remove NaN / inf
working = working[
working[lat_col].notna() & working[lon_col].notna() &
np.isfinite(working[lat_col]) & np.isfinite(working[lon_col])
]
clean = working.dropna(subset=[lat_col, lon_col]).reset_index(drop=True)
return clean, (original - len(clean))
def detect_and_set_header_from_rows(df: pandas.DataFrame, max_search_rows: int = 15) -> pandas.DataFrame:
"""Inspect the first `max_search_rows` rows of `df` to find a row that contains
both latitude and longitude column labels (case-insensitive). If found, promote
that row to be the DataFrame header and drop all rows above it. If not found,
ensure column names are strings so downstream .str operations won't fail.
Detection uses simple word-boundary regex for 'lat'/'latitude' and 'lon'/'longitude'.
"""
if df is None or df.empty:
return df
# Make a defensive copy for inspection
working = df.copy().reset_index(drop=True)
nrows = min(len(working), max_search_rows)
lat_pattern = r"\blat\b|\blatitude\b"
lon_pattern = r"\blon\b|\blongitude\b"
header_row_idx = None
for i in range(nrows):
# convert row values to strings and lowercase for matching
row = working.iloc[i].astype(str).fillna("").str.lower()
has_lat = row.str.contains(lat_pattern, regex=True, na=False).any()
has_lon = row.str.contains(lon_pattern, regex=True, na=False).any()
if has_lat and has_lon:
header_row_idx = i
break
if header_row_idx is not None:
# Use that row as header
new_columns = working.iloc[header_row_idx].astype(str).str.strip().tolist()
new_df = working.iloc[header_row_idx + 1 :].copy().reset_index(drop=True)
# Ensure column names are unique strings
new_df.columns = [str(c) for c in new_columns]
return new_df
# If no header row found, convert existing column names to strings
try:
working.columns = [str(c) for c in working.columns]
except Exception:
pass
return working
@st.cache_data(show_spinner=False)
def cached_ip_lookup(ip: str):
"""Cache individual IP lookups to avoid repeated API calls during a session."""
try:
return geocoder.ipinfo(ip).json
except Exception:
return None
now = datetime.datetime.now()
st.set_page_config(
page_title="Fetch v5.3",
#page_icon="🔴",
layout="wide",
initial_sidebar_state="expanded",
menu_items={}
)
logo = ("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")
_hdr_col, _lang_col = st.columns([7, 1])
with _hdr_col:
header_html = "<img src='data:image/png;base64,{}' class='img-fluid'>".format(logo)
st.markdown(header_html, unsafe_allow_html=True)
with _lang_col:
_LANG_OPTIONS = {"English": "en", "Español": "es", "Português": "pt", "Deutsch": "de", "Русский": "ru"}
_lang_sel = st.selectbox("🌐", list(_LANG_OPTIONS.keys()), key="_ui_lang_sel", label_visibility="collapsed")
st.session_state["_ui_lang_code"] = _LANG_OPTIONS[_lang_sel]
#Custom button color to bring prominence to executable actions
st.markdown("""
<style>
div.stButton > button:first-child {
background-color: #ff0000;
color:#ffffff;
}
div.stButton > button:hover {
background-color: #8b0000;
color:#ff0000;
}
</style>""", unsafe_allow_html=True)
#This removes Streamlit default settings icons
hide_streamlit_style = """
<style>
#MainMenu {visibility: visible;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
# Top-level Kepler export button removed. Use the Kepler download button shown below the embedded map.
### Global Variables ###
get_headings = ""
selected_encoding = ""
icon_options = ["Yellow Paddle", "Green Paddle", "Blue Paddle", "White Paddle", "Teal Paddle", "Red Paddle", "Yellow Pushpin", "White Pushpin", "Red Pushpin", "Square"]
selected_icon = {'Square' :'http://maps.google.com/mapfiles/kml/shapes/placemark_square.png','Yellow Pushpin' : "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png",'Red Pushpin' : "http://maps.google.com/mapfiles/kml/pushpin/red-pushpin.png",'White Pushpin' : "http://maps.google.com/mapfiles/kml/pushpin/wht-pushpin.png",'Red Paddle' : "http://maps.google.com/mapfiles/kml/paddle/red-circle.png",'Green Paddle' : "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png",'Blue Paddle' : "http://maps.google.com/mapfiles/kml/paddle/blu-circle.png",'Teal Paddle' : "http://maps.google.com/mapfiles/kml/paddle/ltblu-circle.png",'Yellow Paddle' : "http://maps.google.com/mapfiles/kml/paddle/ylw-circle.png",'White Paddle' : "http://maps.google.com/mapfiles/kml/paddle/wht-circle.png"}
invalid_ips = ['0', '10.', '127.0.0.1','172.16', '172.17', '172.18', '172.19', '172.2', '172.21', '172.22', '172.23', '172.24', '172.25',
'172.26', '172.27', '172.28', '172.29', '172.30', '172.31', '192.168', '169.254', "255.255" ,"fc00"]
# ---------------- Hotspot / Clustering Helpers ----------------
def compute_hotspots(df: pandas.DataFrame, radius_m: float, min_samples: int, time_col: Optional[str], trim_chaining: bool = True):
"""Run DBSCAN (haversine) on LATITUDE/LONGITUDE columns (meters radius) and return
(clusters_df, summary_df).
Notes
-----
DBSCAN's notion of a cluster allows *chaining*: points can be connected via a series
of <= eps links even if the overall diameter is >> eps. That can yield MAX_DISTANCE_M
much larger than the user-selected radius. When trim_chaining is True we post-filter
each cluster to retain only points within radius_m of the cluster centroid; any points
outside are re-labelled as noise. Clusters falling below min_samples after trimming
are discarded. This makes MAX_DISTANCE_M always <= radius_m (or very close due to
floating error) and matches an intuitive "circular hotspot" expectation.
"""
try:
from sklearn.cluster import DBSCAN # dynamic import in case installed after first run
except Exception as e:
raise RuntimeError("scikit-learn not available: install scikit-learn") from e
earth_radius_m = 6371000.0
eps = radius_m / earth_radius_m
coords_rad = np.radians(df[['LATITUDE','LONGITUDE']].to_numpy())
model = DBSCAN(eps=eps, min_samples=min_samples, metric='haversine')
labels = model.fit_predict(coords_rad)
df = df.copy()
df['HOTSPOT_ID'] = labels
clusters = df[df['HOTSPOT_ID'] != -1].copy()
if clusters.empty:
return df, pandas.DataFrame(columns=['HOTSPOT_ID','COUNT','CENTER_LAT','CENTER_LON','MAX_DISTANCE_M','RADIUS_INPUT_M','FIRST_OBS','LAST_OBS'])
earth_r = earth_radius_m
summary_rows = []