import os import json import streamlit as st import pandas as pd from PIL import Image from pathlib import Path import time import folium from streamlit_folium import st_folium import math import base64 from io import BytesIO # --- CACHE IMMAGINE --- @st.cache_data def get_image_base64(img_path): img = Image.open(img_path).convert("RGBA") w, h = img.size buffered = BytesIO() img.save(buffered, format="PNG") img_str = base64.b64encode(buffered.getvalue()).decode("ascii") return f"data:image/png;base64,{img_str}", w, h # --- FUNZIONE SALVATAGGIO FISICO --- def force_save_json(path, data): try: with open(path, "w", encoding='utf-8') as f: json.dump(data, f, indent=4) return True except Exception as e: st.error(f"Errore scrittura disco: {e}") return False def show_mapper(cfg): # Configurazione percorsi MAPS_DIR = Path(cfg['maps']['map_dir']) TRAIN_BASE = Path("/data/train") JOBS_BASE = Path(cfg['collect_train']['jobs_dir']) # Riferimenti directory per Addestramento e Test SAMPLES_DIR = TRAIN_BASE / "samples" TEST_SAMPLES_DIR = TRAIN_BASE / "testsamples" PENDING_DIR = JOBS_BASE / "pending" ERROR_DIR = JOBS_BASE / "error" TEST_PENDING_DIR = TRAIN_BASE / "testjobs/pending" TEST_ERROR_DIR = TRAIN_BASE / "testjobs/error" # File configurazione BEACONS_FILE = cfg.get('paths', {}).get('beacons_csv', "/data/config/beacons.csv") GROUPS_FILE = "/data/config/beacongroup.csv" CSV_DELIM = cfg.get('paths', {}).get('csv_delimiter', ';') # Creazione directory se mancano for p in [MAPS_DIR, SAMPLES_DIR, TEST_SAMPLES_DIR, PENDING_DIR, ERROR_DIR, TEST_PENDING_DIR, TEST_ERROR_DIR]: p.mkdir(parents=True, exist_ok=True) # --- 1. GESTIONE UPLOAD --- maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0] for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])]) with st.expander("📂 Carica Nuova Planimetria", expanded=not maps): c_up1, c_up2 = st.columns([1, 2]) new_f_id = c_up1.text_input("ID Piano (es. 0, -1):", key="new_f_id_v21") up_file = c_up2.file_uploader("Immagine (PNG/JPG):", type=['png', 'jpg', 'jpeg'], key="up_v21") if st.button("🚀 SALVA PIANO", use_container_width=True): if new_f_id and up_file: ext = Path(up_file.name).suffix img_save_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{new_f_id}{ext}" with open(img_save_path, "wb") as f: f.write(up_file.getbuffer()) m_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{new_f_id}.json" force_save_json(m_path, {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50}) st.session_state.current_floor = new_f_id st.rerun() if not maps: return # --- 2. RIGA STATO --- c_p, c_s, c_o = st.columns([2.5, 2, 2]) with c_p: s1, s2 = st.columns([1, 1.2]) s1.markdown("
Piano Attivo:
", unsafe_allow_html=True) def_idx = maps.index(st.session_state.current_floor) if st.session_state.get("current_floor") in maps else 0 floor_id = s2.selectbox("", maps, index=def_idx, key="f_v21", label_visibility="collapsed") if st.session_state.get("prev_floor") != floor_id: st.session_state.cal_points = [] st.session_state.temp_lat, st.session_state.temp_lng = None, None st.session_state.prev_floor = floor_id meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json" if meta_path.exists(): with open(meta_path, "r") as f: meta = json.load(f) else: meta = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0]} with c_s: st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'}\n({meta['pixel_ratio']:.4f} px/cm)") with c_o: st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'}\n(X:{meta['origin'][0]}, Y:{meta['origin'][1]})") # --- 3. IMPOSTAZIONI GLOBALI --- st.markdown("---") g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2]) with g1: show_grid = st.toggle("Griglia", value=False, key="grid_v21") with g2: grid_cm = st.select_slider("Passo (cm):", options=[25, 50, 100, 200], value=50, key="step_v21") with g3: snap_on = st.toggle("Stay Grid", value=False, key="snap_v21") with g4: m_size = st.slider("Dimensione Marker:", 5, 20, 8, key="msize_v21") st.markdown("---") # --- 4. TOOLSET --- t1, t2, t3 = st.columns(3) if t1.button("📏 CALIBRA", use_container_width=True): st.session_state.map_tool = "Calibra"; st.session_state.cal_points = [] if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine" if t3.button("📡 RILEVA", use_container_width=True): st.session_state.map_tool = "Rileva" tool = st.session_state.get('map_tool', 'Rileva') # --- 5. MAPPA --- col_map, col_ui = st.columns([3, 1.2]) with col_map: img_p = next((MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}" for e in ['.png','.jpg','.jpeg'] if (MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}").exists())) img_data, w, h = get_image_base64(img_p) bounds = [[0, 0], [h, w]] m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False) m.fit_bounds(bounds) m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0}) folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds, interactive=True).add_to(m) if meta["origin"] != [0, 0]: ox, oy = meta["origin"] folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000).add_to(m) if show_grid and meta["calibrated"] and meta["origin"] != [0, 0]: px_step = grid_cm * meta["pixel_ratio"] ox, oy = meta["origin"] for x in sorted(list(range(int(ox), w, int(px_step))) + list(range(int(ox), 0, -int(px_step)))): folium.PolyLine([[0, x], [h, x]], color="blue", weight=1, opacity=0.1).add_to(m) for y in sorted(list(range(int(oy), h, int(px_step))) + list(range(int(oy), 0, -int(px_step)))): folium.PolyLine([[y, 0], [y, w]], color="blue", weight=1, opacity=0.1).add_to(m) # --- DISEGNO PUNTI STORICI --- def draw_historical_samples(directory, color, shape="circle", is_test=False): if not meta["calibrated"] or meta["origin"] == [0,0]: return for f in Path(directory).glob("*.csv"): try: parts = f.stem.split('_') if len(parts) < 4: continue fz = parts[-3] fx = float(parts[-2].replace(',', '.')) fy = float(parts[-1].replace(',', '.')) if str(fz) == str(floor_id): px_x = (fx * meta["pixel_ratio"]) + meta["origin"][0] px_y = meta["origin"][1] - (fy * meta["pixel_ratio"]) dash = "5, 5" if is_test else None weight = 3 if is_test else 1 folium.CircleMarker( location=[px_y, px_x], radius=m_size, color=color, weight=weight, dash_array=dash, fill=True, fill_opacity=0.7 ).add_to(m) except: continue draw_historical_samples(SAMPLES_DIR, "green", "circle", is_test=False) draw_historical_samples(TEST_SAMPLES_DIR, "#00FFFF", "circle", is_test=True) def draw_jobs(directory, color, shape="diamond", is_test=False): if not meta["calibrated"] or meta["origin"] == [0,0]: return for f in Path(directory).glob("*.csv"): try: df = pd.read_csv(f, sep=";") df.columns = [c.lower() for c in df.columns] row = df.iloc[0] z_val = str(row.get('z', row.get('floor'))) if z_val == str(floor_id): px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0] px_y = meta["origin"][1] - (row['y'] * meta["pixel_ratio"]) dash = "3, 3" if is_test else None folium.RegularPolygonMarker( location=[px_y, px_x], number_of_sides=4, rotation=45 if shape=="diamond" else 0, radius=m_size, color=color, weight=2, dash_array=dash, fill=True ).add_to(m) except: continue draw_jobs(PENDING_DIR, "orange", "diamond", False) draw_jobs(TEST_PENDING_DIR, "#f1c40f", "diamond", True) draw_jobs(ERROR_DIR, "red", "square", False) draw_jobs(TEST_ERROR_DIR, "#e74c3c", "square", True) if tool == "Calibra" and st.session_state.get("cal_points"): for p in st.session_state.cal_points: folium.CircleMarker(location=p, radius=6, color="red", fill=True).add_to(m) if len(st.session_state.cal_points) == 2: folium.PolyLine(st.session_state.cal_points, color="red", weight=3).add_to(m) elif st.session_state.get("temp_lat") is not None: folium.CircleMarker(location=[st.session_state.temp_lat, st.session_state.temp_lng], radius=m_size+2, color="blue", fill=True, zindex=500).add_to(m) out = st_folium(m, height=600, width=None, key=f"map_v24_{floor_id}_{tool}") click = out.get("last_clicked") if click: clat, clng = click["lat"], click["lng"] if tool == "Calibra": if len(st.session_state.get("cal_points", [])) < 2: st.session_state.cal_points.append([clat, clng]); st.rerun() else: if tool == "Rileva" and snap_on and meta["calibrated"]: px_step = grid_cm * meta["pixel_ratio"] st.session_state.temp_lng = meta["origin"][0] + round((clng - meta["origin"][0]) / px_step) * px_step st.session_state.temp_lat = meta["origin"][1] + round((clat - meta["origin"][1]) / px_step) * px_step else: st.session_state.temp_lat, st.session_state.temp_lng = clat, clng st.rerun() # --- 6. LOGICA UI --- with col_ui: st.write(f"### Tool: **{tool}**") if tool == "Calibra": pts = st.session_state.get("cal_points", []) if len(pts) == 2: dist_cm = st.number_input("Distanza reale (cm):", value=100.0) if st.button("📏 SALVA SCALA", use_container_width=True, type="primary"): px_d = math.sqrt((pts[1][1]-pts[0][1])**2 + (pts[1][0]-pts[0][0])**2) meta.update({"pixel_ratio": px_d / dist_cm, "calibrated": True}) if force_save_json(meta_path, meta): st.session_state.cal_points = []; st.rerun() elif st.session_state.get("temp_lat") is not None: px_x, px_y = st.session_state.temp_lng, st.session_state.temp_lat if tool == "Origine": st.metric("X (px)", int(px_x)); st.metric("Y (px)", int(px_y)) if st.button("💾 SALVA ORIGINE", use_container_width=True, type="primary"): meta["origin"] = [int(px_x), int(px_y)] if force_save_json(meta_path, meta): st.rerun() elif tool == "Rileva" and meta["calibrated"]: rx = (px_x - meta["origin"][0]) / meta["pixel_ratio"] ry = (meta["origin"][1] - px_y) / meta["pixel_ratio"] sx, sy = int(round(rx)), int(round(ry)) # --- OTTIMIZZAZIONE SPAZIO: X e Y SU STESSA RIGA --- c_c1, c_c2 = st.columns(2) c_c1.metric("X (cm)", sx) c_c2.metric("Y (cm)", sy) # Divisore compresso con margine ridotto st.markdown("