|
|
|
@@ -1,49 +1,217 @@ |
|
|
|
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 |
|
|
|
|
|
|
|
def get_image_base64(img): |
|
|
|
# --- 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() |
|
|
|
return f"data:image/png;base64,{img_str}" |
|
|
|
img_str = base64.b64encode(buffered.getvalue()).decode("ascii") |
|
|
|
return f"data:image/png;base64,{img_str}", w, h |
|
|
|
|
|
|
|
def show_mapper_v2(cfg): |
|
|
|
# ... (caricamento meta e percorsi come nel tuo file originale) ... |
|
|
|
|
|
|
|
img_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}.png" |
|
|
|
img = Image.open(img_path).convert("RGBA") |
|
|
|
img_width, img_height = img.size |
|
|
|
img_b64 = get_image_base64(img) |
|
|
|
# --- 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 |
|
|
|
|
|
|
|
# Prepariamo la lista dei punti esistenti (Punto 6 delle specifiche) |
|
|
|
dots_data = [] |
|
|
|
# Qui cicla sui tuoi file CSV e popola dots_data con {x, y, relX, relY, status} |
|
|
|
|
|
|
|
# Integrazione del componente HTML |
|
|
|
# Carichiamo il JS dal file esterno |
|
|
|
with open("leaflet_bridge.js", "r") as f: |
|
|
|
js_code = f.read() |
|
|
|
|
|
|
|
html_content = f""" |
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> |
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> |
|
|
|
<div id="map" style="height: 600px; width: 100%;"></div> |
|
|
|
<script> |
|
|
|
{js_code} |
|
|
|
initMap({{ |
|
|
|
imgUrl: "{img_b64}", |
|
|
|
width: {img_width}, |
|
|
|
height: {img_height}, |
|
|
|
meta: {json.dumps(meta)}, |
|
|
|
dots: {json.dumps(dots_data)}, |
|
|
|
dot_size: {dot_size} |
|
|
|
}}); |
|
|
|
</script> |
|
|
|
""" |
|
|
|
|
|
|
|
# Il componente restituisce il valore di window.parent.postMessage |
|
|
|
result = components.html(html_content, height=650) |
|
|
|
def show_mapper(cfg): |
|
|
|
MAPS_DIR = Path(cfg['maps']['map_dir']) |
|
|
|
SAMPLES_DIR = Path("/data/train/samples") |
|
|
|
JOBS_BASE = Path(cfg['collect_train']['jobs_dir']) |
|
|
|
PENDING_DIR = JOBS_BASE / "pending" |
|
|
|
ERROR_DIR = JOBS_BASE / "error" |
|
|
|
BEACONS_FILE = "/data/config/beacons.csv" |
|
|
|
[p.mkdir(parents=True, exist_ok=True) for p in [MAPS_DIR, SAMPLES_DIR, PENDING_DIR, ERROR_DIR]] |
|
|
|
|
|
|
|
# --- 1. GESTIONE UPLOAD --- |
|
|
|
# La logica ora gestisce stringhe per floor_id permettendo il carattere "-" |
|
|
|
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 result: |
|
|
|
st.write(f"Posizione catturata: {result}") |
|
|
|
# Qui inserisci la tua logica di salvataggio CSV che avevi nel punto 7 |
|
|
|
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 |
|
|
|
# Il nome file includerà il "-" se presente in new_f_id |
|
|
|
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("<p style='padding-top:35px; font-weight:bold;'>Piano Attivo:</p>", 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]})") |
|
|
|
|
|
|
|
if not meta["calibrated"] or meta["origin"] == [0, 0]: |
|
|
|
st.warning(f"💡 **Piano {floor_id} da configurare**: Esegui **Calibra** e imposta l'**Origine**.") |
|
|
|
|
|
|
|
# --- 3. IMPOSTAZIONI GLOBALI (Default disabilitati) --- |
|
|
|
st.markdown("---") |
|
|
|
g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2]) |
|
|
|
with g1: |
|
|
|
# Griglia disattivata di default per evitare interferenze iniziali |
|
|
|
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: |
|
|
|
# Stay Grid disattivato di default |
|
|
|
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]) |
|
|
|
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) |
|
|
|
|
|
|
|
# ORIGINE (Sempre visibile se impostata) |
|
|
|
if meta["origin"] != [0, 0]: |
|
|
|
ox, oy = meta["origin"] |
|
|
|
folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000, tooltip="Origine (0,0)").add_to(m) |
|
|
|
|
|
|
|
# GRIGLIA |
|
|
|
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_points(directory, color, shape="circle"): |
|
|
|
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] |
|
|
|
# Confronto tra stringhe per gestire piani negativi e ID complessi |
|
|
|
if str(df.iloc[0].get('z', df.iloc[0].get('floor'))) == str(floor_id): |
|
|
|
px_x = (df.iloc[0]['x'] * meta["pixel_ratio"]) + meta["origin"][0] |
|
|
|
px_y = meta["origin"][1] - (df.iloc[0]['y'] * meta["pixel_ratio"]) |
|
|
|
if shape == "circle": folium.CircleMarker(location=[px_y, px_x], radius=m_size, color=color, fill=True, fill_opacity=0.8).add_to(m) |
|
|
|
elif shape == "square": folium.RegularPolygonMarker(location=[px_y, px_x], number_of_sides=4, radius=m_size, color="black", weight=2, fill=True, fill_color=color).add_to(m) |
|
|
|
elif shape == "diamond": folium.RegularPolygonMarker(location=[px_y, px_x], number_of_sides=4, rotation=45, radius=m_size, color="black", weight=1, fill=True, fill_color=color).add_to(m) |
|
|
|
except: continue |
|
|
|
|
|
|
|
draw_points(SAMPLES_DIR, "green", "circle") |
|
|
|
draw_points(PENDING_DIR, "yellow", "diamond") |
|
|
|
draw_points(ERROR_DIR, "red", "square") |
|
|
|
|
|
|
|
# Feedback Visivo Click |
|
|
|
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_v21_{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}**") |
|
|
|
err_files = list(ERROR_DIR.glob("*.csv")) |
|
|
|
if err_files and st.button(f"🗑️ PULISCI ERRORI", use_container_width=True): |
|
|
|
for f in err_files: os.remove(f); st.rerun() |
|
|
|
|
|
|
|
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)) |
|
|
|
st.metric("X (cm)", sx); st.metric("Y (cm)", sy) |
|
|
|
if os.path.exists(BEACONS_FILE): |
|
|
|
b_df = pd.read_csv(BEACONS_FILE, sep=";") |
|
|
|
sel_b = st.selectbox("Beacon:", b_df.apply(lambda x: f"{x['BeaconName']} | {x['MAC']}", axis=1)) |
|
|
|
if st.button("🚀 REGISTRA", use_container_width=True, type="primary"): |
|
|
|
b_name, b_mac = sel_b.split(" | ") |
|
|
|
id_str = f"{b_name}_{floor_id}_{sx}_{sy}" |
|
|
|
# Salvataggio Z come stringa per mantenere il "-" |
|
|
|
data = {"Position": id_str, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": b_mac} |
|
|
|
pd.DataFrame([data]).to_csv(PENDING_DIR / f"{id_str}.csv", sep=";", index=False); st.rerun() |