import os import json import streamlit as st import pandas as pd from PIL import Image, ImageDraw from pathlib import Path from streamlit_drawable_canvas import st_canvas import time def show_mapper(cfg): # --- 1. CONFIGURAZIONE PERCORSI --- MAPS_DIR = Path(cfg['maps']['map_dir']) SAMPLES_DIR = Path(cfg['train']['samples_dir']) PENDING_DIR = Path(cfg['collect_train']['jobs_dir']) / "pending" BEACONS_FILE = "/data/config/beacons.csv" [p.mkdir(parents=True, exist_ok=True) for p in [MAPS_DIR, SAMPLES_DIR, PENDING_DIR]] def load_map_metadata(f_id): meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{f_id}.json" defaults = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50} if meta_path.exists(): with open(meta_path, "r") as f: return {**defaults, **json.load(f)} return defaults st.subheader("πŸ—ΊοΈ Gestione Mappatura e Rilevamento") # Selezione Piano maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0] for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])]) if not maps: st.error("Nessuna mappa trovata."); return floor_id = st.selectbox("Seleziona Piano (Z):", maps) meta = load_map_metadata(floor_id) # --- 2. STATO SISTEMA --- c_s1, c_s2 = st.columns(2) with c_s1: st.info(f"πŸ“ Scala: {'βœ…' if meta['calibrated'] else '❌'} ({meta['pixel_ratio']:.4f} px/cm)") with c_s2: st.info(f"🎯 Origine: {'βœ…' if meta['origin'] != [0,0] else '❌'} (X:{meta['origin'][0]}, Y:{meta['origin'][1]})") # --- 3. CONTROLLI VISUALIZZAZIONE --- with st.expander("🎨 Opzioni Visualizzazione", expanded=True): c_v1, c_v2, c_v3 = st.columns(3) zoom = c_v1.slider("πŸ” Zoom Mappa", 0.1, 4.0, 1.0, 0.1) dot_size = c_v2.slider("πŸ”΅/🟒 Dimensione Punti", 10, 100, 40) mode = c_v3.radio("ModalitΓ  Interazione:", ["πŸ–οΈ NAVIGA", "🎯 AZIONE"], horizontal=True) # --- 4. SELEZIONE STRUMENTO --- t1, t2, t3 = st.columns(3) if t1.button("πŸ“ IMPOSTA SCALA", use_container_width=True): st.session_state.map_tool = "Calibra" if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine" is_ready = meta.get("calibrated", False) and meta["origin"] != [0, 0] if t3.button("πŸ“‘ RILEVA", use_container_width=True, disabled=not is_ready): st.session_state.map_tool = "Rileva" tool = st.session_state.get('map_tool', 'Rileva') st.write(f"Strumento attivo: **{tool}**") # --- 5. PREPARAZIONE IMMAGINE E STORICO --- img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" if not img_path.exists(): img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.jpg" img = Image.open(img_path).convert("RGBA") draw = ImageDraw.Draw(img) if is_ready: samples_files = list(SAMPLES_DIR.glob(f"{floor_id}_*.csv")) pending_files = list(PENDING_DIR.glob(f"{floor_id}_*.csv")) def draw_points(files, color): for f in files: try: p = f.stem.split("_") px = (int(p[1]) * meta["pixel_ratio"]) + meta["origin"][0] py = (int(p[2]) * meta["pixel_ratio"]) + meta["origin"][1] r = dot_size // 2 draw.ellipse([px-r, py-r, px+r, py+r], fill=color, outline="white", width=2) except: continue draw_points(samples_files, "#228B22") # Verde: Completati draw_points(pending_files, "#0000FF") # Blu: In corso # --- 6. CANVAS --- d_mode = "transform" if mode == "πŸ–οΈ NAVIGA" else ("line" if tool == "Calibra" else "point") canvas_result = st_canvas( background_image=img, height=int(img.size[1] * zoom), width=int(img.size[0] * zoom), drawing_mode=d_mode, display_toolbar=True, update_streamlit=True, key=f"canvas_v10_{floor_id}_{zoom}_{mode}_{tool}", ) # --- 7. LOGICA AZIONI E SALVATAGGIO CSV --- if mode == "🎯 AZIONE" and canvas_result.json_data and canvas_result.json_data["objects"]: last = canvas_result.json_data["objects"][-1] raw_x, raw_y = last["left"] / zoom, last["top"] / zoom if tool == "Calibra" and last["type"] == "line": px_dist = ((last["x2"] - last["x1"])**2 + (last["y2"] - last["y1"])**2)**0.5 / zoom dist_cm = st.number_input("Distanza reale (cm):", value=100.0) if st.button("APPLICA SCALA"): meta.update({"pixel_ratio": px_dist / dist_cm, "calibrated": True}) with open(MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json", "w") as f: json.dump(meta, f) st.rerun() elif tool == "Origine": if st.button(f"Fissa Origine qui: {int(raw_x)}, {int(raw_y)}"): meta["origin"] = [int(raw_x), int(raw_y)] with open(MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json", "w") as f: json.dump(meta, f) st.rerun() elif tool == "Rileva" and is_ready: dx = (raw_x - meta["origin"][0]) / meta["pixel_ratio"] dy = (raw_y - meta["origin"][1]) / meta["pixel_ratio"] grid = meta.get("grid_size", 50) sx, sy = int(round(dx/grid)*grid), int(round(dy/grid)*grid) st.write(f"### πŸ“ Punto: {sx}cm, {sy}cm") if os.path.exists(BEACONS_FILE): b_df = pd.read_csv(BEACONS_FILE, sep=";") sel_b = st.selectbox("Seleziona Beacon:", b_df.apply(lambda x: f"{x['BeaconName']} | {x['MAC']}", axis=1)) if st.button("πŸš€ REGISTRA LETTURA", type="primary", use_container_width=True): b_name, b_mac = sel_b.split(" | ") fname = f"{floor_id}_{sx}_{sy}.csv" # --- FIX SINTASSI CSV --- # Formato: Position;Floor;RoomName;X;Y;Z;BeaconName;MAC data_out = { "Position": f"P_{sx}_{sy}", "Floor": floor_id, "RoomName": "Area_Rilevazione", "X": sx, "Y": sy, "Z": 0, "BeaconName": b_name, "MAC": b_mac } pd.DataFrame([data_out]).to_csv(PENDING_DIR / fname, index=False, sep=";") st.toast(f"Punto Blu registrato!") time.sleep(1); st.rerun()