Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

218 linhas
12 KiB

  1. import os
  2. import json
  3. import streamlit as st
  4. import pandas as pd
  5. from PIL import Image
  6. from pathlib import Path
  7. import time
  8. import folium
  9. from streamlit_folium import st_folium
  10. import math
  11. import base64
  12. from io import BytesIO
  13. # --- CACHE IMMAGINE ---
  14. @st.cache_data
  15. def get_image_base64(img_path):
  16. img = Image.open(img_path).convert("RGBA")
  17. w, h = img.size
  18. buffered = BytesIO()
  19. img.save(buffered, format="PNG")
  20. img_str = base64.b64encode(buffered.getvalue()).decode("ascii")
  21. return f"data:image/png;base64,{img_str}", w, h
  22. # --- FUNZIONE SALVATAGGIO FISICO ---
  23. def force_save_json(path, data):
  24. try:
  25. with open(path, "w", encoding='utf-8') as f:
  26. json.dump(data, f, indent=4)
  27. return True
  28. except Exception as e:
  29. st.error(f"Errore scrittura disco: {e}")
  30. return False
  31. def show_mapper(cfg):
  32. MAPS_DIR = Path(cfg['maps']['map_dir'])
  33. SAMPLES_DIR = Path("/data/train/samples")
  34. JOBS_BASE = Path(cfg['collect_train']['jobs_dir'])
  35. PENDING_DIR = JOBS_BASE / "pending"
  36. ERROR_DIR = JOBS_BASE / "error"
  37. BEACONS_FILE = "/data/config/beacons.csv"
  38. [p.mkdir(parents=True, exist_ok=True) for p in [MAPS_DIR, SAMPLES_DIR, PENDING_DIR, ERROR_DIR]]
  39. # --- 1. GESTIONE UPLOAD ---
  40. # La logica ora gestisce stringhe per floor_id permettendo il carattere "-"
  41. maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0]
  42. for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])])
  43. with st.expander("📂 Carica Nuova Planimetria", expanded=not maps):
  44. c_up1, c_up2 = st.columns([1, 2])
  45. new_f_id = c_up1.text_input("ID Piano (es. 0, -1):", key="new_f_id_v21")
  46. up_file = c_up2.file_uploader("Immagine (PNG/JPG):", type=['png', 'jpg', 'jpeg'], key="up_v21")
  47. if st.button("🚀 SALVA PIANO", use_container_width=True):
  48. if new_f_id and up_file:
  49. ext = Path(up_file.name).suffix
  50. # Il nome file includerà il "-" se presente in new_f_id
  51. img_save_path = MAPS_DIR / f"{cfg['maps']['floor_prefix']}{new_f_id}{ext}"
  52. with open(img_save_path, "wb") as f:
  53. f.write(up_file.getbuffer())
  54. m_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{new_f_id}.json"
  55. force_save_json(m_path, {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0], "grid_size": 50})
  56. st.session_state.current_floor = new_f_id
  57. st.rerun()
  58. if not maps: return
  59. # --- 2. RIGA STATO ---
  60. c_p, c_s, c_o = st.columns([2.5, 2, 2])
  61. with c_p:
  62. s1, s2 = st.columns([1, 1.2])
  63. s1.markdown("<p style='padding-top:35px; font-weight:bold;'>Piano Attivo:</p>", unsafe_allow_html=True)
  64. def_idx = maps.index(st.session_state.current_floor) if st.session_state.get("current_floor") in maps else 0
  65. floor_id = s2.selectbox("", maps, index=def_idx, key="f_v21", label_visibility="collapsed")
  66. if st.session_state.get("prev_floor") != floor_id:
  67. st.session_state.cal_points = []
  68. st.session_state.temp_lat, st.session_state.temp_lng = None, None
  69. st.session_state.prev_floor = floor_id
  70. meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json"
  71. if meta_path.exists():
  72. with open(meta_path, "r") as f: meta = json.load(f)
  73. else:
  74. meta = {"pixel_ratio": 1.0, "calibrated": False, "origin": [0, 0]}
  75. with c_s: st.info(f"📏 Scala: {'✅' if meta['calibrated'] else '❌'}\n({meta['pixel_ratio']:.4f} px/cm)")
  76. with c_o: st.info(f"🎯 Origine: {'✅' if meta['origin'] != [0,0] else '❌'}\n(X:{meta['origin'][0]}, Y:{meta['origin'][1]})")
  77. if not meta["calibrated"] or meta["origin"] == [0, 0]:
  78. st.warning(f"💡 **Piano {floor_id} da configurare**: Esegui **Calibra** e imposta l'**Origine**.")
  79. # --- 3. IMPOSTAZIONI GLOBALI (Default disabilitati) ---
  80. st.markdown("---")
  81. g1, g2, g3, g4 = st.columns([1.2, 1.5, 1.2, 2])
  82. with g1:
  83. # Griglia disattivata di default per evitare interferenze iniziali
  84. show_grid = st.toggle("Griglia", value=False, key="grid_v21")
  85. with g2:
  86. grid_cm = st.select_slider("Passo (cm):", options=[25, 50, 100, 200], value=50, key="step_v21")
  87. with g3:
  88. # Stay Grid disattivato di default
  89. snap_on = st.toggle("Stay Grid", value=False, key="snap_v21")
  90. with g4:
  91. m_size = st.slider("Dimensione Marker:", 5, 20, 8, key="msize_v21")
  92. st.markdown("---")
  93. # --- 4. TOOLSET ---
  94. t1, t2, t3 = st.columns(3)
  95. if t1.button("📏 CALIBRA", use_container_width=True): st.session_state.map_tool = "Calibra"; st.session_state.cal_points = []
  96. if t2.button("🎯 SET ORIGINE", use_container_width=True): st.session_state.map_tool = "Origine"
  97. if t3.button("📡 RILEVA", use_container_width=True): st.session_state.map_tool = "Rileva"
  98. tool = st.session_state.get('map_tool', 'Rileva')
  99. # --- 5. MAPPA ---
  100. col_map, col_ui = st.columns([3, 1])
  101. with col_map:
  102. 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()))
  103. img_data, w, h = get_image_base64(img_p)
  104. bounds = [[0, 0], [h, w]]
  105. m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False)
  106. m.fit_bounds(bounds)
  107. m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0})
  108. folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds, interactive=True).add_to(m)
  109. # ORIGINE (Sempre visibile se impostata)
  110. if meta["origin"] != [0, 0]:
  111. ox, oy = meta["origin"]
  112. folium.CircleMarker(location=[oy, ox], radius=6, color="black", fill=True, zindex=1000, tooltip="Origine (0,0)").add_to(m)
  113. # GRIGLIA
  114. if show_grid and meta["calibrated"] and meta["origin"] != [0, 0]:
  115. px_step = grid_cm * meta["pixel_ratio"]
  116. ox, oy = meta["origin"]
  117. for x in sorted(list(range(int(ox), w, int(px_step))) + list(range(int(ox), 0, -int(px_step)))):
  118. folium.PolyLine([[0, x], [h, x]], color="blue", weight=1, opacity=0.1).add_to(m)
  119. for y in sorted(list(range(int(oy), h, int(px_step))) + list(range(int(oy), 0, -int(px_step)))):
  120. folium.PolyLine([[y, 0], [y, w]], color="blue", weight=1, opacity=0.1).add_to(m)
  121. # Disegno Punti Storici
  122. def draw_points(directory, color, shape="circle"):
  123. if not meta["calibrated"] or meta["origin"] == [0,0]: return
  124. for f in Path(directory).glob("*.csv"):
  125. try:
  126. df = pd.read_csv(f, sep=";")
  127. df.columns = [c.lower() for c in df.columns]
  128. # Confronto tra stringhe per gestire piani negativi e ID complessi
  129. if str(df.iloc[0].get('z', df.iloc[0].get('floor'))) == str(floor_id):
  130. px_x = (df.iloc[0]['x'] * meta["pixel_ratio"]) + meta["origin"][0]
  131. px_y = meta["origin"][1] - (df.iloc[0]['y'] * meta["pixel_ratio"])
  132. if shape == "circle": folium.CircleMarker(location=[px_y, px_x], radius=m_size, color=color, fill=True, fill_opacity=0.8).add_to(m)
  133. 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)
  134. 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)
  135. except: continue
  136. draw_points(SAMPLES_DIR, "green", "circle")
  137. draw_points(PENDING_DIR, "yellow", "diamond")
  138. draw_points(ERROR_DIR, "red", "square")
  139. # Feedback Visivo Click
  140. if tool == "Calibra" and st.session_state.get("cal_points"):
  141. for p in st.session_state.cal_points: folium.CircleMarker(location=p, radius=6, color="red", fill=True).add_to(m)
  142. if len(st.session_state.cal_points) == 2: folium.PolyLine(st.session_state.cal_points, color="red", weight=3).add_to(m)
  143. elif st.session_state.get("temp_lat") is not None:
  144. 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)
  145. out = st_folium(m, height=600, width=None, key=f"map_v21_{floor_id}_{tool}")
  146. click = out.get("last_clicked")
  147. if click:
  148. clat, clng = click["lat"], click["lng"]
  149. if tool == "Calibra":
  150. if len(st.session_state.get("cal_points", [])) < 2:
  151. st.session_state.cal_points.append([clat, clng]); st.rerun()
  152. else:
  153. if tool == "Rileva" and snap_on and meta["calibrated"]:
  154. px_step = grid_cm * meta["pixel_ratio"]
  155. st.session_state.temp_lng = meta["origin"][0] + round((clng - meta["origin"][0]) / px_step) * px_step
  156. st.session_state.temp_lat = meta["origin"][1] + round((clat - meta["origin"][1]) / px_step) * px_step
  157. else:
  158. st.session_state.temp_lat, st.session_state.temp_lng = clat, clng
  159. st.rerun()
  160. # --- 6. LOGICA UI ---
  161. with col_ui:
  162. st.write(f"### Tool: **{tool}**")
  163. err_files = list(ERROR_DIR.glob("*.csv"))
  164. if err_files and st.button(f"🗑️ PULISCI ERRORI", use_container_width=True):
  165. for f in err_files: os.remove(f); st.rerun()
  166. if tool == "Calibra":
  167. pts = st.session_state.get("cal_points", [])
  168. if len(pts) == 2:
  169. dist_cm = st.number_input("Distanza reale (cm):", value=100.0)
  170. if st.button("📏 SALVA SCALA", use_container_width=True, type="primary"):
  171. px_d = math.sqrt((pts[1][1]-pts[0][1])**2 + (pts[1][0]-pts[0][0])**2)
  172. meta.update({"pixel_ratio": px_d / dist_cm, "calibrated": True})
  173. if force_save_json(meta_path, meta): st.session_state.cal_points = []; st.rerun()
  174. elif st.session_state.get("temp_lat") is not None:
  175. px_x, px_y = st.session_state.temp_lng, st.session_state.temp_lat
  176. if tool == "Origine":
  177. st.metric("X (px)", int(px_x)); st.metric("Y (px)", int(px_y))
  178. if st.button("💾 SALVA ORIGINE", use_container_width=True, type="primary"):
  179. meta["origin"] = [int(px_x), int(px_y)]
  180. if force_save_json(meta_path, meta): st.rerun()
  181. elif tool == "Rileva" and meta["calibrated"]:
  182. rx = (px_x - meta["origin"][0]) / meta["pixel_ratio"]
  183. ry = (meta["origin"][1] - px_y) / meta["pixel_ratio"]
  184. sx, sy = int(round(rx)), int(round(ry))
  185. st.metric("X (cm)", sx); st.metric("Y (cm)", sy)
  186. if os.path.exists(BEACONS_FILE):
  187. b_df = pd.read_csv(BEACONS_FILE, sep=";")
  188. sel_b = st.selectbox("Beacon:", b_df.apply(lambda x: f"{x['BeaconName']} | {x['MAC']}", axis=1))
  189. if st.button("🚀 REGISTRA", use_container_width=True, type="primary"):
  190. b_name, b_mac = sel_b.split(" | ")
  191. id_str = f"{b_name}_{floor_id}_{sx}_{sy}"
  192. # Salvataggio Z come stringa per mantenere il "-"
  193. data = {"Position": id_str, "Floor": floor_id, "RoomName": b_name, "X": sx, "Y": sy, "Z": floor_id, "BeaconName": b_name, "MAC": b_mac}
  194. pd.DataFrame([data]).to_csv(PENDING_DIR / f"{id_str}.csv", sep=";", index=False); st.rerun()