diff --git a/mapa_incidencias.py b/mapa_incidencias.py new file mode 100644 index 0000000..0d19b54 --- /dev/null +++ b/mapa_incidencias.py @@ -0,0 +1,503 @@ +import pandas as pd +import folium +from folium.plugins import HeatMap +import json +import zipfile +import tempfile +import os +import xml.etree.ElementTree as ET +from collections import Counter +from datetime import datetime + +def cargar_y_analizar_todos_datos(ruta_incidencias): + """ + Carga datos y analiza TODOS los tipos de incidentes + """ + df = pd.read_csv(ruta_incidencias) + print(f"📊 Datos cargados: {len(df)} registros") + + # Análisis de TODOS los tipos de incidente + contador_tipos = Counter(df['tipo_incidente']) + total_incidencias = len(df) + + # Ordenar por frecuencia descendente + todos_tipos = [] + for tipo, count in contador_tipos.most_common(): + porcentaje = (count / total_incidencias) * 100 + todos_tipos.append({ + 'tipo': tipo, + 'count': count, + 'porcentaje': porcentaje + }) + + print(f"🎯 Se analizaron {len(todos_tipos)} tipos de incidentes diferentes") + + # Análisis temporal por tipo (si hay fechas) + tendencias = {} + if 'fincidencia' in df.columns: + df['fecha'] = pd.to_datetime(df['fincidencia'], errors='coerce') + df = df.dropna(subset=['fecha']) + df['mes'] = df['fecha'].dt.to_period('M') + + for item in todos_tipos: + tipo = item['tipo'] + datos_tipo = df[df['tipo_incidente'] == tipo] + tendencia_mensual = datos_tipo.groupby('mes').size() + + # Calcular tendencia (último mes vs penúltimo mes) + if len(tendencia_mensual) >= 2: + ultimo_mes = tendencia_mensual.iloc[-1] + penultimo_mes = tendencia_mensual.iloc[-2] + cambio = ultimo_mes - penultimo_mes + porcentaje_cambio = (cambio / penultimo_mes * 100) if penultimo_mes > 0 else 0 + else: + ultimo_mes = tendencia_mensual.iloc[-1] if len(tendencia_mensual) > 0 else 0 + penultimo_mes = 0 + cambio = 0 + porcentaje_cambio = 0 + + tendencias[tipo] = { + 'total': item['count'], + 'porcentaje': item['porcentaje'], + 'tendencia_mensual': tendencia_mensual, + 'ultimo_mes': ultimo_mes, + 'penultimo_mes': penultimo_mes, + 'cambio': cambio, + 'porcentaje_cambio': porcentaje_cambio + } + else: + # Si no hay fechas, solo mostrar datos básicos + for item in todos_tipos: + tipo = item['tipo'] + tendencias[tipo] = { + 'total': item['count'], + 'porcentaje': item['porcentaje'], + 'ultimo_mes': 0, + 'penultimo_mes': 0, + 'cambio': 0, + 'porcentaje_cambio': 0 + } + + # Procesar coordenadas para TODOS los tipos + coordenadas_por_tipo = {} + for item in todos_tipos: + coordenadas_por_tipo[item['tipo']] = [] + + for idx, row in df.iterrows(): + try: + coord_str = str(row['lat_long']).strip('() ') + if coord_str and coord_str != 'nan': + partes = coord_str.split(',') + if len(partes) == 2: + lat = float(partes[0].strip()) + lon = float(partes[1].strip()) + tipo_incidente = row['tipo_incidente'] + + if 15.0 < lat < 18.0 and -98.0 < lon < -94.0: + coordenadas_por_tipo[tipo_incidente].append([lat, lon]) + except: + continue + + return df, coordenadas_por_tipo, todos_tipos, tendencias + +def kmz_a_geojson_simple(ruta_kmz): + """ + Convierte KMZ a GeoJSON de forma simple y segura + """ + try: + with zipfile.ZipFile(ruta_kmz, 'r') as kmz: + kml_files = [f for f in kmz.namelist() if f.endswith('.kml')] + if not kml_files: + return None + + with tempfile.TemporaryDirectory() as temp_dir: + kmz.extract(kml_files[0], temp_dir) + kml_path = os.path.join(temp_dir, kml_files[0]) + return parsear_kml_manual(kml_path) + + except Exception as e: + print(f"❌ Error procesando KMZ: {e}") + return None + +def parsear_kml_manual(ruta_kml): + """ + Parsea KML manualmente + """ + try: + tree = ET.parse(ruta_kml) + root = tree.getroot() + ns = {'kml': 'http://www.opengis.net/kml/2.2'} + + features = [] + for placemark in root.findall('.//kml:Placemark', ns): + feature = { + 'type': 'Feature', + 'properties': {}, + 'geometry': {'type': 'Point', 'coordinates': [0, 0]} + } + + name_elem = placemark.find('kml:name', ns) + if name_elem is not None: + feature['properties']['name'] = name_elem.text + + coords_elem = placemark.find('.//kml:coordinates', ns) + if coords_elem is not None and coords_elem.text: + try: + coords_text = coords_elem.text.strip() + parts = coords_text.split(',') + if len(parts) >= 2: + lon = float(parts[0]) + lat = float(parts[1]) + feature['geometry']['coordinates'] = [lon, lat] + features.append(feature) + except ValueError: + continue + + return { + 'type': 'FeatureCollection', + 'features': features + } + + except Exception as e: + print(f"❌ Error parseando KML: {e}") + return None + +def crear_mapa_completo(ruta_incidencias, ruta_camaras_kmz=None, ruta_vehiculos_kmz=None): + """ + Crea mapa con análisis completo de TODOS los tipos de incidentes + """ + df, coordenadas_por_tipo, todos_tipos, tendencias = cargar_y_analizar_todos_datos(ruta_incidencias) + + # Calcular centro del mapa + todas_coordenadas = [] + for coords in coordenadas_por_tipo.values(): + todas_coordenadas.extend(coords) + + if not todas_coordenadas: + return None, df, todos_tipos, tendencias + + lats = [coord[0] for coord in todas_coordenadas] + lons = [coord[1] for coord in todas_coordenadas] + centro = [sum(lats)/len(lats), sum(lons)/len(lons)] + + m = folium.Map(location=centro, zoom_start=11) + + # Generar paleta de colores más amplia + colores_base = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', + '#FFA07A', '#20B2AA', '#778899', '#DEB887', '#5F9EA0', + '#FF69B4', '#BA55D3', '#9370DB', '#3CB371', '#FFD700'] + + # Extender paleta si hay más tipos + colores = colores_base + if len(todos_tipos) > len(colores_base): + import colorsys + colores_extra = [] + for i in range(len(todos_tipos) - len(colores_base)): + hue = i / (len(todos_tipos) - len(colores_base)) + rgb = colorsys.hsv_to_rgb(hue, 0.8, 0.9) + color = '#{:02x}{:02x}{:02x}'.format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255)) + colores_extra.append(color) + colores = colores_base + colores_extra + + # Capa base de heatmap (todos los tipos) + todas_las_coordenadas = [] + for coords in coordenadas_por_tipo.values(): + todas_las_coordenadas.extend(coords) + + HeatMap(todas_las_coordenadas, radius=15, blur=12, min_opacity=0.3).add_to(m) + + # Cargar capas KMZ + capas_kmz = {} + if ruta_camaras_kmz and os.path.exists(ruta_camaras_kmz): + print("📷 Cargando cámaras KMZ...") + capas_kmz['camaras'] = kmz_a_geojson_simple(ruta_camaras_kmz) + + if ruta_vehiculos_kmz and os.path.exists(ruta_vehiculos_kmz): + print("🚗 Cargando vehículos KMZ...") + capas_kmz['vehiculos'] = kmz_a_geojson_simple(ruta_vehiculos_kmz) + + # Añadir marcadores KMZ + for capa_nombre, geojson in capas_kmz.items(): + if geojson: + color = 'black' if capa_nombre == 'camaras' else 'blue' + icono = 'camera' if capa_nombre == 'camaras' else 'car' + etiqueta = '📹' if capa_nombre == 'camaras' else '🚗' + + for feature in geojson['features']: + coords = feature['geometry']['coordinates'] + nombre = feature['properties'].get('name', capa_nombre.title()) + + folium.Marker( + [coords[1], coords[0]], + popup=f"{etiqueta} {nombre}", + icon=folium.Icon(color=color, icon=icono, prefix='fa') + ).add_to(m) + + # Crear HTML para panel completo + crear_panel_completo(m, todos_tipos, tendencias, colores, len(df)) + + return m, df, todos_tipos, tendencias + +def crear_panel_completo(m, todos_tipos, tendencias, colores, total_incidencias): + """ + Crea panel de análisis completo de TODOS los tipos + """ + # HTML para el panel completo + panel_html = f''' +