Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
3ee46e7
1
Parent(s): 950e36a
Refina mapas Leaflet e prepara nova release Windows
Browse files- backend/app/api/visualizacao.py +11 -1
- backend/app/core/map_layers.py +82 -38
- backend/app/core/visualizacao/app.py +1 -1
- backend/app/core/visualizacao/map_payload.py +828 -0
- backend/app/services/elaboracao_service.py +49 -12
- backend/app/services/pesquisa_service.py +279 -2
- backend/app/services/trabalhos_tecnicos_service.py +129 -0
- backend/app/services/visualizacao_service.py +194 -19
- frontend/package-lock.json +126 -0
- frontend/package.json +4 -0
- frontend/src/api.js +4 -0
- frontend/src/components/AvaliacaoTab.jsx +4 -1
- frontend/src/components/ElaboracaoTab.jsx +46 -25
- frontend/src/components/LeafletMapFrame.jsx +1004 -0
- frontend/src/components/MapFrame.jsx +6 -1
- frontend/src/components/PesquisaTab.jsx +215 -41
- frontend/src/components/RepositorioTab.jsx +5 -1
- frontend/src/components/TrabalhosTecnicosTab.jsx +5 -1
- frontend/src/components/VisualizacaoTab.jsx +205 -44
- frontend/src/main.jsx +2 -0
- frontend/src/styles.css +362 -3
backend/app/api/visualizacao.py
CHANGED
|
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|
| 3 |
import os
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
-
from fastapi import APIRouter, File, Form, Request, UploadFile
|
| 7 |
from fastapi.responses import FileResponse
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
|
|
|
| 10 |
from app.services import auth_service, elaboracao_service, visualizacao_service
|
| 11 |
from app.services.audit_log_service import log_event
|
| 12 |
from app.services.session_store import session_store
|
|
@@ -151,6 +152,15 @@ def map_popup(payload: MapaPopupPayload) -> dict[str, Any]:
|
|
| 151 |
return visualizacao_service.carregar_popup_ponto_mapa(session, payload.row_id)
|
| 152 |
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
@router.post("/evaluation/fields")
|
| 155 |
def evaluation_fields(payload: SessionPayload) -> dict[str, Any]:
|
| 156 |
session = session_store.get(payload.session_id)
|
|
|
|
| 3 |
import os
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
+
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
| 7 |
from fastapi.responses import FileResponse
|
| 8 |
from pydantic import BaseModel
|
| 9 |
|
| 10 |
+
from app.core.map_layers import get_bairros_geojson
|
| 11 |
from app.services import auth_service, elaboracao_service, visualizacao_service
|
| 12 |
from app.services.audit_log_service import log_event
|
| 13 |
from app.services.session_store import session_store
|
|
|
|
| 152 |
return visualizacao_service.carregar_popup_ponto_mapa(session, payload.row_id)
|
| 153 |
|
| 154 |
|
| 155 |
+
@router.get("/map/bairros.geojson")
|
| 156 |
+
def map_bairros_geojson(request: Request) -> dict[str, Any]:
|
| 157 |
+
auth_service.require_user(request)
|
| 158 |
+
geojson = get_bairros_geojson()
|
| 159 |
+
if not isinstance(geojson, dict):
|
| 160 |
+
raise HTTPException(status_code=404, detail="Camada de bairros indisponivel")
|
| 161 |
+
return geojson
|
| 162 |
+
|
| 163 |
+
|
| 164 |
@router.post("/evaluation/fields")
|
| 165 |
def evaluation_fields(payload: SessionPayload) -> dict[str, Any]:
|
| 166 |
session = session_store.get(payload.session_id)
|
backend/app/core/map_layers.py
CHANGED
|
@@ -95,6 +95,10 @@ def _carregar_bairros_geojson() -> dict[str, Any] | None:
|
|
| 95 |
return geojson
|
| 96 |
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
def add_bairros_layer(
|
| 99 |
mapa: folium.Map,
|
| 100 |
*,
|
|
@@ -180,14 +184,14 @@ def add_indice_marker(
|
|
| 180 |
).add_to(camada)
|
| 181 |
|
| 182 |
|
| 183 |
-
def
|
| 184 |
-
camada: folium.map.FeatureGroup,
|
| 185 |
trabalhos: list[dict[str, Any]] | None,
|
| 186 |
*,
|
| 187 |
origem: str = "pesquisa_mapa",
|
| 188 |
marker_style: str = "estrela",
|
| 189 |
ignore_bounds: bool = True,
|
| 190 |
-
) ->
|
|
|
|
| 191 |
for item in trabalhos or []:
|
| 192 |
try:
|
| 193 |
lat = float(item.get("coord_lat"))
|
|
@@ -261,43 +265,83 @@ def add_trabalhos_tecnicos_markers(
|
|
| 261 |
+ "</div>"
|
| 262 |
)
|
| 263 |
|
| 264 |
-
if str(marker_style or "").strip().lower() =
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
"border:1px solid #ffffff;box-shadow:0 0 0 1px rgba(20,42,66,0.20);'></div>"
|
| 274 |
-
),
|
| 275 |
-
icon_size=(8, 8),
|
| 276 |
-
icon_anchor=(4, 4),
|
| 277 |
-
class_name="mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-marker-dot",
|
| 278 |
-
),
|
| 279 |
)
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
"</svg></div>"
|
| 294 |
-
),
|
| 295 |
-
icon_size=(14, 14),
|
| 296 |
-
icon_anchor=(7, 7),
|
| 297 |
-
class_name="mesa-trabalho-tecnico-marker",
|
| 298 |
-
),
|
| 299 |
)
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
marcador.add_to(camada)
|
| 302 |
|
| 303 |
|
|
|
|
| 95 |
return geojson
|
| 96 |
|
| 97 |
|
| 98 |
+
def get_bairros_geojson() -> dict[str, Any] | None:
|
| 99 |
+
return _carregar_bairros_geojson()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
def add_bairros_layer(
|
| 103 |
mapa: folium.Map,
|
| 104 |
*,
|
|
|
|
| 184 |
).add_to(camada)
|
| 185 |
|
| 186 |
|
| 187 |
+
def build_trabalhos_tecnicos_marker_payloads(
|
|
|
|
| 188 |
trabalhos: list[dict[str, Any]] | None,
|
| 189 |
*,
|
| 190 |
origem: str = "pesquisa_mapa",
|
| 191 |
marker_style: str = "estrela",
|
| 192 |
ignore_bounds: bool = True,
|
| 193 |
+
) -> list[dict[str, Any]]:
|
| 194 |
+
payloads: list[dict[str, Any]] = []
|
| 195 |
for item in trabalhos or []:
|
| 196 |
try:
|
| 197 |
lat = float(item.get("coord_lat"))
|
|
|
|
| 265 |
+ "</div>"
|
| 266 |
)
|
| 267 |
|
| 268 |
+
if str(marker_style or "").strip().lower() != "ponto":
|
| 269 |
+
marker_html = (
|
| 270 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 271 |
+
"width:14px;height:14px;'>"
|
| 272 |
+
"<svg width='14' height='14' viewBox='0 0 24 24' aria-hidden='true'>"
|
| 273 |
+
"<polygon points='12,1.8 15.2,8.2 22.2,9.2 17.1,14.1 18.3,21.1 "
|
| 274 |
+
"12,17.8 5.7,21.1 6.9,14.1 1.8,9.2 8.8,8.2' "
|
| 275 |
+
"fill='#c62828' stroke='#000000' stroke-width='1.4' stroke-linejoin='round'/>"
|
| 276 |
+
"</svg></div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
)
|
| 278 |
+
payloads.append(
|
| 279 |
+
{
|
| 280 |
+
"lat": lat,
|
| 281 |
+
"lon": lon,
|
| 282 |
+
"tooltip_html": detalhes_html,
|
| 283 |
+
"popup_html": detalhes_html,
|
| 284 |
+
"marker_html": marker_html,
|
| 285 |
+
"marker_style": "estrela",
|
| 286 |
+
"ignore_bounds": bool(ignore_bounds),
|
| 287 |
+
"icon_size": [14, 14],
|
| 288 |
+
"icon_anchor": [7, 7],
|
| 289 |
+
"class_name": "mesa-trabalho-tecnico-marker",
|
| 290 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
)
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
marker_html = (
|
| 295 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 296 |
+
"width:8px;height:8px;border-radius:999px;background:#1f6fb2;"
|
| 297 |
+
"border:1px solid #ffffff;box-shadow:0 0 0 1px rgba(20,42,66,0.20);'></div>"
|
| 298 |
+
)
|
| 299 |
+
payloads.append(
|
| 300 |
+
{
|
| 301 |
+
"lat": lat,
|
| 302 |
+
"lon": lon,
|
| 303 |
+
"tooltip_html": detalhes_html,
|
| 304 |
+
"popup_html": detalhes_html,
|
| 305 |
+
"marker_html": marker_html,
|
| 306 |
+
"marker_style": "ponto",
|
| 307 |
+
"ignore_bounds": bool(ignore_bounds),
|
| 308 |
+
"icon_size": [8, 8],
|
| 309 |
+
"icon_anchor": [4, 4],
|
| 310 |
+
"class_name": "mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-marker-dot",
|
| 311 |
+
}
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
return payloads
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def add_trabalhos_tecnicos_markers(
|
| 318 |
+
camada: folium.map.FeatureGroup,
|
| 319 |
+
trabalhos: list[dict[str, Any]] | None,
|
| 320 |
+
*,
|
| 321 |
+
origem: str = "pesquisa_mapa",
|
| 322 |
+
marker_style: str = "estrela",
|
| 323 |
+
ignore_bounds: bool = True,
|
| 324 |
+
) -> None:
|
| 325 |
+
payloads = build_trabalhos_tecnicos_marker_payloads(
|
| 326 |
+
trabalhos,
|
| 327 |
+
origem=origem,
|
| 328 |
+
marker_style=marker_style,
|
| 329 |
+
ignore_bounds=ignore_bounds,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
for item in payloads:
|
| 333 |
+
marcador = folium.Marker(
|
| 334 |
+
location=[float(item["lat"]), float(item["lon"])],
|
| 335 |
+
tooltip=folium.Tooltip(str(item.get("tooltip_html") or ""), sticky=True),
|
| 336 |
+
popup=folium.Popup(str(item.get("popup_html") or ""), max_width=360),
|
| 337 |
+
icon=folium.DivIcon(
|
| 338 |
+
html=str(item.get("marker_html") or ""),
|
| 339 |
+
icon_size=tuple(item.get("icon_size") or [14, 14]),
|
| 340 |
+
icon_anchor=tuple(item.get("icon_anchor") or [7, 7]),
|
| 341 |
+
class_name=str(item.get("class_name") or "mesa-trabalho-tecnico-marker"),
|
| 342 |
+
),
|
| 343 |
+
)
|
| 344 |
+
marcador.options["mesaIgnoreBounds"] = bool(item.get("ignore_bounds"))
|
| 345 |
marcador.add_to(camada)
|
| 346 |
|
| 347 |
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -929,7 +929,7 @@ def criar_mapa(
|
|
| 929 |
camada_indices.add_to(mapa)
|
| 930 |
|
| 931 |
if avaliandos_tecnicos:
|
| 932 |
-
camada_trabalhos_tecnicos = folium.FeatureGroup(name="Avaliandos
|
| 933 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 934 |
camada_trabalhos_tecnicos.add_to(mapa)
|
| 935 |
|
|
|
|
| 929 |
camada_indices.add_to(mapa)
|
| 930 |
|
| 931 |
if avaliandos_tecnicos:
|
| 932 |
+
camada_trabalhos_tecnicos = folium.FeatureGroup(name="Avaliandos", show=True)
|
| 933 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 934 |
camada_trabalhos_tecnicos.add_to(mapa)
|
| 935 |
|
backend/app/core/visualizacao/map_payload.py
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
import branca.colormap as cm
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from scipy.interpolate import griddata
|
| 9 |
+
|
| 10 |
+
from app.core.elaboracao.charts import (
|
| 11 |
+
_contorno_convexo_lng_lat,
|
| 12 |
+
_mascara_dentro_poligono,
|
| 13 |
+
_montar_popup_registro_em_colunas,
|
| 14 |
+
_normalizar_stops_cor,
|
| 15 |
+
)
|
| 16 |
+
from app.core.map_layers import build_trabalhos_tecnicos_marker_payloads
|
| 17 |
+
from app.core.visualizacao.app import COR_PRINCIPAL, _aplicar_jitter_sobrepostos, formatar_monetario
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
_LAT_ALIASES = {"lat", "latitude", "siat_latitude"}
|
| 21 |
+
_LON_ALIASES = {"lon", "longitude", "long", "siat_longitude"}
|
| 22 |
+
_TILE_LAYERS = [
|
| 23 |
+
{"id": "osm", "label": "OpenStreetMap", "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"},
|
| 24 |
+
{"id": "positron", "label": "Positron", "url": "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"},
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _primeira_serie_por_nome(dataframe: pd.DataFrame, nome_coluna: str) -> pd.Series | None:
|
| 29 |
+
matches = [i for i, c in enumerate(dataframe.columns) if str(c) == str(nome_coluna)]
|
| 30 |
+
if not matches:
|
| 31 |
+
return None
|
| 32 |
+
return dataframe.iloc[:, matches[0]]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _detectar_coluna(df: pd.DataFrame, aliases: set[str]) -> str | None:
|
| 36 |
+
for col in df.columns:
|
| 37 |
+
if str(col).lower() in aliases:
|
| 38 |
+
return str(col)
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _formatar_tooltip_valor(coluna: str | None, valor: Any) -> str:
|
| 43 |
+
if valor is None:
|
| 44 |
+
return "—"
|
| 45 |
+
try:
|
| 46 |
+
if pd.isna(valor):
|
| 47 |
+
return "—"
|
| 48 |
+
except Exception:
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
col_norm = str(coluna or "").lower()
|
| 52 |
+
if isinstance(valor, (int, float, np.integer, np.floating)):
|
| 53 |
+
numero = float(valor)
|
| 54 |
+
if not np.isfinite(numero):
|
| 55 |
+
return "—"
|
| 56 |
+
if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]):
|
| 57 |
+
return formatar_monetario(numero)
|
| 58 |
+
return f"{numero:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 59 |
+
return str(valor)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _resolver_bounds(df_mapa: pd.DataFrame, lat_key: str, lon_key: str) -> list[list[float]]:
|
| 63 |
+
df_bounds = df_mapa
|
| 64 |
+
if len(df_mapa) >= 8:
|
| 65 |
+
lat_vals = df_mapa[lat_key]
|
| 66 |
+
lon_vals = df_mapa[lon_key]
|
| 67 |
+
lat_med = float(lat_vals.median())
|
| 68 |
+
lon_med = float(lon_vals.median())
|
| 69 |
+
lat_mad = float((lat_vals - lat_med).abs().median())
|
| 70 |
+
lon_mad = float((lon_vals - lon_med).abs().median())
|
| 71 |
+
lat_span = float(lat_vals.max() - lat_vals.min())
|
| 72 |
+
lon_span = float(lon_vals.max() - lon_vals.min())
|
| 73 |
+
lat_scale = max(lat_mad, lat_span / 30.0, 1e-6)
|
| 74 |
+
lon_scale = max(lon_mad, lon_span / 30.0, 1e-6)
|
| 75 |
+
score = ((lat_vals - lat_med) / lat_scale) ** 2 + ((lon_vals - lon_med) / lon_scale) ** 2
|
| 76 |
+
lim = float(score.quantile(0.75))
|
| 77 |
+
df_core = df_mapa[score <= lim]
|
| 78 |
+
if len(df_core) >= max(5, int(len(df_mapa) * 0.45)):
|
| 79 |
+
df_bounds = df_core
|
| 80 |
+
|
| 81 |
+
if len(df_bounds) >= 50:
|
| 82 |
+
lat_min, lat_max = df_bounds[lat_key].quantile([0.01, 0.99]).tolist()
|
| 83 |
+
lon_min, lon_max = df_bounds[lon_key].quantile([0.01, 0.99]).tolist()
|
| 84 |
+
else:
|
| 85 |
+
lat_min, lat_max = float(df_bounds[lat_key].min()), float(df_bounds[lat_key].max())
|
| 86 |
+
lon_min, lon_max = float(df_bounds[lon_key].min()), float(df_bounds[lon_key].max())
|
| 87 |
+
|
| 88 |
+
if not np.isfinite(lat_min) or not np.isfinite(lat_max):
|
| 89 |
+
lat_min, lat_max = float(df_mapa[lat_key].min()), float(df_mapa[lat_key].max())
|
| 90 |
+
if not np.isfinite(lon_min) or not np.isfinite(lon_max):
|
| 91 |
+
lon_min, lon_max = float(df_mapa[lon_key].min()), float(df_mapa[lon_key].max())
|
| 92 |
+
|
| 93 |
+
if np.isclose(lat_min, lat_max):
|
| 94 |
+
lat_min = float(lat_min) - 0.0008
|
| 95 |
+
lat_max = float(lat_max) + 0.0008
|
| 96 |
+
if np.isclose(lon_min, lon_max):
|
| 97 |
+
lon_min = float(lon_min) - 0.0008
|
| 98 |
+
lon_max = float(lon_max) + 0.0008
|
| 99 |
+
|
| 100 |
+
return [[float(lat_min), float(lon_min)], [float(lat_max), float(lon_max)]]
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _normalizar_bounds_lista(bounds: list[list[float]] | None) -> list[list[float]] | None:
|
| 104 |
+
coords = []
|
| 105 |
+
for item in bounds or []:
|
| 106 |
+
if not isinstance(item, (list, tuple)) or len(item) < 2:
|
| 107 |
+
continue
|
| 108 |
+
try:
|
| 109 |
+
lat = float(item[0])
|
| 110 |
+
lon = float(item[1])
|
| 111 |
+
except Exception:
|
| 112 |
+
continue
|
| 113 |
+
if not np.isfinite(lat) or not np.isfinite(lon):
|
| 114 |
+
continue
|
| 115 |
+
coords.append((lat, lon))
|
| 116 |
+
|
| 117 |
+
if not coords:
|
| 118 |
+
return None
|
| 119 |
+
|
| 120 |
+
lat_values = [lat for lat, _ in coords]
|
| 121 |
+
lon_values = [lon for _, lon in coords]
|
| 122 |
+
lat_min = min(lat_values)
|
| 123 |
+
lat_max = max(lat_values)
|
| 124 |
+
lon_min = min(lon_values)
|
| 125 |
+
lon_max = max(lon_values)
|
| 126 |
+
|
| 127 |
+
if np.isclose(lat_min, lat_max):
|
| 128 |
+
lat_min -= 0.0008
|
| 129 |
+
lat_max += 0.0008
|
| 130 |
+
if np.isclose(lon_min, lon_max):
|
| 131 |
+
lon_min -= 0.0008
|
| 132 |
+
lon_max += 0.0008
|
| 133 |
+
|
| 134 |
+
return [[float(lat_min), float(lon_min)], [float(lat_max), float(lon_max)]]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def build_leaflet_payload(
|
| 138 |
+
*,
|
| 139 |
+
bounds: list[list[float]] | None,
|
| 140 |
+
center: list[float] | None = None,
|
| 141 |
+
legend: dict[str, Any] | None = None,
|
| 142 |
+
overlay_layers: list[dict[str, Any]] | None = None,
|
| 143 |
+
notice: dict[str, Any] | None = None,
|
| 144 |
+
show_bairros: bool = True,
|
| 145 |
+
bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
|
| 146 |
+
) -> dict[str, Any] | None:
|
| 147 |
+
normalized_bounds = _normalizar_bounds_lista(bounds)
|
| 148 |
+
if normalized_bounds is None:
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
if center and len(center) >= 2:
|
| 152 |
+
center_lat = float(center[0])
|
| 153 |
+
center_lon = float(center[1])
|
| 154 |
+
else:
|
| 155 |
+
center_lat = float((normalized_bounds[0][0] + normalized_bounds[1][0]) / 2.0)
|
| 156 |
+
center_lon = float((normalized_bounds[0][1] + normalized_bounds[1][1]) / 2.0)
|
| 157 |
+
|
| 158 |
+
final_overlay_layers = list(overlay_layers or [])
|
| 159 |
+
if show_bairros:
|
| 160 |
+
final_overlay_layers.insert(
|
| 161 |
+
0,
|
| 162 |
+
{
|
| 163 |
+
"id": "bairros",
|
| 164 |
+
"label": "Bairros",
|
| 165 |
+
"show": True,
|
| 166 |
+
"geojson_url": bairros_geojson_url,
|
| 167 |
+
"geojson_style": {
|
| 168 |
+
"color": "#4c6882",
|
| 169 |
+
"weight": 1.0,
|
| 170 |
+
"fillColor": "#f39c12",
|
| 171 |
+
"fillOpacity": 0.04,
|
| 172 |
+
},
|
| 173 |
+
"geojson_tooltip_properties": ["NOME", "BAIRRO", "NME_BAI", "NOME_BAIRRO"],
|
| 174 |
+
"geojson_tooltip_label": "Bairro",
|
| 175 |
+
},
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
return {
|
| 179 |
+
"type": "mesa_leaflet_payload",
|
| 180 |
+
"version": 1,
|
| 181 |
+
"center": [center_lat, center_lon],
|
| 182 |
+
"bounds": normalized_bounds,
|
| 183 |
+
"tile_layers": _TILE_LAYERS,
|
| 184 |
+
"controls": {
|
| 185 |
+
"fullscreen": True,
|
| 186 |
+
"measure": True,
|
| 187 |
+
"layer_control": True,
|
| 188 |
+
},
|
| 189 |
+
"radius_behavior": {
|
| 190 |
+
"min_radius": 1.6,
|
| 191 |
+
"max_radius": 52.0,
|
| 192 |
+
"reference_zoom": 12.0,
|
| 193 |
+
"growth_factor": 0.20,
|
| 194 |
+
},
|
| 195 |
+
"legend": legend,
|
| 196 |
+
"notice": notice,
|
| 197 |
+
"overlay_layers": final_overlay_layers,
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def build_elaboracao_map_payload(
|
| 202 |
+
df: pd.DataFrame,
|
| 203 |
+
*,
|
| 204 |
+
lat_col: str = "lat",
|
| 205 |
+
lon_col: str = "lon",
|
| 206 |
+
cor_col: str | None = None,
|
| 207 |
+
indice_destacado: Any = None,
|
| 208 |
+
tamanho_col: str | None = None,
|
| 209 |
+
modo: str | None = "pontos",
|
| 210 |
+
cor_vmin: float | None = None,
|
| 211 |
+
cor_vmax: float | None = None,
|
| 212 |
+
cor_caption: str | None = None,
|
| 213 |
+
cor_colors: list[str] | None = None,
|
| 214 |
+
cor_stops: list[float] | None = None,
|
| 215 |
+
cor_tick_values: list[float] | None = None,
|
| 216 |
+
cor_tick_labels: list[str] | None = None,
|
| 217 |
+
bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
|
| 218 |
+
) -> dict[str, Any] | None:
|
| 219 |
+
modo_normalizado = str(modo or "pontos").strip().lower()
|
| 220 |
+
|
| 221 |
+
cols_lower = {str(c).lower(): c for c in df.columns}
|
| 222 |
+
lat_real = cols_lower.get(str(lat_col).lower()) if str(lat_col).lower() in cols_lower else None
|
| 223 |
+
lon_real = cols_lower.get(str(lon_col).lower()) if str(lon_col).lower() in cols_lower else None
|
| 224 |
+
|
| 225 |
+
if lat_real is None:
|
| 226 |
+
for nome in ["lat", "latitude", "siat_latitude"]:
|
| 227 |
+
if nome in cols_lower:
|
| 228 |
+
lat_real = cols_lower[nome]
|
| 229 |
+
break
|
| 230 |
+
if lon_real is None:
|
| 231 |
+
for nome in ["lon", "longitude", "long", "siat_longitude"]:
|
| 232 |
+
if nome in cols_lower:
|
| 233 |
+
lon_real = cols_lower[nome]
|
| 234 |
+
break
|
| 235 |
+
if lat_real is None or lon_real is None:
|
| 236 |
+
return None
|
| 237 |
+
|
| 238 |
+
df_mapa = df.copy()
|
| 239 |
+
df_mapa[lat_real] = pd.to_numeric(df_mapa[lat_real], errors="coerce")
|
| 240 |
+
df_mapa[lon_real] = pd.to_numeric(df_mapa[lon_real], errors="coerce")
|
| 241 |
+
df_mapa = df_mapa.dropna(subset=[lat_real, lon_real])
|
| 242 |
+
df_mapa = df_mapa[~((df_mapa[lat_real] == 0.0) & (df_mapa[lon_real] == 0.0))]
|
| 243 |
+
df_mapa = df_mapa[
|
| 244 |
+
(df_mapa[lat_real] >= -90.0)
|
| 245 |
+
& (df_mapa[lat_real] <= 90.0)
|
| 246 |
+
& (df_mapa[lon_real] >= -180.0)
|
| 247 |
+
& (df_mapa[lon_real] <= 180.0)
|
| 248 |
+
].copy()
|
| 249 |
+
if df_mapa.empty:
|
| 250 |
+
return None
|
| 251 |
+
|
| 252 |
+
limite_pontos = 2500
|
| 253 |
+
total_pontos = len(df_mapa)
|
| 254 |
+
houve_amostragem = total_pontos > limite_pontos
|
| 255 |
+
if houve_amostragem:
|
| 256 |
+
df_mapa = df_mapa.sample(n=limite_pontos, random_state=42).copy()
|
| 257 |
+
|
| 258 |
+
centro_lat = float(df_mapa[lat_real].median())
|
| 259 |
+
centro_lon = float(df_mapa[lon_real].median())
|
| 260 |
+
|
| 261 |
+
colors = (
|
| 262 |
+
[str(item) for item in cor_colors if str(item).strip()]
|
| 263 |
+
if isinstance(cor_colors, list) and len(cor_colors) >= 2
|
| 264 |
+
else ["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"]
|
| 265 |
+
)
|
| 266 |
+
modo_calor = modo_normalizado == "calor" and tamanho_col is not None and tamanho_col in df_mapa.columns
|
| 267 |
+
modo_superficie = modo_normalizado == "superficie" and tamanho_col is not None and tamanho_col in df_mapa.columns
|
| 268 |
+
if modo_normalizado not in {"pontos", "calor", "superficie"}:
|
| 269 |
+
modo_normalizado = "pontos"
|
| 270 |
+
|
| 271 |
+
cor_col_resolvida = cor_col or tamanho_col
|
| 272 |
+
colormap = None
|
| 273 |
+
legend = None
|
| 274 |
+
if cor_col_resolvida and cor_col_resolvida in df_mapa.columns:
|
| 275 |
+
serie_cor = pd.to_numeric(df_mapa[cor_col_resolvida], errors="coerce")
|
| 276 |
+
vmin = float(cor_vmin) if cor_vmin is not None and np.isfinite(cor_vmin) else float(serie_cor.min())
|
| 277 |
+
vmax = float(cor_vmax) if cor_vmax is not None and np.isfinite(cor_vmax) else float(serie_cor.max())
|
| 278 |
+
if np.isfinite(vmin) and np.isfinite(vmax):
|
| 279 |
+
if np.isclose(vmin, vmax):
|
| 280 |
+
vmax = float(vmin) + 1.0
|
| 281 |
+
color_index = _normalizar_stops_cor(cor_stops, colors, float(vmin), float(vmax))
|
| 282 |
+
colormap_kwargs: dict[str, Any] = {
|
| 283 |
+
"colors": colors,
|
| 284 |
+
"vmin": float(vmin),
|
| 285 |
+
"vmax": float(vmax),
|
| 286 |
+
"caption": str(cor_caption or cor_col_resolvida),
|
| 287 |
+
}
|
| 288 |
+
if color_index is not None:
|
| 289 |
+
colormap_kwargs["index"] = color_index
|
| 290 |
+
colormap = cm.LinearColormap(**colormap_kwargs)
|
| 291 |
+
legend = {
|
| 292 |
+
"title": str(cor_caption or cor_col_resolvida),
|
| 293 |
+
"vmin": float(vmin),
|
| 294 |
+
"vmax": float(vmax),
|
| 295 |
+
"colors": colors,
|
| 296 |
+
}
|
| 297 |
+
if (
|
| 298 |
+
isinstance(cor_tick_values, list)
|
| 299 |
+
and isinstance(cor_tick_labels, list)
|
| 300 |
+
and len(cor_tick_values) == len(cor_tick_labels)
|
| 301 |
+
and len(cor_tick_values) > 1
|
| 302 |
+
):
|
| 303 |
+
try:
|
| 304 |
+
legend["tick_values"] = [float(item) for item in cor_tick_values]
|
| 305 |
+
legend["tick_labels"] = [str(item) for item in cor_tick_labels]
|
| 306 |
+
except (TypeError, ValueError):
|
| 307 |
+
pass
|
| 308 |
+
|
| 309 |
+
raio_min, raio_max = 3.0, 18.0
|
| 310 |
+
tamanho_func = None
|
| 311 |
+
if tamanho_col and tamanho_col in df_mapa.columns:
|
| 312 |
+
serie_tamanho = pd.to_numeric(df_mapa[tamanho_col], errors="coerce")
|
| 313 |
+
t_min = float(serie_tamanho.min())
|
| 314 |
+
t_max = float(serie_tamanho.max())
|
| 315 |
+
if np.isfinite(t_min) and np.isfinite(t_max):
|
| 316 |
+
if t_max > t_min:
|
| 317 |
+
tamanho_func = (
|
| 318 |
+
lambda v, _min=t_min, _max=t_max: raio_min
|
| 319 |
+
+ (v - _min) / (_max - _min) * (raio_max - raio_min)
|
| 320 |
+
)
|
| 321 |
+
else:
|
| 322 |
+
tamanho_func = lambda _v: (raio_min + raio_max) / 2.0
|
| 323 |
+
|
| 324 |
+
mostrar_indices = not modo_calor and not modo_superficie and len(df_mapa) <= 800
|
| 325 |
+
lat_plot_col = "__mesa_lat_plot__"
|
| 326 |
+
lon_plot_col = "__mesa_lon_plot__"
|
| 327 |
+
if not modo_calor and not modo_superficie:
|
| 328 |
+
df_plot_pontos = _aplicar_jitter_sobrepostos(
|
| 329 |
+
df_mapa,
|
| 330 |
+
lat_col=str(lat_real),
|
| 331 |
+
lon_col=str(lon_real),
|
| 332 |
+
lat_plot_col=lat_plot_col,
|
| 333 |
+
lon_plot_col=lon_plot_col,
|
| 334 |
+
)
|
| 335 |
+
else:
|
| 336 |
+
df_plot_pontos = df_mapa.copy()
|
| 337 |
+
df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
|
| 338 |
+
df_plot_pontos[lon_plot_col] = df_plot_pontos[lon_real]
|
| 339 |
+
|
| 340 |
+
overlay_layers: list[dict[str, Any]] = []
|
| 341 |
+
if modo_calor and tamanho_col and tamanho_col in df_mapa.columns:
|
| 342 |
+
pesos = pd.to_numeric(df_mapa[tamanho_col], errors="coerce")
|
| 343 |
+
mask_pesos = np.isfinite(pesos.to_numpy())
|
| 344 |
+
df_calor = df_mapa.loc[mask_pesos, [lat_real, lon_real]].copy()
|
| 345 |
+
if not df_calor.empty:
|
| 346 |
+
pesos_validos = pesos.loc[df_calor.index].to_numpy(dtype=float)
|
| 347 |
+
fixed_scale = (
|
| 348 |
+
cor_vmin is not None
|
| 349 |
+
and cor_vmax is not None
|
| 350 |
+
and np.isfinite(cor_vmin)
|
| 351 |
+
and np.isfinite(cor_vmax)
|
| 352 |
+
and float(cor_vmax) > float(cor_vmin)
|
| 353 |
+
)
|
| 354 |
+
if fixed_scale:
|
| 355 |
+
peso_min = float(cor_vmin)
|
| 356 |
+
peso_max = float(cor_vmax)
|
| 357 |
+
pesos_clip = np.clip(pesos_validos, peso_min, peso_max)
|
| 358 |
+
pesos_norm = (pesos_clip - peso_min) / (peso_max - peso_min)
|
| 359 |
+
else:
|
| 360 |
+
peso_min = float(np.min(pesos_validos))
|
| 361 |
+
peso_max = float(np.max(pesos_validos))
|
| 362 |
+
if np.isfinite(peso_min) and np.isfinite(peso_max) and peso_max > peso_min:
|
| 363 |
+
pesos_norm = 0.1 + 0.9 * (pesos_validos - peso_min) / (peso_max - peso_min)
|
| 364 |
+
else:
|
| 365 |
+
pesos_norm = np.ones_like(pesos_validos)
|
| 366 |
+
gradient = None
|
| 367 |
+
if len(colors) >= 2:
|
| 368 |
+
if (
|
| 369 |
+
cor_vmin is not None
|
| 370 |
+
and cor_vmax is not None
|
| 371 |
+
and np.isfinite(cor_vmin)
|
| 372 |
+
and np.isfinite(cor_vmax)
|
| 373 |
+
and float(cor_vmax) > float(cor_vmin)
|
| 374 |
+
):
|
| 375 |
+
color_index = _normalizar_stops_cor(cor_stops, colors, float(cor_vmin), float(cor_vmax))
|
| 376 |
+
if color_index is not None:
|
| 377 |
+
gradient = {}
|
| 378 |
+
for stop, color in zip(color_index, colors):
|
| 379 |
+
ratio = (float(stop) - float(cor_vmin)) / (float(cor_vmax) - float(cor_vmin))
|
| 380 |
+
gradient[float(np.clip(ratio, 0.0, 1.0))] = color
|
| 381 |
+
elif len(colors) == 2:
|
| 382 |
+
gradient = {0.0: colors[0], 1.0: colors[1]}
|
| 383 |
+
else:
|
| 384 |
+
gradient = {i / (len(colors) - 1): colors[i] for i in range(len(colors))}
|
| 385 |
+
elif len(colors) == 2:
|
| 386 |
+
gradient = {0.0: colors[0], 1.0: colors[1]}
|
| 387 |
+
else:
|
| 388 |
+
gradient = {i / (len(colors) - 1): colors[i] for i in range(len(colors))}
|
| 389 |
+
overlay_layers.append(
|
| 390 |
+
{
|
| 391 |
+
"id": "mapa_calor",
|
| 392 |
+
"label": "Mapa de calor",
|
| 393 |
+
"show": True,
|
| 394 |
+
"heatmap": {
|
| 395 |
+
"points": [
|
| 396 |
+
{
|
| 397 |
+
"lat": float(df_calor.iloc[idx][lat_real]),
|
| 398 |
+
"lon": float(df_calor.iloc[idx][lon_real]),
|
| 399 |
+
"weight": float(pesos_norm[idx]),
|
| 400 |
+
}
|
| 401 |
+
for idx in range(len(df_calor))
|
| 402 |
+
],
|
| 403 |
+
"radius": 20,
|
| 404 |
+
"blur": 18,
|
| 405 |
+
"min_opacity": 0.28,
|
| 406 |
+
"max_zoom": 17,
|
| 407 |
+
"gradient": gradient,
|
| 408 |
+
},
|
| 409 |
+
}
|
| 410 |
+
)
|
| 411 |
+
elif modo_superficie and tamanho_col and tamanho_col in df_mapa.columns:
|
| 412 |
+
lats = pd.to_numeric(df_mapa[lat_real], errors="coerce").to_numpy(dtype=float)
|
| 413 |
+
lons = pd.to_numeric(df_mapa[lon_real], errors="coerce").to_numpy(dtype=float)
|
| 414 |
+
valores = pd.to_numeric(df_mapa[tamanho_col], errors="coerce").to_numpy(dtype=float)
|
| 415 |
+
mask_valid = np.isfinite(lats) & np.isfinite(lons) & np.isfinite(valores)
|
| 416 |
+
if mask_valid.sum() >= 6:
|
| 417 |
+
lats = lats[mask_valid]
|
| 418 |
+
lons = lons[mask_valid]
|
| 419 |
+
valores = valores[mask_valid]
|
| 420 |
+
contorno = _contorno_convexo_lng_lat(lons, lats)
|
| 421 |
+
if contorno is not None:
|
| 422 |
+
lon_min = float(np.min(contorno[:, 0]))
|
| 423 |
+
lon_max = float(np.max(contorno[:, 0]))
|
| 424 |
+
lat_min = float(np.min(contorno[:, 1]))
|
| 425 |
+
lat_max = float(np.max(contorno[:, 1]))
|
| 426 |
+
if not np.isclose(lon_min, lon_max) and not np.isclose(lat_min, lat_max):
|
| 427 |
+
n_obs = len(valores)
|
| 428 |
+
n_grid = 46 if n_obs <= 400 else (40 if n_obs <= 1200 else 34)
|
| 429 |
+
grid_lon = np.linspace(lon_min, lon_max, n_grid)
|
| 430 |
+
grid_lat = np.linspace(lat_min, lat_max, n_grid)
|
| 431 |
+
mesh_lon, mesh_lat = np.meshgrid(grid_lon, grid_lat)
|
| 432 |
+
pontos = np.column_stack([lons, lats])
|
| 433 |
+
try:
|
| 434 |
+
superficie = griddata(pontos, valores, (mesh_lon, mesh_lat), method="linear")
|
| 435 |
+
except Exception:
|
| 436 |
+
superficie = None
|
| 437 |
+
if superficie is not None:
|
| 438 |
+
superficie = np.asarray(superficie, dtype=float)
|
| 439 |
+
if np.isnan(superficie).all():
|
| 440 |
+
try:
|
| 441 |
+
superficie = griddata(pontos, valores, (mesh_lon, mesh_lat), method="nearest")
|
| 442 |
+
except Exception:
|
| 443 |
+
superficie = None
|
| 444 |
+
elif np.isnan(superficie).any():
|
| 445 |
+
try:
|
| 446 |
+
nearest = griddata(pontos, valores, (mesh_lon, mesh_lat), method="nearest")
|
| 447 |
+
except Exception:
|
| 448 |
+
nearest = None
|
| 449 |
+
if nearest is not None:
|
| 450 |
+
superficie = np.where(np.isfinite(superficie), superficie, np.asarray(nearest, dtype=float))
|
| 451 |
+
if superficie is not None:
|
| 452 |
+
superficie = np.asarray(superficie, dtype=float)
|
| 453 |
+
mascara = _mascara_dentro_poligono(mesh_lon, mesh_lat, contorno)
|
| 454 |
+
superficie = np.where(mascara, superficie, np.nan)
|
| 455 |
+
if np.isfinite(superficie).any():
|
| 456 |
+
vmin_sup = float(cor_vmin) if cor_vmin is not None and np.isfinite(cor_vmin) else float(np.nanmin(superficie))
|
| 457 |
+
vmax_sup = float(cor_vmax) if cor_vmax is not None and np.isfinite(cor_vmax) else float(np.nanmax(superficie))
|
| 458 |
+
if np.isfinite(vmin_sup) and np.isfinite(vmax_sup):
|
| 459 |
+
if np.isclose(vmin_sup, vmax_sup):
|
| 460 |
+
vmax_sup = vmin_sup + 1.0
|
| 461 |
+
color_index = _normalizar_stops_cor(cor_stops, colors, vmin_sup, vmax_sup)
|
| 462 |
+
colormap_kwargs = {
|
| 463 |
+
"colors": colors,
|
| 464 |
+
"vmin": float(vmin_sup),
|
| 465 |
+
"vmax": float(vmax_sup),
|
| 466 |
+
"caption": str(cor_caption or f"{tamanho_col} (superfície)"),
|
| 467 |
+
}
|
| 468 |
+
if color_index is not None:
|
| 469 |
+
colormap_kwargs["index"] = color_index
|
| 470 |
+
colormap = cm.LinearColormap(**colormap_kwargs)
|
| 471 |
+
legend = {
|
| 472 |
+
"title": str(cor_caption or f"{tamanho_col} (superfície)"),
|
| 473 |
+
"vmin": float(vmin_sup),
|
| 474 |
+
"vmax": float(vmax_sup),
|
| 475 |
+
"colors": colors,
|
| 476 |
+
}
|
| 477 |
+
if (
|
| 478 |
+
isinstance(cor_tick_values, list)
|
| 479 |
+
and isinstance(cor_tick_labels, list)
|
| 480 |
+
and len(cor_tick_values) == len(cor_tick_labels)
|
| 481 |
+
and len(cor_tick_values) > 1
|
| 482 |
+
):
|
| 483 |
+
try:
|
| 484 |
+
legend["tick_values"] = [float(item) for item in cor_tick_values]
|
| 485 |
+
legend["tick_labels"] = [str(item) for item in cor_tick_labels]
|
| 486 |
+
except (TypeError, ValueError):
|
| 487 |
+
pass
|
| 488 |
+
centros_lon = (grid_lon[:-1] + grid_lon[1:]) / 2.0
|
| 489 |
+
centros_lat = (grid_lat[:-1] + grid_lat[1:]) / 2.0
|
| 490 |
+
centro_mesh_lon, centro_mesh_lat = np.meshgrid(centros_lon, centros_lat)
|
| 491 |
+
mascara_centros = _mascara_dentro_poligono(centro_mesh_lon, centro_mesh_lat, contorno)
|
| 492 |
+
valores_celula = (
|
| 493 |
+
superficie[:-1, :-1]
|
| 494 |
+
+ superficie[1:, :-1]
|
| 495 |
+
+ superficie[:-1, 1:]
|
| 496 |
+
+ superficie[1:, 1:]
|
| 497 |
+
) / 4.0
|
| 498 |
+
shapes = []
|
| 499 |
+
for i in range(valores_celula.shape[0]):
|
| 500 |
+
for j in range(valores_celula.shape[1]):
|
| 501 |
+
if not mascara_centros[i, j]:
|
| 502 |
+
continue
|
| 503 |
+
valor = valores_celula[i, j]
|
| 504 |
+
if not np.isfinite(valor):
|
| 505 |
+
continue
|
| 506 |
+
cor = str(colormap(float(valor)))
|
| 507 |
+
valor_fmt = f"{float(valor):.3f}".replace(".", ",")
|
| 508 |
+
shapes.append(
|
| 509 |
+
{
|
| 510 |
+
"type": "polygon",
|
| 511 |
+
"coords": [
|
| 512 |
+
[float(grid_lat[i]), float(grid_lon[j])],
|
| 513 |
+
[float(grid_lat[i]), float(grid_lon[j + 1])],
|
| 514 |
+
[float(grid_lat[i + 1]), float(grid_lon[j + 1])],
|
| 515 |
+
[float(grid_lat[i + 1]), float(grid_lon[j])],
|
| 516 |
+
],
|
| 517 |
+
"color": cor,
|
| 518 |
+
"weight": 0,
|
| 519 |
+
"fill": True,
|
| 520 |
+
"fill_color": cor,
|
| 521 |
+
"fill_opacity": 0.6,
|
| 522 |
+
"tooltip_html": (
|
| 523 |
+
f"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px;'>"
|
| 524 |
+
f"{str(tamanho_col)} interpolado: <b>{valor_fmt}</b>"
|
| 525 |
+
"</div>"
|
| 526 |
+
),
|
| 527 |
+
}
|
| 528 |
+
)
|
| 529 |
+
if shapes:
|
| 530 |
+
overlay_layers.append(
|
| 531 |
+
{
|
| 532 |
+
"id": "superficie_continua",
|
| 533 |
+
"label": "Superfície contínua",
|
| 534 |
+
"show": True,
|
| 535 |
+
"shapes": shapes,
|
| 536 |
+
}
|
| 537 |
+
)
|
| 538 |
+
else:
|
| 539 |
+
popup_cols: list[str]
|
| 540 |
+
if len(df_plot_pontos) <= 1200:
|
| 541 |
+
popup_cols = [str(c) for c in df_plot_pontos.columns]
|
| 542 |
+
elif tamanho_col and tamanho_col in df_plot_pontos.columns:
|
| 543 |
+
popup_cols = [str(tamanho_col)]
|
| 544 |
+
else:
|
| 545 |
+
popup_cols = []
|
| 546 |
+
|
| 547 |
+
market_points: list[dict[str, Any]] = []
|
| 548 |
+
indices_markers: list[dict[str, Any]] = []
|
| 549 |
+
for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()):
|
| 550 |
+
idx_display = int(idx) if isinstance(idx, (int, np.integer)) else marker_ordem + 1
|
| 551 |
+
popup_html, popup_width = _montar_popup_registro_em_colunas(
|
| 552 |
+
idx_display,
|
| 553 |
+
row,
|
| 554 |
+
popup_cols,
|
| 555 |
+
max_itens_coluna=8,
|
| 556 |
+
popup_uid=f"mesa-pop-elab-{marker_ordem}",
|
| 557 |
+
)
|
| 558 |
+
tooltip_html = (
|
| 559 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>"
|
| 560 |
+
f"<b>Índice {idx_display}</b>"
|
| 561 |
+
)
|
| 562 |
+
if tamanho_col and tamanho_col in df_plot_pontos.columns:
|
| 563 |
+
valor_tooltip = row[tamanho_col]
|
| 564 |
+
valor_texto = (
|
| 565 |
+
f"{float(valor_tooltip):.2f}"
|
| 566 |
+
if isinstance(valor_tooltip, (int, float, np.integer, np.floating)) and np.isfinite(float(valor_tooltip))
|
| 567 |
+
else str(valor_tooltip)
|
| 568 |
+
)
|
| 569 |
+
tooltip_html += (
|
| 570 |
+
f"<br><span style='color:#555;'>{str(tamanho_col)}:</span> "
|
| 571 |
+
f"<b>{valor_texto}</b>"
|
| 572 |
+
)
|
| 573 |
+
tooltip_html += "</div>"
|
| 574 |
+
|
| 575 |
+
cor = COR_PRINCIPAL
|
| 576 |
+
if colormap and cor_col_resolvida and cor_col_resolvida in df_mapa.columns:
|
| 577 |
+
valor_cor = pd.to_numeric(pd.Series([row.get(cor_col_resolvida)]), errors="coerce").iloc[0]
|
| 578 |
+
if pd.notna(valor_cor):
|
| 579 |
+
cor = str(colormap(float(valor_cor)))
|
| 580 |
+
|
| 581 |
+
if idx == indice_destacado:
|
| 582 |
+
raio = raio_max + 4.0
|
| 583 |
+
elif tamanho_func and tamanho_col and tamanho_col in row.index and pd.notna(row[tamanho_col]):
|
| 584 |
+
raio = float(tamanho_func(float(row[tamanho_col])))
|
| 585 |
+
else:
|
| 586 |
+
raio = 4.0
|
| 587 |
+
stroke_weight = 3.0 if idx == indice_destacado else 1.0
|
| 588 |
+
fill_opacity = 0.8 if idx == indice_destacado else 0.6
|
| 589 |
+
|
| 590 |
+
market_points.append(
|
| 591 |
+
{
|
| 592 |
+
"lat": float(row[lat_plot_col]),
|
| 593 |
+
"lon": float(row[lon_plot_col]),
|
| 594 |
+
"indice": idx_display,
|
| 595 |
+
"color": cor,
|
| 596 |
+
"base_radius": float(max(1.0, raio)),
|
| 597 |
+
"stroke_color": "#000000",
|
| 598 |
+
"stroke_weight": float(stroke_weight),
|
| 599 |
+
"fill_opacity": float(fill_opacity),
|
| 600 |
+
"tooltip_html": tooltip_html,
|
| 601 |
+
"popup_html": popup_html,
|
| 602 |
+
"popup_max_width": int(popup_width),
|
| 603 |
+
}
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
if mostrar_indices:
|
| 607 |
+
indices_markers.append(
|
| 608 |
+
{
|
| 609 |
+
"lat": float(row[lat_plot_col]),
|
| 610 |
+
"lon": float(row[lon_plot_col]),
|
| 611 |
+
"marker_html": (
|
| 612 |
+
'<div style="transform: translate(10px, -14px);display:inline-block;background: rgba(255, 255, 255, 0.9);'
|
| 613 |
+
+ 'border: 1px solid rgba(28, 45, 66, 0.45);border-radius: 10px;padding: 1px 6px;font-size: 11px;'
|
| 614 |
+
+ 'font-weight: 700;line-height: 1.2;color: #1f2f44;white-space: nowrap;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);'
|
| 615 |
+
+ 'pointer-events: none;">'
|
| 616 |
+
+ f"{idx_display}"
|
| 617 |
+
+ "</div>"
|
| 618 |
+
),
|
| 619 |
+
"icon_size": [72, 24],
|
| 620 |
+
"icon_anchor": [0, 0],
|
| 621 |
+
"class_name": "mesa-indice-label",
|
| 622 |
+
"interactive": False,
|
| 623 |
+
"keyboard": False,
|
| 624 |
+
}
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
overlay_layers.append(
|
| 628 |
+
{
|
| 629 |
+
"id": "mercado",
|
| 630 |
+
"label": "Mercado",
|
| 631 |
+
"show": True,
|
| 632 |
+
"points": market_points,
|
| 633 |
+
}
|
| 634 |
+
)
|
| 635 |
+
if mostrar_indices and indices_markers:
|
| 636 |
+
overlay_layers.append(
|
| 637 |
+
{
|
| 638 |
+
"id": "indices",
|
| 639 |
+
"label": "Índices",
|
| 640 |
+
"show": False,
|
| 641 |
+
"markers": indices_markers,
|
| 642 |
+
}
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
notice = None
|
| 646 |
+
if houve_amostragem:
|
| 647 |
+
notice = {
|
| 648 |
+
"message": f"Exibindo {len(df_mapa)} de {total_pontos} pontos para melhor desempenho.",
|
| 649 |
+
"position": "topright",
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
return build_leaflet_payload(
|
| 653 |
+
bounds=_resolver_bounds(df_mapa, str(lat_real), str(lon_real)),
|
| 654 |
+
center=[centro_lat, centro_lon],
|
| 655 |
+
legend=legend,
|
| 656 |
+
notice=notice,
|
| 657 |
+
overlay_layers=overlay_layers,
|
| 658 |
+
show_bairros=True,
|
| 659 |
+
bairros_geojson_url=bairros_geojson_url,
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def build_visualizacao_map_payload(
|
| 664 |
+
df: pd.DataFrame,
|
| 665 |
+
*,
|
| 666 |
+
cor_col: str | None = None,
|
| 667 |
+
tamanho_col: str | None = None,
|
| 668 |
+
col_y: str | None = None,
|
| 669 |
+
avaliandos_tecnicos: list[dict[str, Any]] | None = None,
|
| 670 |
+
bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
|
| 671 |
+
) -> dict[str, Any] | None:
|
| 672 |
+
lat_real = _detectar_coluna(df, _LAT_ALIASES)
|
| 673 |
+
lon_real = _detectar_coluna(df, _LON_ALIASES)
|
| 674 |
+
if lat_real is None or lon_real is None:
|
| 675 |
+
return None
|
| 676 |
+
|
| 677 |
+
df_mapa = df.copy()
|
| 678 |
+
lat_key = "__mesa_lat__"
|
| 679 |
+
lon_key = "__mesa_lon__"
|
| 680 |
+
lat_serie = _primeira_serie_por_nome(df_mapa, lat_real)
|
| 681 |
+
lon_serie = _primeira_serie_por_nome(df_mapa, lon_real)
|
| 682 |
+
if lat_serie is None or lon_serie is None:
|
| 683 |
+
return None
|
| 684 |
+
|
| 685 |
+
df_mapa[lat_key] = pd.to_numeric(lat_serie, errors="coerce")
|
| 686 |
+
df_mapa[lon_key] = pd.to_numeric(lon_serie, errors="coerce")
|
| 687 |
+
df_mapa = df_mapa.dropna(subset=[lat_key, lon_key])
|
| 688 |
+
df_mapa = df_mapa[
|
| 689 |
+
(df_mapa[lat_key] >= -90.0)
|
| 690 |
+
& (df_mapa[lat_key] <= 90.0)
|
| 691 |
+
& (df_mapa[lon_key] >= -180.0)
|
| 692 |
+
& (df_mapa[lon_key] <= 180.0)
|
| 693 |
+
].copy()
|
| 694 |
+
if df_mapa.empty:
|
| 695 |
+
return None
|
| 696 |
+
|
| 697 |
+
centro_lat = float(df_mapa[lat_key].median())
|
| 698 |
+
centro_lon = float(df_mapa[lon_key].median())
|
| 699 |
+
|
| 700 |
+
cor_col_resolvida = cor_col
|
| 701 |
+
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col_resolvida:
|
| 702 |
+
cor_col_resolvida = tamanho_col
|
| 703 |
+
|
| 704 |
+
colormap = None
|
| 705 |
+
cor_key = None
|
| 706 |
+
legend = None
|
| 707 |
+
if cor_col_resolvida and cor_col_resolvida in df_mapa.columns:
|
| 708 |
+
serie_cor = _primeira_serie_por_nome(df_mapa, cor_col_resolvida)
|
| 709 |
+
if serie_cor is not None:
|
| 710 |
+
cor_key = "__mesa_cor__"
|
| 711 |
+
df_mapa[cor_key] = pd.to_numeric(serie_cor, errors="coerce")
|
| 712 |
+
vmin = df_mapa[cor_key].min()
|
| 713 |
+
vmax = df_mapa[cor_key].max()
|
| 714 |
+
if pd.notna(vmin) and pd.notna(vmax):
|
| 715 |
+
colormap = cm.LinearColormap(
|
| 716 |
+
colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 717 |
+
vmin=vmin,
|
| 718 |
+
vmax=vmax,
|
| 719 |
+
caption=cor_col_resolvida,
|
| 720 |
+
)
|
| 721 |
+
legend = {
|
| 722 |
+
"title": str(cor_col_resolvida),
|
| 723 |
+
"vmin": float(vmin),
|
| 724 |
+
"vmax": float(vmax),
|
| 725 |
+
"colors": ["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
raio_min, raio_max = 3.0, 18.0
|
| 729 |
+
tamanho_func = None
|
| 730 |
+
tamanho_key = None
|
| 731 |
+
if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
|
| 732 |
+
serie_tamanho = _primeira_serie_por_nome(df_mapa, tamanho_col)
|
| 733 |
+
if serie_tamanho is not None:
|
| 734 |
+
tamanho_key = "__mesa_tamanho__"
|
| 735 |
+
df_mapa[tamanho_key] = pd.to_numeric(serie_tamanho, errors="coerce")
|
| 736 |
+
t_min = df_mapa[tamanho_key].min()
|
| 737 |
+
t_max = df_mapa[tamanho_key].max()
|
| 738 |
+
if pd.notna(t_min) and pd.notna(t_max):
|
| 739 |
+
if t_max > t_min:
|
| 740 |
+
tamanho_func = (
|
| 741 |
+
lambda v, _min=t_min, _max=t_max: raio_min
|
| 742 |
+
+ (v - _min) / (_max - _min) * (raio_max - raio_min)
|
| 743 |
+
)
|
| 744 |
+
else:
|
| 745 |
+
tamanho_func = lambda v: (raio_min + raio_max) / 2.0
|
| 746 |
+
|
| 747 |
+
show_indices = False
|
| 748 |
+
lat_plot_key = "__mesa_lat_plot__"
|
| 749 |
+
lon_plot_key = "__mesa_lon_plot__"
|
| 750 |
+
df_plot_pontos = _aplicar_jitter_sobrepostos(
|
| 751 |
+
df_mapa,
|
| 752 |
+
lat_col=lat_key,
|
| 753 |
+
lon_col=lon_key,
|
| 754 |
+
lat_plot_col=lat_plot_key,
|
| 755 |
+
lon_plot_col=lon_plot_key,
|
| 756 |
+
)
|
| 757 |
+
|
| 758 |
+
tooltip_col = None
|
| 759 |
+
tooltip_key = None
|
| 760 |
+
if tamanho_key:
|
| 761 |
+
tooltip_col = tamanho_col
|
| 762 |
+
tooltip_key = tamanho_key
|
| 763 |
+
elif col_y and col_y in df_mapa.columns:
|
| 764 |
+
serie_tooltip = _primeira_serie_por_nome(df_mapa, col_y)
|
| 765 |
+
if serie_tooltip is not None:
|
| 766 |
+
tooltip_col = col_y
|
| 767 |
+
tooltip_key = "__mesa_tooltip__"
|
| 768 |
+
df_mapa[tooltip_key] = serie_tooltip
|
| 769 |
+
df_plot_pontos[tooltip_key] = df_mapa.loc[df_plot_pontos.index, tooltip_key]
|
| 770 |
+
|
| 771 |
+
total_pontos_plot = len(df_plot_pontos)
|
| 772 |
+
raio_padrao = 4.0 if total_pontos_plot <= 2500 else 3.0
|
| 773 |
+
|
| 774 |
+
market_points: list[dict[str, Any]] = []
|
| 775 |
+
for idx, row in df_plot_pontos.iterrows():
|
| 776 |
+
cor = colormap(row[cor_key]) if colormap and cor_key and pd.notna(row[cor_key]) else COR_PRINCIPAL
|
| 777 |
+
if tamanho_func and tamanho_key and pd.notna(row[tamanho_key]):
|
| 778 |
+
raio = float(tamanho_func(row[tamanho_key]))
|
| 779 |
+
else:
|
| 780 |
+
raio = raio_padrao
|
| 781 |
+
|
| 782 |
+
idx_display = int(row["index"]) if "index" in row.index else int(idx)
|
| 783 |
+
tooltip_payload = {
|
| 784 |
+
"title": f"Índice {idx_display}",
|
| 785 |
+
"label": str(tooltip_col or ""),
|
| 786 |
+
"value": (
|
| 787 |
+
_formatar_tooltip_valor(str(tooltip_col or ""), row[tooltip_key])
|
| 788 |
+
if tooltip_col and tooltip_key and tooltip_key in row.index
|
| 789 |
+
else ""
|
| 790 |
+
),
|
| 791 |
+
}
|
| 792 |
+
row_id_raw = row["__mesa_row_id__"] if "__mesa_row_id__" in row.index else None
|
| 793 |
+
market_points.append(
|
| 794 |
+
{
|
| 795 |
+
"lat": float(row[lat_plot_key]),
|
| 796 |
+
"lon": float(row[lon_plot_key]),
|
| 797 |
+
"indice": idx_display,
|
| 798 |
+
"row_id": int(row_id_raw) if row_id_raw is not None and pd.notna(row_id_raw) else None,
|
| 799 |
+
"color": str(cor),
|
| 800 |
+
"base_radius": float(max(1.0, raio)),
|
| 801 |
+
"tooltip": tooltip_payload,
|
| 802 |
+
}
|
| 803 |
+
)
|
| 804 |
+
|
| 805 |
+
return {
|
| 806 |
+
"type": "mesa_leaflet_payload",
|
| 807 |
+
"version": 1,
|
| 808 |
+
"center": [centro_lat, centro_lon],
|
| 809 |
+
"bounds": _resolver_bounds(df_mapa, lat_key, lon_key),
|
| 810 |
+
"tile_layers": _TILE_LAYERS,
|
| 811 |
+
"controls": {
|
| 812 |
+
"fullscreen": True,
|
| 813 |
+
"measure": True,
|
| 814 |
+
"layer_control": True,
|
| 815 |
+
},
|
| 816 |
+
"radius_behavior": {
|
| 817 |
+
"min_radius": 1.6,
|
| 818 |
+
"max_radius": 52.0,
|
| 819 |
+
"reference_zoom": 12.0,
|
| 820 |
+
"growth_factor": 0.20,
|
| 821 |
+
},
|
| 822 |
+
"show_indices": show_indices,
|
| 823 |
+
"show_bairros": True,
|
| 824 |
+
"bairros_geojson_url": bairros_geojson_url,
|
| 825 |
+
"legend": legend,
|
| 826 |
+
"market_points": market_points,
|
| 827 |
+
"trabalhos_tecnicos_points": build_trabalhos_tecnicos_marker_payloads(avaliandos_tecnicos),
|
| 828 |
+
}
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -53,6 +53,7 @@ from app.core.elaboracao.formatadores import (
|
|
| 53 |
formatar_micronumerosidade_html,
|
| 54 |
formatar_outliers_anteriores_html,
|
| 55 |
)
|
|
|
|
| 56 |
from app.models.session import SessionState
|
| 57 |
from app.runtime_config import resolve_core_path
|
| 58 |
from app.services import model_repository
|
|
@@ -899,6 +900,19 @@ def _render_mapa_if_enabled(session: SessionState, df: pd.DataFrame | None, **kw
|
|
| 899 |
return charts.criar_mapa(df, **kwargs)
|
| 900 |
|
| 901 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
def classificar_tipos_variaveis_x(session: SessionState, colunas_x: list[str] | None) -> dict[str, Any]:
|
| 903 |
df = session.df_original if session.df_original is not None else session.df_filtrado
|
| 904 |
if df is None:
|
|
@@ -1020,11 +1034,11 @@ def _set_dataframe_base(
|
|
| 1020 |
|
| 1021 |
colunas_numericas = [str(c) for c in obter_colunas_numericas(df)]
|
| 1022 |
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
| 1023 |
-
|
| 1024 |
|
| 1025 |
return {
|
| 1026 |
"dados": dataframe_to_payload(df, decimals=4),
|
| 1027 |
-
|
| 1028 |
"colunas_numericas": colunas_numericas,
|
| 1029 |
"coluna_y_padrao": coluna_y_padrao,
|
| 1030 |
"colunas_area_candidatas": detectar_colunas_area(df),
|
|
@@ -1308,7 +1322,7 @@ def apply_selection(
|
|
| 1308 |
},
|
| 1309 |
"transformacao_y": session.transformacao_y,
|
| 1310 |
"transform_fields": transform_fields,
|
| 1311 |
-
|
| 1312 |
"contexto": _selection_context(session),
|
| 1313 |
}
|
| 1314 |
|
|
@@ -2328,16 +2342,25 @@ def detalhes_knn_avaliacao_elaboracao(session: SessionState, valores_x: dict[str
|
|
| 2328 |
tabela_payload = dataframe_to_payload(df_vizinhos, decimals=4, max_rows=None)
|
| 2329 |
|
| 2330 |
# Import local evita acoplamento no carregamento inicial do módulo.
|
| 2331 |
-
from app.services.visualizacao_service import _coordenadas_de_valores_knn, _criar_mapa_knn_destaque
|
| 2332 |
|
| 2333 |
aval_lat, aval_lon = _coordenadas_de_valores_knn(entradas)
|
| 2334 |
-
|
| 2335 |
df_knn,
|
| 2336 |
posicoes_vizinhos,
|
| 2337 |
coluna_y_knn,
|
| 2338 |
avaliando_lat=aval_lat,
|
| 2339 |
avaliando_lon=aval_lon,
|
| 2340 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2341 |
avaliando = [{"variavel": str(col), "valor": entradas.get(col)} for col in colunas_x]
|
| 2342 |
if session.coluna_area and session.coluna_area not in colunas_x:
|
| 2343 |
avaliando.append({"variavel": f"{session.coluna_area} (area)", "valor": valor_area})
|
|
@@ -2350,6 +2373,7 @@ def detalhes_knn_avaliacao_elaboracao(session: SessionState, valores_x: dict[str
|
|
| 2350 |
return sanitize_value(
|
| 2351 |
{
|
| 2352 |
"mapa_html": mapa_html,
|
|
|
|
| 2353 |
"avaliando": avaliando,
|
| 2354 |
"vizinhos_tabela": tabela_payload,
|
| 2355 |
"knn": resultado_knn,
|
|
@@ -2612,7 +2636,8 @@ def atualizar_mapa(session: SessionState, var_mapa: str | None, modo_mapa: str |
|
|
| 2612 |
modo = "pontos"
|
| 2613 |
session.mapa_habilitado = True
|
| 2614 |
mapa_html = charts.criar_mapa(df, tamanho_col=tamanho_col, modo=modo)
|
| 2615 |
-
|
|
|
|
| 2616 |
|
| 2617 |
|
| 2618 |
def _normalizar_extremo_abs_residuos(valor: float | None) -> float | None:
|
|
@@ -2690,8 +2715,20 @@ def atualizar_mapa_residuos(
|
|
| 2690 |
cor_tick_values=ticks_valores,
|
| 2691 |
cor_tick_labels=ticks_labels,
|
| 2692 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2693 |
return {
|
| 2694 |
"mapa_html": mapa_html,
|
|
|
|
| 2695 |
"variavel_mapa": var_escolhida,
|
| 2696 |
"modo_mapa": modo,
|
| 2697 |
"escala_extremo_abs": extremo_abs,
|
|
@@ -2759,7 +2796,7 @@ def mapear_coordenadas_manualmente(session: SessionState, col_lat: str, col_lon:
|
|
| 2759 |
|
| 2760 |
return {
|
| 2761 |
"status": "Coordenadas mapeadas com sucesso",
|
| 2762 |
-
|
| 2763 |
"dados": dataframe_to_payload(df_filtrado, decimals=4),
|
| 2764 |
"coords": _build_coords_payload(df_novo, True),
|
| 2765 |
}
|
|
@@ -2790,7 +2827,7 @@ def geocodificar(session: SessionState, col_cdlog: str, col_num: str, auto_200:
|
|
| 2790 |
"status_html": status_html,
|
| 2791 |
"falhas_html": falhas_html,
|
| 2792 |
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 2793 |
-
|
| 2794 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2795 |
"coords": _build_coords_payload(session.df_original, True),
|
| 2796 |
}
|
|
@@ -2822,7 +2859,7 @@ def reiniciar_geocodificacao(session: SessionState) -> dict[str, Any]:
|
|
| 2822 |
"status_html": "",
|
| 2823 |
"falhas_html": "",
|
| 2824 |
"falhas_para_correcao": dataframe_to_payload(pd.DataFrame(), decimals=None),
|
| 2825 |
-
|
| 2826 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2827 |
"coords": _build_coords_payload(df_base, tem_coords),
|
| 2828 |
}
|
|
@@ -2849,7 +2886,7 @@ def excluir_coordenadas_para_geocodificacao(session: SessionState) -> dict[str,
|
|
| 2849 |
"status_html": "",
|
| 2850 |
"falhas_html": "",
|
| 2851 |
"falhas_para_correcao": dataframe_to_payload(pd.DataFrame(), decimals=None),
|
| 2852 |
-
|
| 2853 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2854 |
"coords": _build_coords_payload(session.df_original, False),
|
| 2855 |
}
|
|
@@ -2920,7 +2957,7 @@ def aplicar_correcoes_geocodificacao(
|
|
| 2920 |
"status_html": status_html,
|
| 2921 |
"falhas_html": falhas_html,
|
| 2922 |
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 2923 |
-
|
| 2924 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2925 |
"coords": _build_coords_payload(session.df_original, True),
|
| 2926 |
}
|
|
@@ -2938,7 +2975,7 @@ def limpar_historico_outliers(session: SessionState) -> dict[str, Any]:
|
|
| 2938 |
|
| 2939 |
return {
|
| 2940 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2941 |
-
|
| 2942 |
"outliers_html": "",
|
| 2943 |
"resumo_outliers": "Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0",
|
| 2944 |
"contexto": _selection_context(session),
|
|
|
|
| 53 |
formatar_micronumerosidade_html,
|
| 54 |
formatar_outliers_anteriores_html,
|
| 55 |
)
|
| 56 |
+
from app.core.visualizacao.map_payload import build_elaboracao_map_payload
|
| 57 |
from app.models.session import SessionState
|
| 58 |
from app.runtime_config import resolve_core_path
|
| 59 |
from app.services import model_repository
|
|
|
|
| 900 |
return charts.criar_mapa(df, **kwargs)
|
| 901 |
|
| 902 |
|
| 903 |
+
def _build_mapa_payload_if_enabled(session: SessionState, df: pd.DataFrame | None, **kwargs: Any) -> dict[str, Any] | None:
|
| 904 |
+
if not session.mapa_habilitado or df is None:
|
| 905 |
+
return None
|
| 906 |
+
return build_elaboracao_map_payload(df, **kwargs)
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def _mapa_bundle_if_enabled(session: SessionState, df: pd.DataFrame | None, **kwargs: Any) -> dict[str, Any]:
|
| 910 |
+
return {
|
| 911 |
+
"mapa_html": _render_mapa_if_enabled(session, df, **kwargs),
|
| 912 |
+
"mapa_payload": _build_mapa_payload_if_enabled(session, df, **kwargs),
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
|
| 916 |
def classificar_tipos_variaveis_x(session: SessionState, colunas_x: list[str] | None) -> dict[str, Any]:
|
| 917 |
df = session.df_original if session.df_original is not None else session.df_filtrado
|
| 918 |
if df is None:
|
|
|
|
| 1034 |
|
| 1035 |
colunas_numericas = [str(c) for c in obter_colunas_numericas(df)]
|
| 1036 |
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
| 1037 |
+
mapa_bundle = _mapa_bundle_if_enabled(session, df)
|
| 1038 |
|
| 1039 |
return {
|
| 1040 |
"dados": dataframe_to_payload(df, decimals=4),
|
| 1041 |
+
**mapa_bundle,
|
| 1042 |
"colunas_numericas": colunas_numericas,
|
| 1043 |
"coluna_y_padrao": coluna_y_padrao,
|
| 1044 |
"colunas_area_candidatas": detectar_colunas_area(df),
|
|
|
|
| 1322 |
},
|
| 1323 |
"transformacao_y": session.transformacao_y,
|
| 1324 |
"transform_fields": transform_fields,
|
| 1325 |
+
**_mapa_bundle_if_enabled(session, df_filtrado),
|
| 1326 |
"contexto": _selection_context(session),
|
| 1327 |
}
|
| 1328 |
|
|
|
|
| 2342 |
tabela_payload = dataframe_to_payload(df_vizinhos, decimals=4, max_rows=None)
|
| 2343 |
|
| 2344 |
# Import local evita acoplamento no carregamento inicial do módulo.
|
| 2345 |
+
from app.services.visualizacao_service import _coordenadas_de_valores_knn, _criar_mapa_knn_destaque, _criar_payload_knn_destaque
|
| 2346 |
|
| 2347 |
aval_lat, aval_lon = _coordenadas_de_valores_knn(entradas)
|
| 2348 |
+
mapa_payload = _criar_payload_knn_destaque(
|
| 2349 |
df_knn,
|
| 2350 |
posicoes_vizinhos,
|
| 2351 |
coluna_y_knn,
|
| 2352 |
avaliando_lat=aval_lat,
|
| 2353 |
avaliando_lon=aval_lon,
|
| 2354 |
)
|
| 2355 |
+
mapa_html = ""
|
| 2356 |
+
if mapa_payload is None:
|
| 2357 |
+
mapa_html = _criar_mapa_knn_destaque(
|
| 2358 |
+
df_knn,
|
| 2359 |
+
posicoes_vizinhos,
|
| 2360 |
+
coluna_y_knn,
|
| 2361 |
+
avaliando_lat=aval_lat,
|
| 2362 |
+
avaliando_lon=aval_lon,
|
| 2363 |
+
)
|
| 2364 |
avaliando = [{"variavel": str(col), "valor": entradas.get(col)} for col in colunas_x]
|
| 2365 |
if session.coluna_area and session.coluna_area not in colunas_x:
|
| 2366 |
avaliando.append({"variavel": f"{session.coluna_area} (area)", "valor": valor_area})
|
|
|
|
| 2373 |
return sanitize_value(
|
| 2374 |
{
|
| 2375 |
"mapa_html": mapa_html,
|
| 2376 |
+
"mapa_payload": mapa_payload,
|
| 2377 |
"avaliando": avaliando,
|
| 2378 |
"vizinhos_tabela": tabela_payload,
|
| 2379 |
"knn": resultado_knn,
|
|
|
|
| 2636 |
modo = "pontos"
|
| 2637 |
session.mapa_habilitado = True
|
| 2638 |
mapa_html = charts.criar_mapa(df, tamanho_col=tamanho_col, modo=modo)
|
| 2639 |
+
mapa_payload = build_elaboracao_map_payload(df, tamanho_col=tamanho_col, modo=modo)
|
| 2640 |
+
return {"mapa_html": mapa_html, "mapa_payload": mapa_payload}
|
| 2641 |
|
| 2642 |
|
| 2643 |
def _normalizar_extremo_abs_residuos(valor: float | None) -> float | None:
|
|
|
|
| 2715 |
cor_tick_values=ticks_valores,
|
| 2716 |
cor_tick_labels=ticks_labels,
|
| 2717 |
)
|
| 2718 |
+
mapa_payload = build_elaboracao_map_payload(
|
| 2719 |
+
df,
|
| 2720 |
+
tamanho_col=var_escolhida,
|
| 2721 |
+
modo=modo,
|
| 2722 |
+
cor_vmin=cor_vmin,
|
| 2723 |
+
cor_vmax=cor_vmax,
|
| 2724 |
+
cor_caption=caption,
|
| 2725 |
+
cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
|
| 2726 |
+
cor_tick_values=ticks_valores,
|
| 2727 |
+
cor_tick_labels=ticks_labels,
|
| 2728 |
+
)
|
| 2729 |
return {
|
| 2730 |
"mapa_html": mapa_html,
|
| 2731 |
+
"mapa_payload": mapa_payload,
|
| 2732 |
"variavel_mapa": var_escolhida,
|
| 2733 |
"modo_mapa": modo,
|
| 2734 |
"escala_extremo_abs": extremo_abs,
|
|
|
|
| 2796 |
|
| 2797 |
return {
|
| 2798 |
"status": "Coordenadas mapeadas com sucesso",
|
| 2799 |
+
**_mapa_bundle_if_enabled(session, df_filtrado),
|
| 2800 |
"dados": dataframe_to_payload(df_filtrado, decimals=4),
|
| 2801 |
"coords": _build_coords_payload(df_novo, True),
|
| 2802 |
}
|
|
|
|
| 2827 |
"status_html": status_html,
|
| 2828 |
"falhas_html": falhas_html,
|
| 2829 |
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 2830 |
+
**_mapa_bundle_if_enabled(session, session.df_filtrado),
|
| 2831 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2832 |
"coords": _build_coords_payload(session.df_original, True),
|
| 2833 |
}
|
|
|
|
| 2859 |
"status_html": "",
|
| 2860 |
"falhas_html": "",
|
| 2861 |
"falhas_para_correcao": dataframe_to_payload(pd.DataFrame(), decimals=None),
|
| 2862 |
+
**_mapa_bundle_if_enabled(session, session.df_filtrado),
|
| 2863 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2864 |
"coords": _build_coords_payload(df_base, tem_coords),
|
| 2865 |
}
|
|
|
|
| 2886 |
"status_html": "",
|
| 2887 |
"falhas_html": "",
|
| 2888 |
"falhas_para_correcao": dataframe_to_payload(pd.DataFrame(), decimals=None),
|
| 2889 |
+
**_mapa_bundle_if_enabled(session, session.df_filtrado),
|
| 2890 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2891 |
"coords": _build_coords_payload(session.df_original, False),
|
| 2892 |
}
|
|
|
|
| 2957 |
"status_html": status_html,
|
| 2958 |
"falhas_html": falhas_html,
|
| 2959 |
"falhas_para_correcao": dataframe_to_payload(df_correcoes, decimals=None),
|
| 2960 |
+
**_mapa_bundle_if_enabled(session, session.df_filtrado),
|
| 2961 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2962 |
"coords": _build_coords_payload(session.df_original, True),
|
| 2963 |
}
|
|
|
|
| 2975 |
|
| 2976 |
return {
|
| 2977 |
"dados": dataframe_to_payload(session.df_filtrado, decimals=4),
|
| 2978 |
+
**_mapa_bundle_if_enabled(session, session.df_filtrado),
|
| 2979 |
"outliers_html": "",
|
| 2980 |
"resumo_outliers": "Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0",
|
| 2981 |
"contexto": _selection_context(session),
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -24,9 +24,11 @@ from app.core.shapefile_runtime import load_attribute_records
|
|
| 24 |
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, normalizar_observacao_modelo
|
| 25 |
from app.core.map_layers import (
|
| 26 |
add_bairros_layer,
|
|
|
|
| 27 |
add_trabalhos_tecnicos_markers,
|
| 28 |
add_zoom_responsive_circle_markers,
|
| 29 |
)
|
|
|
|
| 30 |
from app.portable_runtime_log import append_runtime_log
|
| 31 |
from app.runtime_config import resolve_core_path
|
| 32 |
from app.services import model_repository, trabalhos_tecnicos_service
|
|
@@ -227,15 +229,19 @@ _ADMIN_CONFIG_LOCK = Lock()
|
|
| 227 |
_CACHE_SOURCE_SIGNATURE: str | None = None
|
| 228 |
_ADMIN_FONTES_SESSION: dict[str, list[str]] = {}
|
| 229 |
_CATALOGO_VIAS_CACHE: list[dict[str, Any]] | None = None
|
|
|
|
|
|
|
| 230 |
|
| 231 |
|
| 232 |
def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
|
| 233 |
-
global _CACHE_SOURCE_SIGNATURE
|
| 234 |
resolved = model_repository.resolve_model_repository()
|
| 235 |
with _CACHE_LOCK:
|
| 236 |
if _CACHE_SOURCE_SIGNATURE != resolved.signature:
|
| 237 |
_CACHE.clear()
|
| 238 |
_CACHE_SOURCE_SIGNATURE = resolved.signature
|
|
|
|
|
|
|
| 239 |
return resolved
|
| 240 |
|
| 241 |
|
|
@@ -622,6 +628,46 @@ def _montar_familias_versoes_modelos(caminhos_modelo: list[Path]) -> dict[str, l
|
|
| 622 |
return familias
|
| 623 |
|
| 624 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
def _agrupar_nomes_modelo_por_familia(values: list[Any]) -> dict[str, list[str]]:
|
| 626 |
familias: dict[str, list[str]] = {}
|
| 627 |
for value in values:
|
|
@@ -929,7 +975,7 @@ def gerar_mapa_modelos(
|
|
| 929 |
total_pontos = 0
|
| 930 |
for modelo in modelos_plotados:
|
| 931 |
total_pontos += int(modelo["total_pontos"])
|
| 932 |
-
|
| 933 |
modelos_plotados,
|
| 934 |
bounds,
|
| 935 |
avaliandos_geo,
|
|
@@ -937,6 +983,16 @@ def gerar_mapa_modelos(
|
|
| 937 |
avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
|
| 938 |
trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
|
| 939 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
|
| 941 |
total_trabalhos_modelos = len(
|
| 942 |
{
|
|
@@ -964,6 +1020,9 @@ def gerar_mapa_modelos(
|
|
| 964 |
"mapa_html": mapa_html,
|
| 965 |
"mapa_html_pontos": mapa_html if modo_exibicao_norm == "pontos" else "",
|
| 966 |
"mapa_html_cobertura": mapa_html if modo_exibicao_norm == "cobertura" else "",
|
|
|
|
|
|
|
|
|
|
| 967 |
"total_modelos_plotados": len(modelos_plotados),
|
| 968 |
"total_pontos": total_pontos,
|
| 969 |
"modelos_plotados": [
|
|
@@ -1053,6 +1112,224 @@ def _montar_tooltip_distancia_modelo(modelo: dict[str, Any]) -> str:
|
|
| 1053 |
return ""
|
| 1054 |
|
| 1055 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
def _renderizar_mapa_modelos(
|
| 1057 |
modelos_plotados: list[dict[str, Any]],
|
| 1058 |
bounds: list[list[float]],
|
|
|
|
| 24 |
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, normalizar_observacao_modelo
|
| 25 |
from app.core.map_layers import (
|
| 26 |
add_bairros_layer,
|
| 27 |
+
build_trabalhos_tecnicos_marker_payloads,
|
| 28 |
add_trabalhos_tecnicos_markers,
|
| 29 |
add_zoom_responsive_circle_markers,
|
| 30 |
)
|
| 31 |
+
from app.core.visualizacao.map_payload import build_leaflet_payload
|
| 32 |
from app.portable_runtime_log import append_runtime_log
|
| 33 |
from app.runtime_config import resolve_core_path
|
| 34 |
from app.services import model_repository, trabalhos_tecnicos_service
|
|
|
|
| 229 |
_CACHE_SOURCE_SIGNATURE: str | None = None
|
| 230 |
_ADMIN_FONTES_SESSION: dict[str, list[str]] = {}
|
| 231 |
_CATALOGO_VIAS_CACHE: list[dict[str, Any]] | None = None
|
| 232 |
+
_FAMILIAS_VERSOES_CACHE: dict[str, list[dict[str, Any]]] | None = None
|
| 233 |
+
_FAMILIAS_VERSOES_SIGNATURE: tuple[str, tuple[tuple[str, int, int], ...]] | None = None
|
| 234 |
|
| 235 |
|
| 236 |
def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
|
| 237 |
+
global _CACHE_SOURCE_SIGNATURE, _FAMILIAS_VERSOES_CACHE, _FAMILIAS_VERSOES_SIGNATURE
|
| 238 |
resolved = model_repository.resolve_model_repository()
|
| 239 |
with _CACHE_LOCK:
|
| 240 |
if _CACHE_SOURCE_SIGNATURE != resolved.signature:
|
| 241 |
_CACHE.clear()
|
| 242 |
_CACHE_SOURCE_SIGNATURE = resolved.signature
|
| 243 |
+
_FAMILIAS_VERSOES_CACHE = None
|
| 244 |
+
_FAMILIAS_VERSOES_SIGNATURE = None
|
| 245 |
return resolved
|
| 246 |
|
| 247 |
|
|
|
|
| 628 |
return familias
|
| 629 |
|
| 630 |
|
| 631 |
+
def _assinatura_modelos_familias(caminhos_modelo: list[Path]) -> tuple[tuple[str, int, int], ...]:
|
| 632 |
+
assinatura: list[tuple[str, int, int]] = []
|
| 633 |
+
for caminho in sorted(caminhos_modelo, key=lambda item: item.name.lower()):
|
| 634 |
+
try:
|
| 635 |
+
stat = caminho.stat()
|
| 636 |
+
except OSError:
|
| 637 |
+
continue
|
| 638 |
+
assinatura.append((caminho.name, int(stat.st_mtime_ns), int(stat.st_size)))
|
| 639 |
+
return tuple(assinatura)
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
def obter_familias_versoes_modelos_cache(caminhos_modelo: list[Path] | None = None) -> dict[str, list[dict[str, Any]]]:
|
| 643 |
+
global _FAMILIAS_VERSOES_CACHE, _FAMILIAS_VERSOES_SIGNATURE
|
| 644 |
+
|
| 645 |
+
resolved = _resolver_repositorio_modelos()
|
| 646 |
+
caminhos = list(caminhos_modelo) if caminhos_modelo is not None else list(resolved.modelos_dir.glob("*.dai"))
|
| 647 |
+
assinatura = (resolved.signature, _assinatura_modelos_familias(caminhos))
|
| 648 |
+
|
| 649 |
+
with _CACHE_LOCK:
|
| 650 |
+
if _FAMILIAS_VERSOES_SIGNATURE == assinatura and isinstance(_FAMILIAS_VERSOES_CACHE, dict):
|
| 651 |
+
return {
|
| 652 |
+
chave: [dict(item) for item in itens]
|
| 653 |
+
for chave, itens in _FAMILIAS_VERSOES_CACHE.items()
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
familias = _montar_familias_versoes_modelos(caminhos)
|
| 657 |
+
|
| 658 |
+
with _CACHE_LOCK:
|
| 659 |
+
_FAMILIAS_VERSOES_SIGNATURE = assinatura
|
| 660 |
+
_FAMILIAS_VERSOES_CACHE = {
|
| 661 |
+
chave: [dict(item) for item in itens]
|
| 662 |
+
for chave, itens in familias.items()
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
return {
|
| 666 |
+
chave: [dict(item) for item in itens]
|
| 667 |
+
for chave, itens in familias.items()
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
|
| 671 |
def _agrupar_nomes_modelo_por_familia(values: list[Any]) -> dict[str, list[str]]:
|
| 672 |
familias: dict[str, list[str]] = {}
|
| 673 |
for value in values:
|
|
|
|
| 975 |
total_pontos = 0
|
| 976 |
for modelo in modelos_plotados:
|
| 977 |
total_pontos += int(modelo["total_pontos"])
|
| 978 |
+
mapa_payload = _build_mapa_modelos_payload(
|
| 979 |
modelos_plotados,
|
| 980 |
bounds,
|
| 981 |
avaliandos_geo,
|
|
|
|
| 983 |
avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
|
| 984 |
trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
|
| 985 |
)
|
| 986 |
+
mapa_html = ""
|
| 987 |
+
if mapa_payload is None:
|
| 988 |
+
mapa_html = _renderizar_mapa_modelos(
|
| 989 |
+
modelos_plotados,
|
| 990 |
+
bounds,
|
| 991 |
+
avaliandos_geo,
|
| 992 |
+
modo_exibicao_norm,
|
| 993 |
+
avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
|
| 994 |
+
trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
|
| 995 |
+
)
|
| 996 |
|
| 997 |
total_trabalhos_modelos = len(
|
| 998 |
{
|
|
|
|
| 1020 |
"mapa_html": mapa_html,
|
| 1021 |
"mapa_html_pontos": mapa_html if modo_exibicao_norm == "pontos" else "",
|
| 1022 |
"mapa_html_cobertura": mapa_html if modo_exibicao_norm == "cobertura" else "",
|
| 1023 |
+
"mapa_payload": mapa_payload,
|
| 1024 |
+
"mapa_payload_pontos": mapa_payload if modo_exibicao_norm == "pontos" else None,
|
| 1025 |
+
"mapa_payload_cobertura": mapa_payload if modo_exibicao_norm == "cobertura" else None,
|
| 1026 |
"total_modelos_plotados": len(modelos_plotados),
|
| 1027 |
"total_pontos": total_pontos,
|
| 1028 |
"modelos_plotados": [
|
|
|
|
| 1112 |
return ""
|
| 1113 |
|
| 1114 |
|
| 1115 |
+
def _tooltip_mapa_modelo_html(modelo: dict[str, Any]) -> str:
|
| 1116 |
+
nome = str(modelo.get("nome") or "Modelo").strip() or "Modelo"
|
| 1117 |
+
tooltip_distancia = _montar_tooltip_distancia_modelo(modelo)
|
| 1118 |
+
if tooltip_distancia:
|
| 1119 |
+
return (
|
| 1120 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.5;'>"
|
| 1121 |
+
f"<b>{escape(nome)}</b><br>{escape(tooltip_distancia)}"
|
| 1122 |
+
"</div>"
|
| 1123 |
+
)
|
| 1124 |
+
return (
|
| 1125 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.5;'>"
|
| 1126 |
+
f"<b>{escape(nome)}</b>"
|
| 1127 |
+
"</div>"
|
| 1128 |
+
)
|
| 1129 |
+
|
| 1130 |
+
|
| 1131 |
+
def _marker_payloads_avaliandos(avaliandos_geo: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 1132 |
+
payloads: list[dict[str, Any]] = []
|
| 1133 |
+
for idx, avaliando in enumerate(avaliandos_geo):
|
| 1134 |
+
lat_item, lon_item = _normalizar_coordenadas_avaliando(avaliando.get("lat"), avaliando.get("lon"))
|
| 1135 |
+
if lat_item is None or lon_item is None:
|
| 1136 |
+
continue
|
| 1137 |
+
label = str(avaliando.get("label") or f"A{idx + 1}").strip() or f"A{idx + 1}"
|
| 1138 |
+
endereco = str(avaliando.get("logradouro") or "").strip()
|
| 1139 |
+
numero_usado = str(avaliando.get("numero_usado") or "").strip()
|
| 1140 |
+
tooltip = label
|
| 1141 |
+
if endereco:
|
| 1142 |
+
tooltip = f"{tooltip} • {endereco}{f', {numero_usado}' if numero_usado else ''}"
|
| 1143 |
+
marker_html = (
|
| 1144 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1145 |
+
"width:28px;height:28px;border-radius:999px;background:#c62828;color:#fff;"
|
| 1146 |
+
"font-weight:800;font-size:12px;border:2px solid rgba(255,255,255,0.95);"
|
| 1147 |
+
"box-shadow:0 2px 6px rgba(0,0,0,0.18);'>"
|
| 1148 |
+
f"{escape(label)}"
|
| 1149 |
+
"</div>"
|
| 1150 |
+
)
|
| 1151 |
+
payloads.append(
|
| 1152 |
+
{
|
| 1153 |
+
"lat": float(lat_item),
|
| 1154 |
+
"lon": float(lon_item),
|
| 1155 |
+
"tooltip_html": escape(tooltip),
|
| 1156 |
+
"marker_html": marker_html,
|
| 1157 |
+
"icon_size": [28, 28],
|
| 1158 |
+
"icon_anchor": [14, 14],
|
| 1159 |
+
"class_name": "mesa-avaliando-marker",
|
| 1160 |
+
}
|
| 1161 |
+
)
|
| 1162 |
+
return payloads
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
def _shapes_modelo_payload(
|
| 1166 |
+
modelo: dict[str, Any],
|
| 1167 |
+
aval_lat: float | None,
|
| 1168 |
+
aval_lon: float | None,
|
| 1169 |
+
) -> list[dict[str, Any]]:
|
| 1170 |
+
geometria = modelo.get("geometria") or {}
|
| 1171 |
+
geom_wgs84 = geometria.get("geom_wgs84")
|
| 1172 |
+
if geom_wgs84 is None:
|
| 1173 |
+
return []
|
| 1174 |
+
|
| 1175 |
+
tooltip = _tooltip_mapa_modelo_html(modelo)
|
| 1176 |
+
cor = str(modelo.get("cor") or "#1f77b4")
|
| 1177 |
+
geom_type = str(geometria.get("geom_type") or "")
|
| 1178 |
+
shapes: list[dict[str, Any]] = []
|
| 1179 |
+
|
| 1180 |
+
try:
|
| 1181 |
+
if geom_type == "Polygon":
|
| 1182 |
+
coords = [[float(lat), float(lon)] for lon, lat in list(geom_wgs84.exterior.coords)]
|
| 1183 |
+
shapes.append({"type": "polygon", "coords": coords, "color": cor, "fill": True, "fill_opacity": 0.12, "weight": 2, "tooltip_html": tooltip})
|
| 1184 |
+
elif geom_type == "MultiPolygon":
|
| 1185 |
+
for poligono in list(getattr(geom_wgs84, "geoms", [])):
|
| 1186 |
+
coords = [[float(lat), float(lon)] for lon, lat in list(poligono.exterior.coords)]
|
| 1187 |
+
shapes.append({"type": "polygon", "coords": coords, "color": cor, "fill": True, "fill_opacity": 0.12, "weight": 2, "tooltip_html": tooltip})
|
| 1188 |
+
elif geom_type == "LineString":
|
| 1189 |
+
coords = [[float(lat), float(lon)] for lon, lat in list(geom_wgs84.coords)]
|
| 1190 |
+
shapes.append({"type": "polyline", "coords": coords, "color": cor, "weight": 3, "opacity": 0.8, "tooltip_html": tooltip})
|
| 1191 |
+
elif geom_type == "MultiLineString":
|
| 1192 |
+
for linha in list(getattr(geom_wgs84, "geoms", [])):
|
| 1193 |
+
coords = [[float(lat), float(lon)] for lon, lat in list(linha.coords)]
|
| 1194 |
+
shapes.append({"type": "polyline", "coords": coords, "color": cor, "weight": 3, "opacity": 0.8, "tooltip_html": tooltip})
|
| 1195 |
+
elif geom_type == "Point":
|
| 1196 |
+
shapes.append({"type": "circlemarker", "center": [float(geom_wgs84.y), float(geom_wgs84.x)], "radius": 6, "color": cor, "fill": True, "fill_opacity": 0.7, "tooltip_html": tooltip})
|
| 1197 |
+
elif geom_type == "MultiPoint":
|
| 1198 |
+
for ponto in list(getattr(geom_wgs84, "geoms", [])):
|
| 1199 |
+
shapes.append({"type": "circlemarker", "center": [float(ponto.y), float(ponto.x)], "radius": 6, "color": cor, "fill": True, "fill_opacity": 0.7, "tooltip_html": tooltip})
|
| 1200 |
+
except Exception:
|
| 1201 |
+
return []
|
| 1202 |
+
|
| 1203 |
+
if aval_lat is None or aval_lon is None:
|
| 1204 |
+
return shapes
|
| 1205 |
+
|
| 1206 |
+
try:
|
| 1207 |
+
from shapely.geometry import Point
|
| 1208 |
+
from shapely.ops import nearest_points
|
| 1209 |
+
except ImportError:
|
| 1210 |
+
return shapes
|
| 1211 |
+
|
| 1212 |
+
try:
|
| 1213 |
+
aval_point = Point(float(aval_lon), float(aval_lat))
|
| 1214 |
+
_, nearest_geom = nearest_points(aval_point, geom_wgs84)
|
| 1215 |
+
distancia_km = _to_float_or_none(modelo.get("distancia_km"))
|
| 1216 |
+
if nearest_geom is None or distancia_km is None or distancia_km <= 0:
|
| 1217 |
+
return shapes
|
| 1218 |
+
shapes.append(
|
| 1219 |
+
{
|
| 1220 |
+
"type": "polyline",
|
| 1221 |
+
"coords": [
|
| 1222 |
+
[float(aval_point.y), float(aval_point.x)],
|
| 1223 |
+
[float(nearest_geom.y), float(nearest_geom.x)],
|
| 1224 |
+
],
|
| 1225 |
+
"color": cor,
|
| 1226 |
+
"weight": 2,
|
| 1227 |
+
"opacity": 0.65,
|
| 1228 |
+
"dash_array": "6,6",
|
| 1229 |
+
"tooltip_html": f"Ligação de distância • {escape(str(modelo.get('nome') or 'Modelo'))}",
|
| 1230 |
+
}
|
| 1231 |
+
)
|
| 1232 |
+
except Exception:
|
| 1233 |
+
return shapes
|
| 1234 |
+
|
| 1235 |
+
return shapes
|
| 1236 |
+
|
| 1237 |
+
|
| 1238 |
+
def _build_mapa_modelos_payload(
|
| 1239 |
+
modelos_plotados: list[dict[str, Any]],
|
| 1240 |
+
bounds: list[list[float]],
|
| 1241 |
+
avaliandos_geo: list[dict[str, Any]],
|
| 1242 |
+
modo_exibicao: str,
|
| 1243 |
+
avaliandos_tecnicos_proximos: list[dict[str, Any]] | None = None,
|
| 1244 |
+
trabalhos_tecnicos_raio_m: int | None = None,
|
| 1245 |
+
) -> dict[str, Any] | None:
|
| 1246 |
+
overlay_layers: list[dict[str, Any]] = []
|
| 1247 |
+
avaliando_unico = avaliandos_geo[0] if len(avaliandos_geo) == 1 else None
|
| 1248 |
+
aval_lat = avaliando_unico.get("lat") if avaliando_unico is not None else None
|
| 1249 |
+
aval_lon = avaliando_unico.get("lon") if avaliando_unico is not None else None
|
| 1250 |
+
|
| 1251 |
+
for modelo in modelos_plotados:
|
| 1252 |
+
layer: dict[str, Any] = {
|
| 1253 |
+
"id": str(modelo.get("id") or ""),
|
| 1254 |
+
"label": str(modelo.get("nome") or "Modelo"),
|
| 1255 |
+
"show": True,
|
| 1256 |
+
"markers": build_trabalhos_tecnicos_marker_payloads(modelo.get("avaliandos_tecnicos") or []),
|
| 1257 |
+
}
|
| 1258 |
+
if modo_exibicao == "pontos":
|
| 1259 |
+
tooltip_html = _tooltip_mapa_modelo_html(modelo)
|
| 1260 |
+
layer["points"] = [
|
| 1261 |
+
{
|
| 1262 |
+
"lat": float(ponto["lat"]),
|
| 1263 |
+
"lon": float(ponto["lon"]),
|
| 1264 |
+
"color": str(modelo.get("cor") or "#1f77b4"),
|
| 1265 |
+
"base_radius": 3.0,
|
| 1266 |
+
"stroke_color": str(modelo.get("cor") or "#1f77b4"),
|
| 1267 |
+
"stroke_weight": 1.0,
|
| 1268 |
+
"fill_opacity": 0.72,
|
| 1269 |
+
"tooltip_html": tooltip_html,
|
| 1270 |
+
}
|
| 1271 |
+
for ponto in (modelo.get("pontos") or [])
|
| 1272 |
+
]
|
| 1273 |
+
else:
|
| 1274 |
+
layer["shapes"] = _shapes_modelo_payload(modelo, aval_lat, aval_lon)
|
| 1275 |
+
overlay_layers.append(layer)
|
| 1276 |
+
|
| 1277 |
+
if avaliandos_tecnicos_proximos is not None:
|
| 1278 |
+
label_raio = f" (até {int(trabalhos_tecnicos_raio_m or 0)} m)" if trabalhos_tecnicos_raio_m is not None else ""
|
| 1279 |
+
proximos_layer: dict[str, Any] = {
|
| 1280 |
+
"id": "trabalhos-tecnicos-proximos",
|
| 1281 |
+
"label": f"Trabalhos técnicos próximos{label_raio}",
|
| 1282 |
+
"show": True,
|
| 1283 |
+
"markers": build_trabalhos_tecnicos_marker_payloads(
|
| 1284 |
+
avaliandos_tecnicos_proximos or [],
|
| 1285 |
+
origem="pesquisa_mapa",
|
| 1286 |
+
marker_style="estrela",
|
| 1287 |
+
ignore_bounds=False,
|
| 1288 |
+
),
|
| 1289 |
+
"shapes": [],
|
| 1290 |
+
}
|
| 1291 |
+
if int(trabalhos_tecnicos_raio_m or 0) > 0:
|
| 1292 |
+
for idx, avaliando in enumerate(avaliandos_geo):
|
| 1293 |
+
lat_item, lon_item = _normalizar_coordenadas_avaliando(avaliando.get("lat"), avaliando.get("lon"))
|
| 1294 |
+
if lat_item is None or lon_item is None:
|
| 1295 |
+
continue
|
| 1296 |
+
label = str(avaliando.get("label") or f"A{idx + 1}").strip() or f"A{idx + 1}"
|
| 1297 |
+
tooltip_raio = (
|
| 1298 |
+
f"Raio de busca: até {int(trabalhos_tecnicos_raio_m)} m"
|
| 1299 |
+
if len(avaliandos_geo) == 1
|
| 1300 |
+
else f"Raio de busca • {label}: até {int(trabalhos_tecnicos_raio_m)} m"
|
| 1301 |
+
)
|
| 1302 |
+
proximos_layer["shapes"].append(
|
| 1303 |
+
{
|
| 1304 |
+
"type": "circle",
|
| 1305 |
+
"center": [float(lat_item), float(lon_item)],
|
| 1306 |
+
"radius_m": float(trabalhos_tecnicos_raio_m),
|
| 1307 |
+
"color": "#c62828",
|
| 1308 |
+
"weight": 2,
|
| 1309 |
+
"opacity": 0.75,
|
| 1310 |
+
"fill": True,
|
| 1311 |
+
"fill_color": "#c62828",
|
| 1312 |
+
"fill_opacity": 0.06,
|
| 1313 |
+
"dash_array": "6,6",
|
| 1314 |
+
"tooltip_html": tooltip_raio,
|
| 1315 |
+
}
|
| 1316 |
+
)
|
| 1317 |
+
overlay_layers.append(proximos_layer)
|
| 1318 |
+
|
| 1319 |
+
avaliandos_markers = _marker_payloads_avaliandos(avaliandos_geo)
|
| 1320 |
+
if avaliandos_markers:
|
| 1321 |
+
overlay_layers.append(
|
| 1322 |
+
{
|
| 1323 |
+
"id": "avaliandos",
|
| 1324 |
+
"label": "Avaliando" if len(avaliandos_geo) == 1 else "Avaliandos",
|
| 1325 |
+
"show": True,
|
| 1326 |
+
"markers": avaliandos_markers,
|
| 1327 |
+
}
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
return build_leaflet_payload(bounds=bounds, overlay_layers=overlay_layers, show_bairros=True)
|
| 1331 |
+
|
| 1332 |
+
|
| 1333 |
def _renderizar_mapa_modelos(
|
| 1334 |
modelos_plotados: list[dict[str, Any]],
|
| 1335 |
bounds: list[list[float]],
|
backend/app/services/trabalhos_tecnicos_service.py
CHANGED
|
@@ -16,9 +16,11 @@ from folium import plugins
|
|
| 16 |
from app.core.map_layers import (
|
| 17 |
add_bairros_layer,
|
| 18 |
add_popup_pagination_handlers,
|
|
|
|
| 19 |
add_trabalhos_tecnicos_markers,
|
| 20 |
add_zoom_responsive_circle_markers,
|
| 21 |
)
|
|
|
|
| 22 |
from app.services import model_repository, trabalhos_tecnicos_repository
|
| 23 |
from app.services.serializers import sanitize_value
|
| 24 |
|
|
@@ -872,6 +874,129 @@ def _normalizar_modo_mapa_trabalhos(value: Any) -> str:
|
|
| 872 |
return MAPA_TRABALHOS_CLUSTERIZADO
|
| 873 |
|
| 874 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
def gerar_mapa_trabalhos(
|
| 876 |
trabalhos_ids: list[str] | None = None,
|
| 877 |
*,
|
|
@@ -901,6 +1026,7 @@ def gerar_mapa_trabalhos(
|
|
| 901 |
return sanitize_value(
|
| 902 |
{
|
| 903 |
"mapa_html": "",
|
|
|
|
| 904 |
"total_trabalhos": 0,
|
| 905 |
"total_imoveis": 0,
|
| 906 |
"status": "Nenhum trabalho técnico georreferenciado foi encontrado para o mapa.",
|
|
@@ -977,9 +1103,11 @@ def gerar_mapa_trabalhos(
|
|
| 977 |
|
| 978 |
total_trabalhos = len({str(item.get("trabalho_id") or "").strip() for item in pontos if str(item.get("trabalho_id") or "").strip()})
|
| 979 |
total_imoveis = len(pontos)
|
|
|
|
| 980 |
return sanitize_value(
|
| 981 |
{
|
| 982 |
"mapa_html": mapa.get_root().render(),
|
|
|
|
| 983 |
"total_trabalhos": total_trabalhos,
|
| 984 |
"total_imoveis": total_imoveis,
|
| 985 |
"modo_exibicao": modo_exibicao_norm,
|
|
@@ -1166,6 +1294,7 @@ def detalhar_trabalho(trabalho_id: str) -> dict[str, Any]:
|
|
| 1166 |
"trabalho": {
|
| 1167 |
**trabalho,
|
| 1168 |
"mapa_html": _criar_mapa_trabalho(str(trabalho.get("nome") or chave), imoveis),
|
|
|
|
| 1169 |
"fonte": _source_payload(resolved, meta),
|
| 1170 |
}
|
| 1171 |
}
|
|
|
|
| 16 |
from app.core.map_layers import (
|
| 17 |
add_bairros_layer,
|
| 18 |
add_popup_pagination_handlers,
|
| 19 |
+
build_trabalhos_tecnicos_marker_payloads,
|
| 20 |
add_trabalhos_tecnicos_markers,
|
| 21 |
add_zoom_responsive_circle_markers,
|
| 22 |
)
|
| 23 |
+
from app.core.visualizacao.map_payload import build_leaflet_payload
|
| 24 |
from app.services import model_repository, trabalhos_tecnicos_repository
|
| 25 |
from app.services.serializers import sanitize_value
|
| 26 |
|
|
|
|
| 874 |
return MAPA_TRABALHOS_CLUSTERIZADO
|
| 875 |
|
| 876 |
|
| 877 |
+
def _marker_payload_avaliando(lat: float, lon: float) -> dict[str, Any]:
|
| 878 |
+
return {
|
| 879 |
+
"lat": float(lat),
|
| 880 |
+
"lon": float(lon),
|
| 881 |
+
"tooltip_html": (
|
| 882 |
+
'<div style="font-family:\'Segoe UI\',Arial,sans-serif; font-size:13px; line-height:1.45;">'
|
| 883 |
+
'<strong>Avaliando</strong>'
|
| 884 |
+
'</div>'
|
| 885 |
+
),
|
| 886 |
+
"popup_html": (
|
| 887 |
+
'<div style="font-family:\'Segoe UI\',Arial,sans-serif; font-size:13px; line-height:1.45;">'
|
| 888 |
+
'<strong>Avaliando</strong>'
|
| 889 |
+
'</div>'
|
| 890 |
+
),
|
| 891 |
+
"marker_html": (
|
| 892 |
+
"<div style='display:flex;align-items:center;justify-content:center;width:18px;height:18px;'>"
|
| 893 |
+
"<svg width='18' height='18' viewBox='0 0 24 24' aria-hidden='true'>"
|
| 894 |
+
"<path d='M12 3 4 9v11h5v-6h6v6h5V9l-8-6Z' fill='#d7263d' stroke='#8f1522' stroke-width='1.5' stroke-linejoin='round'/>"
|
| 895 |
+
"</svg></div>"
|
| 896 |
+
),
|
| 897 |
+
"icon_size": [18, 18],
|
| 898 |
+
"icon_anchor": [9, 9],
|
| 899 |
+
"class_name": "mesa-trabalho-tecnico-marker",
|
| 900 |
+
"ignore_bounds": False,
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
def _build_mapa_trabalhos_payload(
|
| 905 |
+
pontos: list[dict[str, Any]],
|
| 906 |
+
*,
|
| 907 |
+
avaliando_coords: tuple[float | None, float | None] | None = None,
|
| 908 |
+
) -> dict[str, Any] | None:
|
| 909 |
+
bounds: list[list[float]] = [
|
| 910 |
+
[float(item["coord_lat"]), float(item["coord_lon"])]
|
| 911 |
+
for item in pontos
|
| 912 |
+
]
|
| 913 |
+
|
| 914 |
+
overlay_layers: list[dict[str, Any]] = [
|
| 915 |
+
{
|
| 916 |
+
"id": "trabalhos_tecnicos",
|
| 917 |
+
"label": "Trabalhos técnicos",
|
| 918 |
+
"show": True,
|
| 919 |
+
"markers": build_trabalhos_tecnicos_marker_payloads(
|
| 920 |
+
pontos,
|
| 921 |
+
origem="trabalhos_tecnicos_mapa",
|
| 922 |
+
marker_style="ponto",
|
| 923 |
+
ignore_bounds=False,
|
| 924 |
+
),
|
| 925 |
+
}
|
| 926 |
+
]
|
| 927 |
+
|
| 928 |
+
aval_lat, aval_lon = avaliando_coords or (None, None)
|
| 929 |
+
if aval_lat is not None and aval_lon is not None:
|
| 930 |
+
bounds.append([float(aval_lat), float(aval_lon)])
|
| 931 |
+
overlay_layers.append(
|
| 932 |
+
{
|
| 933 |
+
"id": "avaliando",
|
| 934 |
+
"label": "Avaliando",
|
| 935 |
+
"show": True,
|
| 936 |
+
"markers": [_marker_payload_avaliando(float(aval_lat), float(aval_lon))],
|
| 937 |
+
}
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
return build_leaflet_payload(bounds=bounds, overlay_layers=overlay_layers, show_bairros=True)
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
def _build_mapa_trabalho_payload(nome_trabalho: str, imoveis: list[dict[str, Any]]) -> dict[str, Any] | None:
|
| 944 |
+
pontos = [
|
| 945 |
+
item for item in imoveis
|
| 946 |
+
if _coordenada_valida(item.get("coord_x")) and _coordenada_valida(item.get("coord_y"))
|
| 947 |
+
]
|
| 948 |
+
if not pontos:
|
| 949 |
+
return None
|
| 950 |
+
|
| 951 |
+
bounds = [[float(item["coord_y"]), float(item["coord_x"])] for item in pontos]
|
| 952 |
+
markers: list[dict[str, Any]] = []
|
| 953 |
+
for index, item in enumerate(pontos, start=1):
|
| 954 |
+
label = str(item.get("label") or f"Imóvel {index}").strip()
|
| 955 |
+
endereco = str(item.get("endereco") or "").strip()
|
| 956 |
+
numero = str(item.get("numero") or "").strip()
|
| 957 |
+
endereco_texto = ", ".join([value for value in [endereco, numero] if value]) or "Endereço não informado"
|
| 958 |
+
modelos = [str(valor).strip() for valor in (item.get("modelos") or []) if str(valor).strip()]
|
| 959 |
+
modelos_texto = ", ".join(modelos) or "Sem modelo informado"
|
| 960 |
+
popup_html = (
|
| 961 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.5; min-width:240px;'>"
|
| 962 |
+
f"<div style='margin-bottom:6px; font-weight:700; color:#24405b;'>{label}</div>"
|
| 963 |
+
f"<div><span style='color:#666;'>Trabalho:</span> {str(nome_trabalho)}</div>"
|
| 964 |
+
f"<div><span style='color:#666;'>Endereço:</span> {endereco_texto}</div>"
|
| 965 |
+
f"<div><span style='color:#666;'>Modelos:</span> {modelos_texto}</div>"
|
| 966 |
+
"</div>"
|
| 967 |
+
)
|
| 968 |
+
markers.append(
|
| 969 |
+
{
|
| 970 |
+
"lat": float(item["coord_y"]),
|
| 971 |
+
"lon": float(item["coord_x"]),
|
| 972 |
+
"tooltip_html": popup_html,
|
| 973 |
+
"popup_html": popup_html,
|
| 974 |
+
"marker_html": (
|
| 975 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 976 |
+
"width:8px;height:8px;border-radius:999px;background:#1f6fb2;"
|
| 977 |
+
"border:1px solid #ffffff;box-shadow:0 0 0 1px rgba(20,42,66,0.20);'></div>"
|
| 978 |
+
),
|
| 979 |
+
"icon_size": [8, 8],
|
| 980 |
+
"icon_anchor": [4, 4],
|
| 981 |
+
"class_name": "mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-marker-dot",
|
| 982 |
+
"ignore_bounds": False,
|
| 983 |
+
}
|
| 984 |
+
)
|
| 985 |
+
|
| 986 |
+
return build_leaflet_payload(
|
| 987 |
+
bounds=bounds,
|
| 988 |
+
overlay_layers=[
|
| 989 |
+
{
|
| 990 |
+
"id": "trabalho",
|
| 991 |
+
"label": str(nome_trabalho or "Imóveis do trabalho"),
|
| 992 |
+
"show": True,
|
| 993 |
+
"markers": markers,
|
| 994 |
+
}
|
| 995 |
+
],
|
| 996 |
+
show_bairros=True,
|
| 997 |
+
)
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
def gerar_mapa_trabalhos(
|
| 1001 |
trabalhos_ids: list[str] | None = None,
|
| 1002 |
*,
|
|
|
|
| 1026 |
return sanitize_value(
|
| 1027 |
{
|
| 1028 |
"mapa_html": "",
|
| 1029 |
+
"mapa_payload": None,
|
| 1030 |
"total_trabalhos": 0,
|
| 1031 |
"total_imoveis": 0,
|
| 1032 |
"status": "Nenhum trabalho técnico georreferenciado foi encontrado para o mapa.",
|
|
|
|
| 1103 |
|
| 1104 |
total_trabalhos = len({str(item.get("trabalho_id") or "").strip() for item in pontos if str(item.get("trabalho_id") or "").strip()})
|
| 1105 |
total_imoveis = len(pontos)
|
| 1106 |
+
mapa_payload = _build_mapa_trabalhos_payload(pontos, avaliando_coords=avaliando_coords)
|
| 1107 |
return sanitize_value(
|
| 1108 |
{
|
| 1109 |
"mapa_html": mapa.get_root().render(),
|
| 1110 |
+
"mapa_payload": mapa_payload,
|
| 1111 |
"total_trabalhos": total_trabalhos,
|
| 1112 |
"total_imoveis": total_imoveis,
|
| 1113 |
"modo_exibicao": modo_exibicao_norm,
|
|
|
|
| 1294 |
"trabalho": {
|
| 1295 |
**trabalho,
|
| 1296 |
"mapa_html": _criar_mapa_trabalho(str(trabalho.get("nome") or chave), imoveis),
|
| 1297 |
+
"mapa_payload": _build_mapa_trabalho_payload(str(trabalho.get("nome") or chave), imoveis),
|
| 1298 |
"fonte": _source_payload(resolved, meta),
|
| 1299 |
}
|
| 1300 |
}
|
backend/app/services/visualizacao_service.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from pathlib import Path
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
import folium
|
|
@@ -11,7 +12,12 @@ from folium import plugins
|
|
| 11 |
from joblib import load
|
| 12 |
|
| 13 |
from app.core.visualizacao import app as viz_app
|
| 14 |
-
from app.core.map_layers import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
from app.core.elaboracao.core import (
|
| 16 |
PERCENTUAL_RUIDO_TOL,
|
| 17 |
_migrar_pacote_v1_para_v2,
|
|
@@ -23,6 +29,7 @@ from app.core.elaboracao.core import (
|
|
| 23 |
normalizar_observacao_modelo,
|
| 24 |
)
|
| 25 |
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
|
|
|
| 26 |
from app.models.session import SessionState
|
| 27 |
from app.services import model_repository, pesquisa_service, trabalhos_tecnicos_service
|
| 28 |
from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
|
|
@@ -35,6 +42,8 @@ BASE_COMPARACAO_SEM_BASE = "__none__"
|
|
| 35 |
COORD_LAT_NAMES = {"lat", "latitude", "siat_latitude"}
|
| 36 |
COORD_LON_NAMES = {"lon", "long", "longitude", "siat_longitude"}
|
| 37 |
TRABALHOS_TECNICOS_MODELOS_MODO_PADRAO = pesquisa_service.TRABALHOS_TECNICOS_MODELOS_SELECIONADOS_E_OUTRAS_VERSOES
|
|
|
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
def _to_dataframe(value: Any) -> pd.DataFrame | None:
|
|
@@ -284,6 +293,104 @@ def _criar_mapa_knn_destaque(
|
|
| 284 |
return mapa.get_root().render()
|
| 285 |
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
def _resolver_indice_base(
|
| 288 |
indice_base_raw: str | None,
|
| 289 |
total_avaliacoes: int,
|
|
@@ -314,6 +421,31 @@ def listar_modelos_repositorio() -> dict[str, Any]:
|
|
| 314 |
return sanitize_value(model_repository.list_repository_models())
|
| 315 |
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
|
| 318 |
caminho = model_repository.resolve_model_file(modelo_id)
|
| 319 |
session.uploaded_file_path = str(caminho)
|
|
@@ -341,6 +473,7 @@ def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, An
|
|
| 341 |
session.dados_visualizacao = None
|
| 342 |
session.avaliacoes_visualizacao = []
|
| 343 |
session.visualizacao_cache = {}
|
|
|
|
| 344 |
|
| 345 |
nome_modelo = Path(caminho_arquivo).stem
|
| 346 |
badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)
|
|
@@ -569,9 +702,7 @@ def _aliases_modelo_visualizacao_para_trabalhos_tecnicos(
|
|
| 569 |
modelo_id, caminho, nome_modelo = _resolver_referencias_modelo_visualizacao(session)
|
| 570 |
|
| 571 |
try:
|
| 572 |
-
familias_versoes = pesquisa_service.
|
| 573 |
-
list(pesquisa_service.ensure_modelos_dir().glob("*.dai"))
|
| 574 |
-
)
|
| 575 |
except Exception:
|
| 576 |
familias_versoes = {}
|
| 577 |
|
|
@@ -616,10 +747,15 @@ def _equacoes_do_modelo(pacote: dict[str, Any], info: dict[str, Any]) -> dict[st
|
|
| 616 |
|
| 617 |
|
| 618 |
def _preparar_dados_visualizacao(pacote: dict[str, Any]) -> pd.DataFrame:
|
| 619 |
-
dados = pacote["dados"]["df"].reset_index()
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
dados["__mesa_row_id__"] = np.arange(len(dados), dtype=int)
|
| 624 |
return dados
|
| 625 |
|
|
@@ -825,17 +961,31 @@ def _payload_modelo_mapa(
|
|
| 825 |
session,
|
| 826 |
trabalhos_tecnicos_modelos_modo,
|
| 827 |
)
|
| 828 |
-
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
core["dados"],
|
| 831 |
col_y=info["nome_y"],
|
| 832 |
-
session_id=session.session_id,
|
| 833 |
-
popup_endpoint=popup_endpoint,
|
| 834 |
-
popup_auth_token=popup_auth_token,
|
| 835 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
|
|
|
| 836 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
return {
|
| 838 |
"mapa_html": mapa_html,
|
|
|
|
| 839 |
"mapa_choices": core["mapa_choices"],
|
| 840 |
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 841 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
|
@@ -944,18 +1094,33 @@ def atualizar_mapa(
|
|
| 944 |
session,
|
| 945 |
trabalhos_tecnicos_modelos_modo,
|
| 946 |
)
|
| 947 |
-
|
| 948 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
dados,
|
| 950 |
tamanho_col=tamanho_col,
|
| 951 |
col_y=info["nome_y"],
|
| 952 |
-
session_id=session.session_id,
|
| 953 |
-
popup_endpoint=popup_endpoint,
|
| 954 |
-
popup_auth_token=popup_auth_token,
|
| 955 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
|
|
|
| 956 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
return {
|
| 958 |
"mapa_html": mapa_html,
|
|
|
|
| 959 |
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 960 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 961 |
}
|
|
@@ -1274,13 +1439,22 @@ def detalhes_knn_avaliacao(
|
|
| 1274 |
df_vizinhos.insert(1, "__distancia_knn__", distancias_validas)
|
| 1275 |
tabela_payload = dataframe_to_payload(df_vizinhos, decimals=4, max_rows=None)
|
| 1276 |
|
| 1277 |
-
|
| 1278 |
df_knn,
|
| 1279 |
posicoes_vizinhos,
|
| 1280 |
info["nome_y"],
|
| 1281 |
avaliando_lat=aval_lat,
|
| 1282 |
avaliando_lon=aval_lon,
|
| 1283 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1284 |
avaliando = [{"variavel": str(col), "valor": entradas.get(col)} for col in colunas_x]
|
| 1285 |
if info.get("coluna_area") and info.get("coluna_area") not in colunas_x:
|
| 1286 |
avaliando.append({"variavel": f"{info['coluna_area']} (area)", "valor": valor_area})
|
|
@@ -1293,6 +1467,7 @@ def detalhes_knn_avaliacao(
|
|
| 1293 |
return sanitize_value(
|
| 1294 |
{
|
| 1295 |
"mapa_html": mapa_html,
|
|
|
|
| 1296 |
"avaliando": avaliando,
|
| 1297 |
"vizinhos_tabela": tabela_payload,
|
| 1298 |
"knn": resultado_knn,
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
from pathlib import Path
|
| 4 |
+
from threading import Lock, Thread
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
import folium
|
|
|
|
| 12 |
from joblib import load
|
| 13 |
|
| 14 |
from app.core.visualizacao import app as viz_app
|
| 15 |
+
from app.core.map_layers import (
|
| 16 |
+
add_bairros_layer,
|
| 17 |
+
add_popup_pagination_handlers,
|
| 18 |
+
add_zoom_responsive_circle_markers,
|
| 19 |
+
get_bairros_geojson,
|
| 20 |
+
)
|
| 21 |
from app.core.elaboracao.core import (
|
| 22 |
PERCENTUAL_RUIDO_TOL,
|
| 23 |
_migrar_pacote_v1_para_v2,
|
|
|
|
| 29 |
normalizar_observacao_modelo,
|
| 30 |
)
|
| 31 |
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 32 |
+
from app.core.visualizacao.map_payload import build_leaflet_payload, build_visualizacao_map_payload
|
| 33 |
from app.models.session import SessionState
|
| 34 |
from app.services import model_repository, pesquisa_service, trabalhos_tecnicos_service
|
| 35 |
from app.services.equacao_service import build_equacoes_payload, exportar_planilha_equacao
|
|
|
|
| 42 |
COORD_LAT_NAMES = {"lat", "latitude", "siat_latitude"}
|
| 43 |
COORD_LON_NAMES = {"lon", "long", "longitude", "siat_longitude"}
|
| 44 |
TRABALHOS_TECNICOS_MODELOS_MODO_PADRAO = pesquisa_service.TRABALHOS_TECNICOS_MODELOS_SELECIONADOS_E_OUTRAS_VERSOES
|
| 45 |
+
_MAP_SUPPORT_WARMUP_LOCK = Lock()
|
| 46 |
+
_MAP_SUPPORT_WARMUP_SIGNATURE: str | None = None
|
| 47 |
|
| 48 |
|
| 49 |
def _to_dataframe(value: Any) -> pd.DataFrame | None:
|
|
|
|
| 293 |
return mapa.get_root().render()
|
| 294 |
|
| 295 |
|
| 296 |
+
def _criar_payload_knn_destaque(
|
| 297 |
+
df_base: pd.DataFrame,
|
| 298 |
+
posicoes_knn: list[int],
|
| 299 |
+
coluna_y: str,
|
| 300 |
+
avaliando_lat: float | None = None,
|
| 301 |
+
avaliando_lon: float | None = None,
|
| 302 |
+
) -> dict[str, Any] | None:
|
| 303 |
+
if df_base is None or df_base.empty:
|
| 304 |
+
return None
|
| 305 |
+
|
| 306 |
+
lat_col = _detectar_coluna_coord(df_base, COORD_LAT_NAMES)
|
| 307 |
+
lon_col = _detectar_coluna_coord(df_base, COORD_LON_NAMES)
|
| 308 |
+
if not lat_col or not lon_col:
|
| 309 |
+
return None
|
| 310 |
+
|
| 311 |
+
lat_serie = _primeira_serie_por_nome(df_base, lat_col)
|
| 312 |
+
lon_serie = _primeira_serie_por_nome(df_base, lon_col)
|
| 313 |
+
if lat_serie is None or lon_serie is None:
|
| 314 |
+
return None
|
| 315 |
+
|
| 316 |
+
dados = df_base.copy()
|
| 317 |
+
dados["__pos_base__"] = np.arange(len(dados), dtype=int)
|
| 318 |
+
dados["__indice_base__"] = [str(v) for v in dados.index]
|
| 319 |
+
dados["__lat__"] = pd.to_numeric(lat_serie, errors="coerce")
|
| 320 |
+
dados["__lon__"] = pd.to_numeric(lon_serie, errors="coerce")
|
| 321 |
+
dados = dados[
|
| 322 |
+
np.isfinite(dados["__lat__"])
|
| 323 |
+
& np.isfinite(dados["__lon__"])
|
| 324 |
+
& (np.abs(dados["__lat__"]) <= 90.0)
|
| 325 |
+
& (np.abs(dados["__lon__"]) <= 180.0)
|
| 326 |
+
].copy()
|
| 327 |
+
if dados.empty:
|
| 328 |
+
return None
|
| 329 |
+
|
| 330 |
+
aval_lat, aval_lon = _normalizar_coordenadas_avaliando(avaliando_lat, avaliando_lon)
|
| 331 |
+
posicoes_set = {int(v) for v in (posicoes_knn or [])}
|
| 332 |
+
points_mercado: list[dict[str, Any]] = []
|
| 333 |
+
points_knn: list[dict[str, Any]] = []
|
| 334 |
+
bounds: list[list[float]] = []
|
| 335 |
+
|
| 336 |
+
for _, row in dados.iterrows():
|
| 337 |
+
lat = float(row["__lat__"])
|
| 338 |
+
lon = float(row["__lon__"])
|
| 339 |
+
pos = int(row["__pos_base__"])
|
| 340 |
+
selecionado = pos in posicoes_set
|
| 341 |
+
col_y_val = row.get(coluna_y)
|
| 342 |
+
valor_tooltip = _formatar_tooltip_valor(coluna_y, col_y_val)
|
| 343 |
+
point_payload = {
|
| 344 |
+
"lat": lat,
|
| 345 |
+
"lon": lon,
|
| 346 |
+
"color": "#d7263d" if selecionado else "#4f6d8a",
|
| 347 |
+
"base_radius": 8.0 if selecionado else 5.0,
|
| 348 |
+
"stroke_color": "#ffffff",
|
| 349 |
+
"stroke_weight": 0.9,
|
| 350 |
+
"fill_opacity": 0.92 if selecionado else 0.52,
|
| 351 |
+
"tooltip_html": (
|
| 352 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.5;'>"
|
| 353 |
+
f"<b>Índice {escape(str(row['__indice_base__']))}</b>"
|
| 354 |
+
f"<br><span style='color:#555;'>{escape(str(coluna_y))}:</span> <b>{escape(valor_tooltip)}</b>"
|
| 355 |
+
"</div>"
|
| 356 |
+
),
|
| 357 |
+
}
|
| 358 |
+
(points_knn if selecionado else points_mercado).append(point_payload)
|
| 359 |
+
bounds.append([lat, lon])
|
| 360 |
+
|
| 361 |
+
overlay_layers: list[dict[str, Any]] = [
|
| 362 |
+
{"id": "mercado", "label": "Mercado", "show": True, "points": points_mercado},
|
| 363 |
+
{"id": "knn", "label": "Selecionados KNN", "show": True, "points": points_knn},
|
| 364 |
+
]
|
| 365 |
+
|
| 366 |
+
if aval_lat is not None and aval_lon is not None:
|
| 367 |
+
overlay_layers.append(
|
| 368 |
+
{
|
| 369 |
+
"id": "avaliando",
|
| 370 |
+
"label": "Avaliando",
|
| 371 |
+
"show": True,
|
| 372 |
+
"markers": [
|
| 373 |
+
{
|
| 374 |
+
"lat": float(aval_lat),
|
| 375 |
+
"lon": float(aval_lon),
|
| 376 |
+
"tooltip_html": "Avaliando",
|
| 377 |
+
"marker_html": (
|
| 378 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 379 |
+
"width:18px;height:18px;border-radius:999px;background:#dc3545;"
|
| 380 |
+
"border:2px solid rgba(255,255,255,0.95);box-shadow:0 1px 4px rgba(0,0,0,0.2);'></div>"
|
| 381 |
+
),
|
| 382 |
+
"icon_size": [18, 18],
|
| 383 |
+
"icon_anchor": [9, 9],
|
| 384 |
+
"class_name": "mesa-avaliando-marker",
|
| 385 |
+
}
|
| 386 |
+
],
|
| 387 |
+
}
|
| 388 |
+
)
|
| 389 |
+
bounds.append([float(aval_lat), float(aval_lon)])
|
| 390 |
+
|
| 391 |
+
return build_leaflet_payload(bounds=bounds, overlay_layers=overlay_layers, show_bairros=True)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
def _resolver_indice_base(
|
| 395 |
indice_base_raw: str | None,
|
| 396 |
total_avaliacoes: int,
|
|
|
|
| 421 |
return sanitize_value(model_repository.list_repository_models())
|
| 422 |
|
| 423 |
|
| 424 |
+
def _warm_visualizacao_support_caches_async() -> None:
|
| 425 |
+
global _MAP_SUPPORT_WARMUP_SIGNATURE
|
| 426 |
+
try:
|
| 427 |
+
signature = model_repository.resolve_model_repository().signature
|
| 428 |
+
except Exception:
|
| 429 |
+
signature = "__unknown__"
|
| 430 |
+
|
| 431 |
+
with _MAP_SUPPORT_WARMUP_LOCK:
|
| 432 |
+
if _MAP_SUPPORT_WARMUP_SIGNATURE == signature:
|
| 433 |
+
return
|
| 434 |
+
_MAP_SUPPORT_WARMUP_SIGNATURE = signature
|
| 435 |
+
|
| 436 |
+
def _worker() -> None:
|
| 437 |
+
try:
|
| 438 |
+
get_bairros_geojson()
|
| 439 |
+
except Exception:
|
| 440 |
+
pass
|
| 441 |
+
try:
|
| 442 |
+
pesquisa_service.obter_familias_versoes_modelos_cache()
|
| 443 |
+
except Exception:
|
| 444 |
+
pass
|
| 445 |
+
|
| 446 |
+
Thread(target=_worker, name="mesa-visualizacao-map-warmup", daemon=True).start()
|
| 447 |
+
|
| 448 |
+
|
| 449 |
def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
|
| 450 |
caminho = model_repository.resolve_model_file(modelo_id)
|
| 451 |
session.uploaded_file_path = str(caminho)
|
|
|
|
| 473 |
session.dados_visualizacao = None
|
| 474 |
session.avaliacoes_visualizacao = []
|
| 475 |
session.visualizacao_cache = {}
|
| 476 |
+
_warm_visualizacao_support_caches_async()
|
| 477 |
|
| 478 |
nome_modelo = Path(caminho_arquivo).stem
|
| 479 |
badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)
|
|
|
|
| 702 |
modelo_id, caminho, nome_modelo = _resolver_referencias_modelo_visualizacao(session)
|
| 703 |
|
| 704 |
try:
|
| 705 |
+
familias_versoes = pesquisa_service.obter_familias_versoes_modelos_cache()
|
|
|
|
|
|
|
| 706 |
except Exception:
|
| 707 |
familias_versoes = {}
|
| 708 |
|
|
|
|
| 747 |
|
| 748 |
|
| 749 |
def _preparar_dados_visualizacao(pacote: dict[str, Any]) -> pd.DataFrame:
|
| 750 |
+
dados = pacote["dados"]["df"].reset_index().copy()
|
| 751 |
+
colunas_numericas = [
|
| 752 |
+
str(col)
|
| 753 |
+
for col in dados.columns
|
| 754 |
+
if pd.api.types.is_numeric_dtype(dados[col])
|
| 755 |
+
and str(col).lower() not in ["lat", "latitude", "lon", "longitude", "long", "siat_latitude", "siat_longitude"]
|
| 756 |
+
]
|
| 757 |
+
if colunas_numericas:
|
| 758 |
+
dados.loc[:, colunas_numericas] = dados.loc[:, colunas_numericas].round(2)
|
| 759 |
dados["__mesa_row_id__"] = np.arange(len(dados), dtype=int)
|
| 760 |
return dados
|
| 761 |
|
|
|
|
| 961 |
session,
|
| 962 |
trabalhos_tecnicos_modelos_modo,
|
| 963 |
)
|
| 964 |
+
bairros_geojson_url = (
|
| 965 |
+
f"{api_base_url.rstrip('/')}/api/visualizacao/map/bairros.geojson"
|
| 966 |
+
if api_base_url
|
| 967 |
+
else "/api/visualizacao/map/bairros.geojson"
|
| 968 |
+
)
|
| 969 |
+
mapa_payload = build_visualizacao_map_payload(
|
| 970 |
core["dados"],
|
| 971 |
col_y=info["nome_y"],
|
|
|
|
|
|
|
|
|
|
| 972 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 973 |
+
bairros_geojson_url=bairros_geojson_url,
|
| 974 |
)
|
| 975 |
+
mapa_html = ""
|
| 976 |
+
if mapa_payload is None:
|
| 977 |
+
popup_endpoint = f"{api_base_url.rstrip('/')}/api/visualizacao/map/popup" if api_base_url else "/api/visualizacao/map/popup"
|
| 978 |
+
mapa_html = viz_app.criar_mapa(
|
| 979 |
+
core["dados"],
|
| 980 |
+
col_y=info["nome_y"],
|
| 981 |
+
session_id=session.session_id,
|
| 982 |
+
popup_endpoint=popup_endpoint,
|
| 983 |
+
popup_auth_token=popup_auth_token,
|
| 984 |
+
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 985 |
+
)
|
| 986 |
return {
|
| 987 |
"mapa_html": mapa_html,
|
| 988 |
+
"mapa_payload": mapa_payload,
|
| 989 |
"mapa_choices": core["mapa_choices"],
|
| 990 |
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 991 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
|
|
|
| 1094 |
session,
|
| 1095 |
trabalhos_tecnicos_modelos_modo,
|
| 1096 |
)
|
| 1097 |
+
bairros_geojson_url = (
|
| 1098 |
+
f"{api_base_url.rstrip('/')}/api/visualizacao/map/bairros.geojson"
|
| 1099 |
+
if api_base_url
|
| 1100 |
+
else "/api/visualizacao/map/bairros.geojson"
|
| 1101 |
+
)
|
| 1102 |
+
mapa_payload = build_visualizacao_map_payload(
|
| 1103 |
dados,
|
| 1104 |
tamanho_col=tamanho_col,
|
| 1105 |
col_y=info["nome_y"],
|
|
|
|
|
|
|
|
|
|
| 1106 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 1107 |
+
bairros_geojson_url=bairros_geojson_url,
|
| 1108 |
)
|
| 1109 |
+
mapa_html = ""
|
| 1110 |
+
if mapa_payload is None:
|
| 1111 |
+
popup_endpoint = f"{api_base_url.rstrip('/')}/api/visualizacao/map/popup" if api_base_url else "/api/visualizacao/map/popup"
|
| 1112 |
+
mapa_html = viz_app.criar_mapa(
|
| 1113 |
+
dados,
|
| 1114 |
+
tamanho_col=tamanho_col,
|
| 1115 |
+
col_y=info["nome_y"],
|
| 1116 |
+
session_id=session.session_id,
|
| 1117 |
+
popup_endpoint=popup_endpoint,
|
| 1118 |
+
popup_auth_token=popup_auth_token,
|
| 1119 |
+
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 1120 |
+
)
|
| 1121 |
return {
|
| 1122 |
"mapa_html": mapa_html,
|
| 1123 |
+
"mapa_payload": mapa_payload,
|
| 1124 |
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 1125 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 1126 |
}
|
|
|
|
| 1439 |
df_vizinhos.insert(1, "__distancia_knn__", distancias_validas)
|
| 1440 |
tabela_payload = dataframe_to_payload(df_vizinhos, decimals=4, max_rows=None)
|
| 1441 |
|
| 1442 |
+
mapa_payload = _criar_payload_knn_destaque(
|
| 1443 |
df_knn,
|
| 1444 |
posicoes_vizinhos,
|
| 1445 |
info["nome_y"],
|
| 1446 |
avaliando_lat=aval_lat,
|
| 1447 |
avaliando_lon=aval_lon,
|
| 1448 |
)
|
| 1449 |
+
mapa_html = ""
|
| 1450 |
+
if mapa_payload is None:
|
| 1451 |
+
mapa_html = _criar_mapa_knn_destaque(
|
| 1452 |
+
df_knn,
|
| 1453 |
+
posicoes_vizinhos,
|
| 1454 |
+
info["nome_y"],
|
| 1455 |
+
avaliando_lat=aval_lat,
|
| 1456 |
+
avaliando_lon=aval_lon,
|
| 1457 |
+
)
|
| 1458 |
avaliando = [{"variavel": str(col), "valor": entradas.get(col)} for col in colunas_x]
|
| 1459 |
if info.get("coluna_area") and info.get("coluna_area") not in colunas_x:
|
| 1460 |
avaliando.append({"variavel": f"{info['coluna_area']} (area)", "valor": valor_area})
|
|
|
|
| 1467 |
return sanitize_value(
|
| 1468 |
{
|
| 1469 |
"mapa_html": mapa_html,
|
| 1470 |
+
"mapa_payload": mapa_payload,
|
| 1471 |
"avaliando": avaliando,
|
| 1472 |
"vizinhos_tabela": tabela_payload,
|
| 1473 |
"knn": resultado_knn,
|
frontend/package-lock.json
CHANGED
|
@@ -8,6 +8,10 @@
|
|
| 8 |
"name": "mesa-frame-frontend",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"plotly.js-dist-min": "^2.35.2",
|
| 12 |
"react": "^18.3.1",
|
| 13 |
"react-dom": "^18.3.1",
|
|
@@ -1367,6 +1371,22 @@
|
|
| 1367 |
"url": "https://opencollective.com/turf"
|
| 1368 |
}
|
| 1369 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
"node_modules/@turf/helpers": {
|
| 1371 |
"version": "7.3.4",
|
| 1372 |
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz",
|
|
@@ -1381,6 +1401,47 @@
|
|
| 1381 |
"url": "https://opencollective.com/turf"
|
| 1382 |
}
|
| 1383 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1384 |
"node_modules/@turf/meta": {
|
| 1385 |
"version": "7.3.4",
|
| 1386 |
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.4.tgz",
|
|
@@ -3102,6 +3163,71 @@
|
|
| 3102 |
"node": ">=0.10.0"
|
| 3103 |
}
|
| 3104 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3105 |
"node_modules/lodash.merge": {
|
| 3106 |
"version": "4.6.2",
|
| 3107 |
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
|
|
|
| 8 |
"name": "mesa-frame-frontend",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"leaflet": "^1.9.4",
|
| 12 |
+
"leaflet-measure": "^3.1.0",
|
| 13 |
+
"leaflet.fullscreen": "^5.3.1",
|
| 14 |
+
"leaflet.heat": "^0.2.0",
|
| 15 |
"plotly.js-dist-min": "^2.35.2",
|
| 16 |
"react": "^18.3.1",
|
| 17 |
"react-dom": "^18.3.1",
|
|
|
|
| 1371 |
"url": "https://opencollective.com/turf"
|
| 1372 |
}
|
| 1373 |
},
|
| 1374 |
+
"node_modules/@turf/distance": {
|
| 1375 |
+
"version": "5.1.5",
|
| 1376 |
+
"resolved": "https://registry.npmjs.org/@turf/distance/-/distance-5.1.5.tgz",
|
| 1377 |
+
"integrity": "sha512-sYCAgYZ2MjNKMtx17EijHlK9qHwpA0MuuQWbR4P30LTCl52UlG/reBfV899wKyF3HuDL9ux78IbILwOfeQ4zgA==",
|
| 1378 |
+
"license": "MIT",
|
| 1379 |
+
"dependencies": {
|
| 1380 |
+
"@turf/helpers": "^5.1.5",
|
| 1381 |
+
"@turf/invariant": "^5.1.5"
|
| 1382 |
+
}
|
| 1383 |
+
},
|
| 1384 |
+
"node_modules/@turf/distance/node_modules/@turf/helpers": {
|
| 1385 |
+
"version": "5.1.5",
|
| 1386 |
+
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz",
|
| 1387 |
+
"integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==",
|
| 1388 |
+
"license": "MIT"
|
| 1389 |
+
},
|
| 1390 |
"node_modules/@turf/helpers": {
|
| 1391 |
"version": "7.3.4",
|
| 1392 |
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz",
|
|
|
|
| 1401 |
"url": "https://opencollective.com/turf"
|
| 1402 |
}
|
| 1403 |
},
|
| 1404 |
+
"node_modules/@turf/invariant": {
|
| 1405 |
+
"version": "5.2.0",
|
| 1406 |
+
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz",
|
| 1407 |
+
"integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==",
|
| 1408 |
+
"license": "MIT",
|
| 1409 |
+
"dependencies": {
|
| 1410 |
+
"@turf/helpers": "^5.1.5"
|
| 1411 |
+
}
|
| 1412 |
+
},
|
| 1413 |
+
"node_modules/@turf/invariant/node_modules/@turf/helpers": {
|
| 1414 |
+
"version": "5.1.5",
|
| 1415 |
+
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz",
|
| 1416 |
+
"integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==",
|
| 1417 |
+
"license": "MIT"
|
| 1418 |
+
},
|
| 1419 |
+
"node_modules/@turf/length": {
|
| 1420 |
+
"version": "5.1.5",
|
| 1421 |
+
"resolved": "https://registry.npmjs.org/@turf/length/-/length-5.1.5.tgz",
|
| 1422 |
+
"integrity": "sha512-0ryx68h512wCoNfwyksLdabxEfwkGNTPg61/QiY+QfGFUOUNhHbP+QimViFpwF5hyX7qmroaSHVclLUqyLGRbg==",
|
| 1423 |
+
"license": "MIT",
|
| 1424 |
+
"dependencies": {
|
| 1425 |
+
"@turf/distance": "^5.1.5",
|
| 1426 |
+
"@turf/helpers": "^5.1.5",
|
| 1427 |
+
"@turf/meta": "^5.1.5"
|
| 1428 |
+
}
|
| 1429 |
+
},
|
| 1430 |
+
"node_modules/@turf/length/node_modules/@turf/helpers": {
|
| 1431 |
+
"version": "5.1.5",
|
| 1432 |
+
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz",
|
| 1433 |
+
"integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==",
|
| 1434 |
+
"license": "MIT"
|
| 1435 |
+
},
|
| 1436 |
+
"node_modules/@turf/length/node_modules/@turf/meta": {
|
| 1437 |
+
"version": "5.2.0",
|
| 1438 |
+
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz",
|
| 1439 |
+
"integrity": "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==",
|
| 1440 |
+
"license": "MIT",
|
| 1441 |
+
"dependencies": {
|
| 1442 |
+
"@turf/helpers": "^5.1.5"
|
| 1443 |
+
}
|
| 1444 |
+
},
|
| 1445 |
"node_modules/@turf/meta": {
|
| 1446 |
"version": "7.3.4",
|
| 1447 |
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.4.tgz",
|
|
|
|
| 3163 |
"node": ">=0.10.0"
|
| 3164 |
}
|
| 3165 |
},
|
| 3166 |
+
"node_modules/leaflet": {
|
| 3167 |
+
"version": "1.9.4",
|
| 3168 |
+
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
| 3169 |
+
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
| 3170 |
+
"license": "BSD-2-Clause"
|
| 3171 |
+
},
|
| 3172 |
+
"node_modules/leaflet-measure": {
|
| 3173 |
+
"version": "3.1.0",
|
| 3174 |
+
"resolved": "https://registry.npmjs.org/leaflet-measure/-/leaflet-measure-3.1.0.tgz",
|
| 3175 |
+
"integrity": "sha512-ln5c9UNaWDEd24tIzDt9hwnpb8OaCPBfSWNBg2H8rb9SA3cbXW9+NqohA6/8TdsNDGJZr36woXMrqRq07Pcl3w==",
|
| 3176 |
+
"license": "MIT",
|
| 3177 |
+
"dependencies": {
|
| 3178 |
+
"@turf/area": "^5.1.5",
|
| 3179 |
+
"@turf/length": "^5.1.5",
|
| 3180 |
+
"lodash": "^4.17.5"
|
| 3181 |
+
},
|
| 3182 |
+
"peerDependencies": {
|
| 3183 |
+
"leaflet": "^1.0.0"
|
| 3184 |
+
}
|
| 3185 |
+
},
|
| 3186 |
+
"node_modules/leaflet-measure/node_modules/@turf/area": {
|
| 3187 |
+
"version": "5.1.5",
|
| 3188 |
+
"resolved": "https://registry.npmjs.org/@turf/area/-/area-5.1.5.tgz",
|
| 3189 |
+
"integrity": "sha512-lz16gqtvoz+j1jD9y3zj0Z5JnGNd3YfS0h+DQY1EcZymvi75Frm9i5YbEyth0RfxYZeOVufY7YIS3LXbJlI57g==",
|
| 3190 |
+
"license": "MIT",
|
| 3191 |
+
"dependencies": {
|
| 3192 |
+
"@turf/helpers": "^5.1.5",
|
| 3193 |
+
"@turf/meta": "^5.1.5"
|
| 3194 |
+
}
|
| 3195 |
+
},
|
| 3196 |
+
"node_modules/leaflet-measure/node_modules/@turf/helpers": {
|
| 3197 |
+
"version": "5.1.5",
|
| 3198 |
+
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz",
|
| 3199 |
+
"integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==",
|
| 3200 |
+
"license": "MIT"
|
| 3201 |
+
},
|
| 3202 |
+
"node_modules/leaflet-measure/node_modules/@turf/meta": {
|
| 3203 |
+
"version": "5.2.0",
|
| 3204 |
+
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz",
|
| 3205 |
+
"integrity": "sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==",
|
| 3206 |
+
"license": "MIT",
|
| 3207 |
+
"dependencies": {
|
| 3208 |
+
"@turf/helpers": "^5.1.5"
|
| 3209 |
+
}
|
| 3210 |
+
},
|
| 3211 |
+
"node_modules/leaflet.fullscreen": {
|
| 3212 |
+
"version": "5.3.1",
|
| 3213 |
+
"resolved": "https://registry.npmjs.org/leaflet.fullscreen/-/leaflet.fullscreen-5.3.1.tgz",
|
| 3214 |
+
"integrity": "sha512-2IO5WJ5xpQWyn2ZLICwwpyiWJ2KdZMuxAUCsrK7b4dj770GZ/zwPp4uVQNKOxqnz9xRWEN1d2VNpLaX4ptSdnA==",
|
| 3215 |
+
"license": "MIT",
|
| 3216 |
+
"peerDependencies": {
|
| 3217 |
+
"leaflet": "^1.7.0 || >=2.0.0-alpha.1"
|
| 3218 |
+
}
|
| 3219 |
+
},
|
| 3220 |
+
"node_modules/leaflet.heat": {
|
| 3221 |
+
"version": "0.2.0",
|
| 3222 |
+
"resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz",
|
| 3223 |
+
"integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ=="
|
| 3224 |
+
},
|
| 3225 |
+
"node_modules/lodash": {
|
| 3226 |
+
"version": "4.18.1",
|
| 3227 |
+
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
| 3228 |
+
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
| 3229 |
+
"license": "MIT"
|
| 3230 |
+
},
|
| 3231 |
"node_modules/lodash.merge": {
|
| 3232 |
"version": "4.6.2",
|
| 3233 |
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
frontend/package.json
CHANGED
|
@@ -9,6 +9,10 @@
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"plotly.js-dist-min": "^2.35.2",
|
| 13 |
"react": "^18.3.1",
|
| 14 |
"react-dom": "^18.3.1",
|
|
|
|
| 9 |
"preview": "vite preview"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
+
"leaflet": "^1.9.4",
|
| 13 |
+
"leaflet-measure": "^3.1.0",
|
| 14 |
+
"leaflet.fullscreen": "^5.3.1",
|
| 15 |
+
"leaflet.heat": "^0.2.0",
|
| 16 |
"plotly.js-dist-min": "^2.35.2",
|
| 17 |
"react": "^18.3.1",
|
| 18 |
"react-dom": "^18.3.1",
|
frontend/src/api.js
CHANGED
|
@@ -29,6 +29,10 @@ export function getAuthToken() {
|
|
| 29 |
return AUTH_TOKEN
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
function authHeaders(extraHeaders = {}) {
|
| 33 |
const headers = { ...extraHeaders }
|
| 34 |
if (AUTH_TOKEN) headers['X-Auth-Token'] = AUTH_TOKEN
|
|
|
|
| 29 |
return AUTH_TOKEN
|
| 30 |
}
|
| 31 |
|
| 32 |
+
export function apiUrl(path) {
|
| 33 |
+
return `${API_BASE}${String(path || '')}`
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
function authHeaders(extraHeaders = {}) {
|
| 37 |
const headers = { ...extraHeaders }
|
| 38 |
if (AUTH_TOKEN) headers['X-Auth-Token'] = AUTH_TOKEN
|
frontend/src/components/AvaliacaoTab.jsx
CHANGED
|
@@ -539,6 +539,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 539 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
| 540 |
const [knnDetalheCardTitulo, setKnnDetalheCardTitulo] = useState('')
|
| 541 |
const [knnDetalheMapaHtml, setKnnDetalheMapaHtml] = useState('')
|
|
|
|
| 542 |
const [knnDetalheAvaliando, setKnnDetalheAvaliando] = useState([])
|
| 543 |
const [knnDetalheTabela, setKnnDetalheTabela] = useState(null)
|
| 544 |
const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
|
|
@@ -1078,6 +1079,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1078 |
setKnnDetalheErro('')
|
| 1079 |
setKnnDetalheCardTitulo(`Aval. ${Number(indice) + 1} — ${String(card?.modelo || 'Modelo')}`)
|
| 1080 |
setKnnDetalheMapaHtml('')
|
|
|
|
| 1081 |
setKnnDetalheAvaliando([])
|
| 1082 |
setKnnDetalheTabela(null)
|
| 1083 |
setKnnDetalheInfo(null)
|
|
@@ -1091,6 +1093,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1091 |
}),
|
| 1092 |
)
|
| 1093 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
|
|
|
| 1094 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
| 1095 |
setKnnDetalheTabela(resp?.vizinhos_tabela || null)
|
| 1096 |
setKnnDetalheInfo(resp?.knn || null)
|
|
@@ -1853,7 +1856,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1853 |
</div>
|
| 1854 |
|
| 1855 |
<div className="avaliacao-knn-map-wrap">
|
| 1856 |
-
<MapFrame html={knnDetalheMapaHtml} />
|
| 1857 |
</div>
|
| 1858 |
|
| 1859 |
<div className="subpanel avaliacao-knn-detalhes-box">
|
|
|
|
| 539 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
| 540 |
const [knnDetalheCardTitulo, setKnnDetalheCardTitulo] = useState('')
|
| 541 |
const [knnDetalheMapaHtml, setKnnDetalheMapaHtml] = useState('')
|
| 542 |
+
const [knnDetalheMapaPayload, setKnnDetalheMapaPayload] = useState(null)
|
| 543 |
const [knnDetalheAvaliando, setKnnDetalheAvaliando] = useState([])
|
| 544 |
const [knnDetalheTabela, setKnnDetalheTabela] = useState(null)
|
| 545 |
const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
|
|
|
|
| 1079 |
setKnnDetalheErro('')
|
| 1080 |
setKnnDetalheCardTitulo(`Aval. ${Number(indice) + 1} — ${String(card?.modelo || 'Modelo')}`)
|
| 1081 |
setKnnDetalheMapaHtml('')
|
| 1082 |
+
setKnnDetalheMapaPayload(null)
|
| 1083 |
setKnnDetalheAvaliando([])
|
| 1084 |
setKnnDetalheTabela(null)
|
| 1085 |
setKnnDetalheInfo(null)
|
|
|
|
| 1093 |
}),
|
| 1094 |
)
|
| 1095 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
| 1096 |
+
setKnnDetalheMapaPayload(resp?.mapa_payload || null)
|
| 1097 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
| 1098 |
setKnnDetalheTabela(resp?.vizinhos_tabela || null)
|
| 1099 |
setKnnDetalheInfo(resp?.knn || null)
|
|
|
|
| 1856 |
</div>
|
| 1857 |
|
| 1858 |
<div className="avaliacao-knn-map-wrap">
|
| 1859 |
+
<MapFrame html={knnDetalheMapaHtml} payload={knnDetalheMapaPayload} sessionId={sessionId} />
|
| 1860 |
</div>
|
| 1861 |
|
| 1862 |
<div className="subpanel avaliacao-knn-detalhes-box">
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -883,10 +883,12 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 883 |
|
| 884 |
const [dados, setDados] = useState(null)
|
| 885 |
const [mapaHtml, setMapaHtml] = useState('')
|
|
|
|
| 886 |
const [mapaVariavel, setMapaVariavel] = useState(MAPA_VARIAVEL_PADRAO)
|
| 887 |
const [mapaModo, setMapaModo] = useState(MAPA_MODO_PONTOS)
|
| 888 |
const [mapaGerado, setMapaGerado] = useState(false)
|
| 889 |
const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
|
|
|
|
| 890 |
const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
|
| 891 |
const [mapaResiduosExtremoAbs, setMapaResiduosExtremoAbs] = useState(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 892 |
const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
|
|
@@ -983,6 +985,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 983 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
| 984 |
const [knnDetalheCardTitulo, setKnnDetalheCardTitulo] = useState('')
|
| 985 |
const [knnDetalheMapaHtml, setKnnDetalheMapaHtml] = useState('')
|
|
|
|
| 986 |
const [knnDetalheAvaliando, setKnnDetalheAvaliando] = useState([])
|
| 987 |
const [knnDetalheTabela, setKnnDetalheTabela] = useState(null)
|
| 988 |
const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
|
|
@@ -1014,6 +1017,22 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 1014 |
? buildElaboracaoModeloLink(repoModeloSelecionado)
|
| 1015 |
: ''
|
| 1016 |
const [sideNavDynamicStyle, setSideNavDynamicStyle] = useState({})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
|
| 1018 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
| 1019 |
const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
|
|
@@ -2106,7 +2125,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2106 |
}
|
| 2107 |
setDataMercadoError('')
|
| 2108 |
if (resp.dados) setDados(resp.dados)
|
| 2109 |
-
if (resp.mapa_html)
|
| 2110 |
if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
|
| 2111 |
if (Array.isArray(resp.colunas_data_mercado)) {
|
| 2112 |
setColunasDataMercado(resp.colunas_data_mercado.map((item) => String(item)))
|
|
@@ -2290,8 +2309,8 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2290 |
syncPeriodoDataMercadoFromContext(resp.contexto)
|
| 2291 |
}
|
| 2292 |
|
| 2293 |
-
if (resp.mapa_html) {
|
| 2294 |
-
|
| 2295 |
}
|
| 2296 |
}
|
| 2297 |
|
|
@@ -2313,7 +2332,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2313 |
setSecao13InterativoEixoYColuna('')
|
| 2314 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2315 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2316 |
-
|
| 2317 |
setMapaResiduosGerado(false)
|
| 2318 |
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
| 2319 |
const transformacoesXAplicadas = resp.transformacoes_x || transformacoesX
|
|
@@ -2466,11 +2485,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2466 |
setImportacaoErro('')
|
| 2467 |
await withBusy(async () => {
|
| 2468 |
setMapaGerado(false)
|
| 2469 |
-
|
| 2470 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2471 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2472 |
setMapaResiduosGerado(false)
|
| 2473 |
-
|
| 2474 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2475 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2476 |
setGeoAuto200(true)
|
|
@@ -2505,11 +2524,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2505 |
setArquivoCarregadoInfo(null)
|
| 2506 |
await withBusy(async () => {
|
| 2507 |
setMapaGerado(false)
|
| 2508 |
-
|
| 2509 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2510 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2511 |
setMapaResiduosGerado(false)
|
| 2512 |
-
|
| 2513 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2514 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2515 |
setGeoAuto200(true)
|
|
@@ -2583,11 +2602,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2583 |
setImportacaoErro('')
|
| 2584 |
await withBusy(async () => {
|
| 2585 |
setMapaGerado(false)
|
| 2586 |
-
|
| 2587 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2588 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2589 |
setMapaResiduosGerado(false)
|
| 2590 |
-
|
| 2591 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2592 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2593 |
setGeoAuto200(true)
|
|
@@ -2733,7 +2752,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2733 |
setGeoProcessError('')
|
| 2734 |
try {
|
| 2735 |
const resp = await api.mapCoords(sessionId, manualLat, manualLon)
|
| 2736 |
-
|
| 2737 |
setDados(resp.dados)
|
| 2738 |
setCoordsInfo(resp.coords)
|
| 2739 |
setGeoStatusHtml('')
|
|
@@ -2757,7 +2776,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2757 |
setGeoStatusHtml(resp.status_html || '')
|
| 2758 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2759 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2760 |
-
|
| 2761 |
setDados(resp.dados || null)
|
| 2762 |
setCoordsInfo(resp.coords || null)
|
| 2763 |
setCoordsMode('geocodificar')
|
|
@@ -2779,7 +2798,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2779 |
setGeoStatusHtml(resp.status_html || '')
|
| 2780 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2781 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2782 |
-
|
| 2783 |
setDados(resp.dados || null)
|
| 2784 |
setCoordsInfo(resp.coords || null)
|
| 2785 |
setCoordsMode('geocodificar')
|
|
@@ -2797,7 +2816,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2797 |
setGeoStatusHtml(resp.status_html || '')
|
| 2798 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2799 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2800 |
-
|
| 2801 |
setDados(resp.dados || null)
|
| 2802 |
setCoordsInfo(resp.coords || null)
|
| 2803 |
setGeoCdlog(escolherColunaCdlogPadrao(resp.coords || null))
|
|
@@ -2817,7 +2836,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2817 |
setGeoStatusHtml(resp.status_html || '')
|
| 2818 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2819 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2820 |
-
|
| 2821 |
setDados(resp.dados || null)
|
| 2822 |
setCoordsInfo(resp.coords || null)
|
| 2823 |
setManualLat(resp.coords?.colunas_disponiveis?.[0] || '')
|
|
@@ -3201,7 +3220,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3201 |
await withBusy(async () => {
|
| 3202 |
const resp = await api.clearOutlierHistory(sessionId)
|
| 3203 |
setDados(resp.dados)
|
| 3204 |
-
|
| 3205 |
setResumoOutliers(resp.resumo_outliers || 'Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
|
| 3206 |
setOutliersAnteriores([])
|
| 3207 |
setIteracao(1)
|
|
@@ -3287,6 +3306,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3287 |
setKnnDetalheErro('')
|
| 3288 |
setKnnDetalheCardTitulo(`Aval. ${idx + 1}`)
|
| 3289 |
setKnnDetalheMapaHtml('')
|
|
|
|
| 3290 |
setKnnDetalheAvaliando([])
|
| 3291 |
setKnnDetalheTabela(null)
|
| 3292 |
setKnnDetalheInfo(null)
|
|
@@ -3294,6 +3314,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3294 |
try {
|
| 3295 |
const resp = await api.evaluationKnnDetailsElab(sessionId, construirPayloadKnnAvaliacao(avaliacao))
|
| 3296 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
|
|
|
| 3297 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
| 3298 |
setKnnDetalheTabela(resp?.vizinhos_tabela || null)
|
| 3299 |
setKnnDetalheInfo(resp?.knn || null)
|
|
@@ -3474,7 +3495,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3474 |
if (!sessionId || !mapaGerado) return
|
| 3475 |
await withBusy(async () => {
|
| 3476 |
const resp = await api.updateElaboracaoMap(sessionId, value, nextModo)
|
| 3477 |
-
|
| 3478 |
})
|
| 3479 |
}
|
| 3480 |
|
|
@@ -3483,7 +3504,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3483 |
if (!sessionId || !mapaGerado) return
|
| 3484 |
await withBusy(async () => {
|
| 3485 |
const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, value)
|
| 3486 |
-
|
| 3487 |
})
|
| 3488 |
}
|
| 3489 |
|
|
@@ -3495,7 +3516,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3495 |
}
|
| 3496 |
await withBusy(async () => {
|
| 3497 |
const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, modoAtual)
|
| 3498 |
-
|
| 3499 |
setMapaGerado(true)
|
| 3500 |
})
|
| 3501 |
}
|
|
@@ -3506,7 +3527,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3506 |
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 3507 |
await withBusy(async () => {
|
| 3508 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value, extremoAbs)
|
| 3509 |
-
|
| 3510 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3511 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3512 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
@@ -3523,7 +3544,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3523 |
if (!sessionId || !mapaResiduosGerado) return
|
| 3524 |
await withBusy(async () => {
|
| 3525 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 3526 |
-
|
| 3527 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3528 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3529 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
@@ -3538,7 +3559,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3538 |
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 3539 |
await withBusy(async () => {
|
| 3540 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 3541 |
-
|
| 3542 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3543 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3544 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
@@ -4525,7 +4546,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4525 |
Fazer download
|
| 4526 |
</button>
|
| 4527 |
</div>
|
| 4528 |
-
<MapFrame html={mapaHtml} />
|
| 4529 |
</details>
|
| 4530 |
)}
|
| 4531 |
</div>
|
|
@@ -5870,7 +5891,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 5870 |
Fazer download
|
| 5871 |
</button>
|
| 5872 |
</div>
|
| 5873 |
-
<MapFrame html={mapaResiduosHtml} />
|
| 5874 |
</>
|
| 5875 |
)}
|
| 5876 |
</div>
|
|
@@ -6312,7 +6333,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 6312 |
</div>
|
| 6313 |
|
| 6314 |
<div className="avaliacao-knn-map-wrap">
|
| 6315 |
-
<MapFrame html={knnDetalheMapaHtml} />
|
| 6316 |
</div>
|
| 6317 |
|
| 6318 |
<div className="subpanel avaliacao-knn-detalhes-box">
|
|
|
|
| 883 |
|
| 884 |
const [dados, setDados] = useState(null)
|
| 885 |
const [mapaHtml, setMapaHtml] = useState('')
|
| 886 |
+
const [mapaPayload, setMapaPayload] = useState(null)
|
| 887 |
const [mapaVariavel, setMapaVariavel] = useState(MAPA_VARIAVEL_PADRAO)
|
| 888 |
const [mapaModo, setMapaModo] = useState(MAPA_MODO_PONTOS)
|
| 889 |
const [mapaGerado, setMapaGerado] = useState(false)
|
| 890 |
const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
|
| 891 |
+
const [mapaResiduosPayload, setMapaResiduosPayload] = useState(null)
|
| 892 |
const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
|
| 893 |
const [mapaResiduosExtremoAbs, setMapaResiduosExtremoAbs] = useState(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 894 |
const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
|
|
|
|
| 985 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
| 986 |
const [knnDetalheCardTitulo, setKnnDetalheCardTitulo] = useState('')
|
| 987 |
const [knnDetalheMapaHtml, setKnnDetalheMapaHtml] = useState('')
|
| 988 |
+
const [knnDetalheMapaPayload, setKnnDetalheMapaPayload] = useState(null)
|
| 989 |
const [knnDetalheAvaliando, setKnnDetalheAvaliando] = useState([])
|
| 990 |
const [knnDetalheTabela, setKnnDetalheTabela] = useState(null)
|
| 991 |
const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
|
|
|
|
| 1017 |
? buildElaboracaoModeloLink(repoModeloSelecionado)
|
| 1018 |
: ''
|
| 1019 |
const [sideNavDynamicStyle, setSideNavDynamicStyle] = useState({})
|
| 1020 |
+
const applyMapaResponse = useCallback((resp) => {
|
| 1021 |
+
setMapaHtml(String(resp?.mapa_html || ''))
|
| 1022 |
+
setMapaPayload(resp?.mapa_payload || null)
|
| 1023 |
+
}, [])
|
| 1024 |
+
const clearMapaResponse = useCallback(() => {
|
| 1025 |
+
setMapaHtml('')
|
| 1026 |
+
setMapaPayload(null)
|
| 1027 |
+
}, [])
|
| 1028 |
+
const applyMapaResiduosResponse = useCallback((resp) => {
|
| 1029 |
+
setMapaResiduosHtml(String(resp?.mapa_html || ''))
|
| 1030 |
+
setMapaResiduosPayload(resp?.mapa_payload || null)
|
| 1031 |
+
}, [])
|
| 1032 |
+
const clearMapaResiduosResponse = useCallback(() => {
|
| 1033 |
+
setMapaResiduosHtml('')
|
| 1034 |
+
setMapaResiduosPayload(null)
|
| 1035 |
+
}, [])
|
| 1036 |
|
| 1037 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
| 1038 |
const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
|
|
|
|
| 2125 |
}
|
| 2126 |
setDataMercadoError('')
|
| 2127 |
if (resp.dados) setDados(resp.dados)
|
| 2128 |
+
if (resp?.mapa_html || resp?.mapa_payload) applyMapaResponse(resp)
|
| 2129 |
if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
|
| 2130 |
if (Array.isArray(resp.colunas_data_mercado)) {
|
| 2131 |
setColunasDataMercado(resp.colunas_data_mercado.map((item) => String(item)))
|
|
|
|
| 2309 |
syncPeriodoDataMercadoFromContext(resp.contexto)
|
| 2310 |
}
|
| 2311 |
|
| 2312 |
+
if (resp?.mapa_html || resp?.mapa_payload) {
|
| 2313 |
+
applyMapaResponse(resp)
|
| 2314 |
}
|
| 2315 |
}
|
| 2316 |
|
|
|
|
| 2332 |
setSecao13InterativoEixoYColuna('')
|
| 2333 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2334 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2335 |
+
clearMapaResiduosResponse()
|
| 2336 |
setMapaResiduosGerado(false)
|
| 2337 |
const transformacaoYAplicada = resp.transformacao_y || transformacaoY
|
| 2338 |
const transformacoesXAplicadas = resp.transformacoes_x || transformacoesX
|
|
|
|
| 2485 |
setImportacaoErro('')
|
| 2486 |
await withBusy(async () => {
|
| 2487 |
setMapaGerado(false)
|
| 2488 |
+
clearMapaResponse()
|
| 2489 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2490 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2491 |
setMapaResiduosGerado(false)
|
| 2492 |
+
clearMapaResiduosResponse()
|
| 2493 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2494 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2495 |
setGeoAuto200(true)
|
|
|
|
| 2524 |
setArquivoCarregadoInfo(null)
|
| 2525 |
await withBusy(async () => {
|
| 2526 |
setMapaGerado(false)
|
| 2527 |
+
clearMapaResponse()
|
| 2528 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2529 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2530 |
setMapaResiduosGerado(false)
|
| 2531 |
+
clearMapaResiduosResponse()
|
| 2532 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2533 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2534 |
setGeoAuto200(true)
|
|
|
|
| 2602 |
setImportacaoErro('')
|
| 2603 |
await withBusy(async () => {
|
| 2604 |
setMapaGerado(false)
|
| 2605 |
+
clearMapaResponse()
|
| 2606 |
setMapaVariavel(MAPA_VARIAVEL_PADRAO)
|
| 2607 |
setMapaModo(MAPA_MODO_PONTOS)
|
| 2608 |
setMapaResiduosGerado(false)
|
| 2609 |
+
clearMapaResiduosResponse()
|
| 2610 |
setMapaResiduosModo(MAPA_MODO_PONTOS)
|
| 2611 |
setMapaResiduosExtremoAbs(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
|
| 2612 |
setGeoAuto200(true)
|
|
|
|
| 2752 |
setGeoProcessError('')
|
| 2753 |
try {
|
| 2754 |
const resp = await api.mapCoords(sessionId, manualLat, manualLon)
|
| 2755 |
+
applyMapaResponse(resp)
|
| 2756 |
setDados(resp.dados)
|
| 2757 |
setCoordsInfo(resp.coords)
|
| 2758 |
setGeoStatusHtml('')
|
|
|
|
| 2776 |
setGeoStatusHtml(resp.status_html || '')
|
| 2777 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2778 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2779 |
+
applyMapaResponse(resp)
|
| 2780 |
setDados(resp.dados || null)
|
| 2781 |
setCoordsInfo(resp.coords || null)
|
| 2782 |
setCoordsMode('geocodificar')
|
|
|
|
| 2798 |
setGeoStatusHtml(resp.status_html || '')
|
| 2799 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2800 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2801 |
+
applyMapaResponse(resp)
|
| 2802 |
setDados(resp.dados || null)
|
| 2803 |
setCoordsInfo(resp.coords || null)
|
| 2804 |
setCoordsMode('geocodificar')
|
|
|
|
| 2816 |
setGeoStatusHtml(resp.status_html || '')
|
| 2817 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2818 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2819 |
+
applyMapaResponse(resp)
|
| 2820 |
setDados(resp.dados || null)
|
| 2821 |
setCoordsInfo(resp.coords || null)
|
| 2822 |
setGeoCdlog(escolherColunaCdlogPadrao(resp.coords || null))
|
|
|
|
| 2836 |
setGeoStatusHtml(resp.status_html || '')
|
| 2837 |
setGeoFalhasHtml(resp.falhas_html || '')
|
| 2838 |
setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
|
| 2839 |
+
applyMapaResponse(resp)
|
| 2840 |
setDados(resp.dados || null)
|
| 2841 |
setCoordsInfo(resp.coords || null)
|
| 2842 |
setManualLat(resp.coords?.colunas_disponiveis?.[0] || '')
|
|
|
|
| 3220 |
await withBusy(async () => {
|
| 3221 |
const resp = await api.clearOutlierHistory(sessionId)
|
| 3222 |
setDados(resp.dados)
|
| 3223 |
+
applyMapaResponse(resp)
|
| 3224 |
setResumoOutliers(resp.resumo_outliers || 'Excluidos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0')
|
| 3225 |
setOutliersAnteriores([])
|
| 3226 |
setIteracao(1)
|
|
|
|
| 3306 |
setKnnDetalheErro('')
|
| 3307 |
setKnnDetalheCardTitulo(`Aval. ${idx + 1}`)
|
| 3308 |
setKnnDetalheMapaHtml('')
|
| 3309 |
+
setKnnDetalheMapaPayload(null)
|
| 3310 |
setKnnDetalheAvaliando([])
|
| 3311 |
setKnnDetalheTabela(null)
|
| 3312 |
setKnnDetalheInfo(null)
|
|
|
|
| 3314 |
try {
|
| 3315 |
const resp = await api.evaluationKnnDetailsElab(sessionId, construirPayloadKnnAvaliacao(avaliacao))
|
| 3316 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
| 3317 |
+
setKnnDetalheMapaPayload(resp?.mapa_payload || null)
|
| 3318 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
| 3319 |
setKnnDetalheTabela(resp?.vizinhos_tabela || null)
|
| 3320 |
setKnnDetalheInfo(resp?.knn || null)
|
|
|
|
| 3495 |
if (!sessionId || !mapaGerado) return
|
| 3496 |
await withBusy(async () => {
|
| 3497 |
const resp = await api.updateElaboracaoMap(sessionId, value, nextModo)
|
| 3498 |
+
applyMapaResponse(resp)
|
| 3499 |
})
|
| 3500 |
}
|
| 3501 |
|
|
|
|
| 3504 |
if (!sessionId || !mapaGerado) return
|
| 3505 |
await withBusy(async () => {
|
| 3506 |
const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, value)
|
| 3507 |
+
applyMapaResponse(resp)
|
| 3508 |
})
|
| 3509 |
}
|
| 3510 |
|
|
|
|
| 3516 |
}
|
| 3517 |
await withBusy(async () => {
|
| 3518 |
const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, modoAtual)
|
| 3519 |
+
applyMapaResponse(resp)
|
| 3520 |
setMapaGerado(true)
|
| 3521 |
})
|
| 3522 |
}
|
|
|
|
| 3527 |
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 3528 |
await withBusy(async () => {
|
| 3529 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value, extremoAbs)
|
| 3530 |
+
applyMapaResiduosResponse(resp)
|
| 3531 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3532 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3533 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
|
|
| 3544 |
if (!sessionId || !mapaResiduosGerado) return
|
| 3545 |
await withBusy(async () => {
|
| 3546 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 3547 |
+
applyMapaResiduosResponse(resp)
|
| 3548 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3549 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3550 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
|
|
| 3559 |
const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
|
| 3560 |
await withBusy(async () => {
|
| 3561 |
const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
|
| 3562 |
+
applyMapaResiduosResponse(resp)
|
| 3563 |
const extremoResp = Number(resp?.escala_extremo_abs)
|
| 3564 |
if (Number.isFinite(extremoResp) && extremoResp > 0) {
|
| 3565 |
setMapaResiduosExtremoAbs(String(Number(extremoResp)))
|
|
|
|
| 4546 |
Fazer download
|
| 4547 |
</button>
|
| 4548 |
</div>
|
| 4549 |
+
<MapFrame html={mapaHtml} payload={mapaPayload} sessionId={sessionId} />
|
| 4550 |
</details>
|
| 4551 |
)}
|
| 4552 |
</div>
|
|
|
|
| 5891 |
Fazer download
|
| 5892 |
</button>
|
| 5893 |
</div>
|
| 5894 |
+
<MapFrame html={mapaResiduosHtml} payload={mapaResiduosPayload} sessionId={sessionId} />
|
| 5895 |
</>
|
| 5896 |
)}
|
| 5897 |
</div>
|
|
|
|
| 6333 |
</div>
|
| 6334 |
|
| 6335 |
<div className="avaliacao-knn-map-wrap">
|
| 6336 |
+
<MapFrame html={knnDetalheMapaHtml} payload={knnDetalheMapaPayload} sessionId={sessionId} />
|
| 6337 |
</div>
|
| 6338 |
|
| 6339 |
<div className="subpanel avaliacao-knn-detalhes-box">
|
frontend/src/components/LeafletMapFrame.jsx
ADDED
|
@@ -0,0 +1,1004 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
+
import L from 'leaflet'
|
| 3 |
+
import 'leaflet.fullscreen'
|
| 4 |
+
import 'leaflet.heat'
|
| 5 |
+
import { apiUrl, getAuthToken } from '../api'
|
| 6 |
+
|
| 7 |
+
let bairrosGeojsonCache = null
|
| 8 |
+
let bairrosGeojsonPromise = null
|
| 9 |
+
|
| 10 |
+
function escapeHtml(value) {
|
| 11 |
+
return String(value ?? '')
|
| 12 |
+
.replaceAll('&', '&')
|
| 13 |
+
.replaceAll('<', '<')
|
| 14 |
+
.replaceAll('>', '>')
|
| 15 |
+
.replaceAll('"', '"')
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function buildTooltipHtml(tooltip) {
|
| 19 |
+
const title = String(tooltip?.title || '').trim()
|
| 20 |
+
const label = String(tooltip?.label || '').trim()
|
| 21 |
+
const value = String(tooltip?.value || '').trim()
|
| 22 |
+
return [
|
| 23 |
+
'<div style="font-family:\'Segoe UI\',Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;">',
|
| 24 |
+
title ? `<b>${escapeHtml(title)}</b>` : '',
|
| 25 |
+
label && value ? `<br><span style="color:#555;">${escapeHtml(label)}:</span> <b>${escapeHtml(value)}</b>` : '',
|
| 26 |
+
'</div>',
|
| 27 |
+
].join('')
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function buildPopupErrorHtml(message) {
|
| 31 |
+
const text = escapeHtml(message || 'Falha ao carregar os dados do registro.')
|
| 32 |
+
return (
|
| 33 |
+
'<div style="font-family:\'Segoe UI\'; border-radius:8px; overflow:hidden; width:340px; max-width:340px;">'
|
| 34 |
+
+ '<div style="background:#6c757d; color:white; padding:10px 15px; font-weight:600;">Dados do Registro</div>'
|
| 35 |
+
+ `<div style="padding:12px 15px; background:#f8f9fa; color:#b42318; font-size:12px;">${text}</div>`
|
| 36 |
+
+ '</div>'
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function ensurePopupPager(root) {
|
| 41 |
+
if (!root || root.dataset.mesaPagerBound === '1') return
|
| 42 |
+
root.dataset.mesaPagerBound = '1'
|
| 43 |
+
|
| 44 |
+
const pages = Array.from(root.querySelectorAll('.mesa-popup-page'))
|
| 45 |
+
if (!pages.length) return
|
| 46 |
+
|
| 47 |
+
const numberWrap = root.querySelector('[data-page-number-wrap]')
|
| 48 |
+
if (numberWrap) {
|
| 49 |
+
numberWrap.innerHTML = ''
|
| 50 |
+
pages.forEach((_, index) => {
|
| 51 |
+
const button = document.createElement('button')
|
| 52 |
+
button.type = 'button'
|
| 53 |
+
button.dataset.pageNumber = String(index + 1)
|
| 54 |
+
button.textContent = String(index + 1)
|
| 55 |
+
button.style.border = '1px solid #ced8e2'
|
| 56 |
+
button.style.background = '#fff'
|
| 57 |
+
button.style.borderRadius = '6px'
|
| 58 |
+
button.style.padding = '2px 7px'
|
| 59 |
+
button.style.fontSize = '11px'
|
| 60 |
+
button.style.cursor = 'pointer'
|
| 61 |
+
button.style.color = '#4e6479'
|
| 62 |
+
button.style.display = 'inline-flex'
|
| 63 |
+
button.style.alignItems = 'center'
|
| 64 |
+
button.style.justifyContent = 'center'
|
| 65 |
+
numberWrap.appendChild(button)
|
| 66 |
+
})
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function updatePager(targetPage) {
|
| 70 |
+
const total = pages.length
|
| 71 |
+
let current = Number.parseInt(String(targetPage || root.dataset.currentPage || '1'), 10)
|
| 72 |
+
if (!Number.isFinite(current) || current < 1) current = 1
|
| 73 |
+
if (current > total) current = total
|
| 74 |
+
root.dataset.currentPage = String(current)
|
| 75 |
+
|
| 76 |
+
pages.forEach((pageEl, index) => {
|
| 77 |
+
pageEl.style.display = index + 1 === current ? 'block' : 'none'
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
Array.from(root.querySelectorAll('[data-page-number]')).forEach((button) => {
|
| 81 |
+
const value = Number.parseInt(String(button.dataset.pageNumber || '1'), 10)
|
| 82 |
+
const ativo = value === current
|
| 83 |
+
button.style.background = ativo ? '#eaf1f7' : '#fff'
|
| 84 |
+
button.style.borderColor = ativo ? '#9fb4c8' : '#ced8e2'
|
| 85 |
+
button.style.color = ativo ? '#2f4b66' : '#4e6479'
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
const onFirst = current <= 1
|
| 89 |
+
const onLast = current >= total
|
| 90 |
+
const navState = [
|
| 91 |
+
['first', onFirst],
|
| 92 |
+
['prev', onFirst],
|
| 93 |
+
['next', onLast],
|
| 94 |
+
['last', onLast],
|
| 95 |
+
]
|
| 96 |
+
navState.forEach(([action, disabled]) => {
|
| 97 |
+
const button = root.querySelector(`[data-a="${action}"]`)
|
| 98 |
+
if (!button) return
|
| 99 |
+
button.disabled = disabled
|
| 100 |
+
button.style.opacity = disabled ? '0.45' : '1'
|
| 101 |
+
button.style.cursor = disabled ? 'not-allowed' : 'pointer'
|
| 102 |
+
})
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
root.addEventListener('click', (event) => {
|
| 106 |
+
const button = event.target.closest('[data-a], [data-page-number]')
|
| 107 |
+
if (!button) return
|
| 108 |
+
event.preventDefault()
|
| 109 |
+
event.stopPropagation()
|
| 110 |
+
|
| 111 |
+
const current = Number.parseInt(String(root.dataset.currentPage || '1'), 10) || 1
|
| 112 |
+
if (button.dataset.pageNumber) {
|
| 113 |
+
updatePager(Number.parseInt(String(button.dataset.pageNumber), 10) || current)
|
| 114 |
+
return
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
const action = String(button.dataset.a || '').trim()
|
| 118 |
+
if (action === 'first') updatePager(1)
|
| 119 |
+
if (action === 'prev') updatePager(current - 1)
|
| 120 |
+
if (action === 'next') updatePager(current + 1)
|
| 121 |
+
if (action === 'last') updatePager(pages.length)
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
updatePager(1)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
async function carregarBairrosGeojson(url) {
|
| 128 |
+
if (bairrosGeojsonCache) return bairrosGeojsonCache
|
| 129 |
+
if (bairrosGeojsonPromise) return bairrosGeojsonPromise
|
| 130 |
+
|
| 131 |
+
const endpoint = String(url || '').trim()
|
| 132 |
+
if (!endpoint) return null
|
| 133 |
+
|
| 134 |
+
bairrosGeojsonPromise = fetch(endpoint, {
|
| 135 |
+
headers: (() => {
|
| 136 |
+
const token = getAuthToken()
|
| 137 |
+
return token ? { 'X-Auth-Token': token } : {}
|
| 138 |
+
})(),
|
| 139 |
+
})
|
| 140 |
+
.then(async (response) => {
|
| 141 |
+
if (!response.ok) {
|
| 142 |
+
throw new Error('Falha ao carregar camada de bairros.')
|
| 143 |
+
}
|
| 144 |
+
return response.json()
|
| 145 |
+
})
|
| 146 |
+
.then((payload) => {
|
| 147 |
+
bairrosGeojsonCache = payload
|
| 148 |
+
return payload
|
| 149 |
+
})
|
| 150 |
+
.finally(() => {
|
| 151 |
+
bairrosGeojsonPromise = null
|
| 152 |
+
})
|
| 153 |
+
|
| 154 |
+
return bairrosGeojsonPromise
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function construirLegendaControl(legend) {
|
| 158 |
+
if (!legend) return null
|
| 159 |
+
|
| 160 |
+
return L.control({ position: 'bottomright' })
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
function formatLegendNumber(value) {
|
| 164 |
+
const number = Number(value)
|
| 165 |
+
if (!Number.isFinite(number)) return ''
|
| 166 |
+
return number.toLocaleString('pt-BR', { maximumFractionDigits: 2 })
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
function buildLegendHtml(legend) {
|
| 170 |
+
const title = escapeHtml(String(legend?.title || '').trim())
|
| 171 |
+
const colors = Array.isArray(legend?.colors) ? legend.colors.filter(Boolean) : []
|
| 172 |
+
const gradient = colors.length ? colors.join(', ') : '#2ecc71, #e74c3c'
|
| 173 |
+
const tickValues = Array.isArray(legend?.tick_values) ? legend.tick_values : []
|
| 174 |
+
const tickLabels = Array.isArray(legend?.tick_labels) ? legend.tick_labels : []
|
| 175 |
+
const hasTicks = tickValues.length > 1 && tickValues.length === tickLabels.length
|
| 176 |
+
|
| 177 |
+
const ticksHtml = hasTicks
|
| 178 |
+
? (
|
| 179 |
+
'<div class="mesa-leaflet-legend-ticks">'
|
| 180 |
+
+ tickValues.map((tickValue, index) => {
|
| 181 |
+
const rawMin = Number(legend?.vmin)
|
| 182 |
+
const rawMax = Number(legend?.vmax)
|
| 183 |
+
const ratio = Number.isFinite(rawMin) && Number.isFinite(rawMax) && rawMax > rawMin
|
| 184 |
+
? (Number(tickValue) - rawMin) / (rawMax - rawMin)
|
| 185 |
+
: index / Math.max(1, tickValues.length - 1)
|
| 186 |
+
const left = Math.max(0, Math.min(100, ratio * 100))
|
| 187 |
+
return (
|
| 188 |
+
`<span class="mesa-leaflet-legend-tick" style="left:${left.toFixed(4)}%;">`
|
| 189 |
+
+ `${escapeHtml(String(tickLabels[index] || ''))}`
|
| 190 |
+
+ '</span>'
|
| 191 |
+
)
|
| 192 |
+
}).join('')
|
| 193 |
+
+ '</div>'
|
| 194 |
+
)
|
| 195 |
+
: ''
|
| 196 |
+
|
| 197 |
+
const scaleHtml = hasTicks
|
| 198 |
+
? ''
|
| 199 |
+
: (
|
| 200 |
+
'<div class="mesa-leaflet-legend-scale">'
|
| 201 |
+
+ `<span>${escapeHtml(formatLegendNumber(legend?.vmin))}</span>`
|
| 202 |
+
+ `<span>${escapeHtml(formatLegendNumber(legend?.vmax))}</span>`
|
| 203 |
+
+ '</div>'
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
return [
|
| 207 |
+
title ? `<strong>${title}</strong>` : '',
|
| 208 |
+
`<div class="mesa-leaflet-legend-bar" style="background: linear-gradient(90deg, ${gradient});"></div>`,
|
| 209 |
+
ticksHtml,
|
| 210 |
+
scaleHtml,
|
| 211 |
+
].join('')
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
function addNoticeControl(map, notice) {
|
| 215 |
+
const message = String(notice?.message || '').trim()
|
| 216 |
+
if (!message) return null
|
| 217 |
+
const control = L.control({ position: notice?.position || 'topright' })
|
| 218 |
+
control.onAdd = () => {
|
| 219 |
+
const div = L.DomUtil.create('div', 'mesa-leaflet-notice')
|
| 220 |
+
div.innerHTML = escapeHtml(message)
|
| 221 |
+
return div
|
| 222 |
+
}
|
| 223 |
+
control.addTo(map)
|
| 224 |
+
return control
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function buildIndiceBadgeHtml(indice) {
|
| 228 |
+
return (
|
| 229 |
+
'<div style="transform: translate(10px, -14px);display:inline-block;background: rgba(255, 255, 255, 0.9);'
|
| 230 |
+
+ 'border: 1px solid rgba(28, 45, 66, 0.45);border-radius: 10px;padding: 1px 6px;font-size: 11px;'
|
| 231 |
+
+ 'font-weight: 700;line-height: 1.2;color: #1f2f44;white-space: nowrap;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);'
|
| 232 |
+
+ 'pointer-events: none;">'
|
| 233 |
+
+ `${escapeHtml(indice)}`
|
| 234 |
+
+ '</div>'
|
| 235 |
+
)
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
function formatCoordinate(value, positiveLabel, negativeLabel) {
|
| 239 |
+
const numeric = Number(value)
|
| 240 |
+
if (!Number.isFinite(numeric)) return '-'
|
| 241 |
+
const absolute = Math.abs(numeric)
|
| 242 |
+
const degrees = Math.floor(absolute)
|
| 243 |
+
const minutesFloat = (absolute - degrees) * 60
|
| 244 |
+
const minutes = Math.floor(minutesFloat)
|
| 245 |
+
const seconds = ((minutesFloat - minutes) * 60).toFixed(2)
|
| 246 |
+
const direction = numeric >= 0 ? positiveLabel : negativeLabel
|
| 247 |
+
return `${degrees}° ${minutes.toString().padStart(2, '0')}' ${seconds.padStart(5, '0')}" ${direction}`
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
function formatLatLng(latlng) {
|
| 251 |
+
if (!latlng) return ''
|
| 252 |
+
return `${formatCoordinate(latlng.lat, 'N', 'S')} / ${formatCoordinate(latlng.lng, 'L', 'W')}`
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
function formatDecimalLatLng(latlng) {
|
| 256 |
+
if (!latlng) return ''
|
| 257 |
+
return `${Number(latlng.lat).toFixed(6)} / ${Number(latlng.lng).toFixed(6)}`
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
function formatDistance(distanceMeters) {
|
| 261 |
+
const value = Number(distanceMeters)
|
| 262 |
+
if (!Number.isFinite(value) || value <= 0) return '0 m'
|
| 263 |
+
if (value >= 1000) {
|
| 264 |
+
return `${(value / 1000).toLocaleString('pt-BR', { maximumFractionDigits: 2 })} km`
|
| 265 |
+
}
|
| 266 |
+
return `${value.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} m`
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function computePolygonArea(latlngs) {
|
| 270 |
+
if (!Array.isArray(latlngs) || latlngs.length < 3) return 0
|
| 271 |
+
const d2r = Math.PI / 180
|
| 272 |
+
const radius = 6378137
|
| 273 |
+
let area = 0
|
| 274 |
+
for (let index = 0; index < latlngs.length; index += 1) {
|
| 275 |
+
const current = latlngs[index]
|
| 276 |
+
const next = latlngs[(index + 1) % latlngs.length]
|
| 277 |
+
area += (
|
| 278 |
+
(Number(next.lng) - Number(current.lng)) * d2r
|
| 279 |
+
* (2 + Math.sin(Number(current.lat) * d2r) + Math.sin(Number(next.lat) * d2r))
|
| 280 |
+
)
|
| 281 |
+
}
|
| 282 |
+
return Math.abs(area * radius * radius / 2)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function formatArea(areaSquareMeters) {
|
| 286 |
+
const value = Number(areaSquareMeters)
|
| 287 |
+
if (!Number.isFinite(value) || value <= 0) return ''
|
| 288 |
+
if (value >= 10000) {
|
| 289 |
+
return `${(value / 10000).toLocaleString('pt-BR', { maximumFractionDigits: 2 })} ha`
|
| 290 |
+
}
|
| 291 |
+
return `${value.toLocaleString('pt-BR', { maximumFractionDigits: 0 })} m²`
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function buildLegacyOverlayLayers(payload) {
|
| 295 |
+
const overlays = []
|
| 296 |
+
|
| 297 |
+
if (payload?.show_bairros) {
|
| 298 |
+
overlays.push({
|
| 299 |
+
id: 'bairros',
|
| 300 |
+
label: 'Bairros',
|
| 301 |
+
show: true,
|
| 302 |
+
geojson_url: payload?.bairros_geojson_url || '',
|
| 303 |
+
geojson_style: {
|
| 304 |
+
color: '#4c6882',
|
| 305 |
+
weight: 1.0,
|
| 306 |
+
fillColor: '#f39c12',
|
| 307 |
+
fillOpacity: 0.04,
|
| 308 |
+
},
|
| 309 |
+
geojson_tooltip_properties: ['NOME', 'BAIRRO', 'NME_BAI', 'NOME_BAIRRO'],
|
| 310 |
+
geojson_tooltip_label: 'Bairro',
|
| 311 |
+
})
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
if (Array.isArray(payload?.market_points) && payload.market_points.length) {
|
| 315 |
+
overlays.push({
|
| 316 |
+
id: 'mercado',
|
| 317 |
+
label: 'Mercado',
|
| 318 |
+
show: true,
|
| 319 |
+
points: payload.market_points.map((item) => ({
|
| 320 |
+
...item,
|
| 321 |
+
popup_request: Number.isFinite(Number(item?.row_id))
|
| 322 |
+
? { kind: 'visualizacao_row', row_id: Number(item.row_id) }
|
| 323 |
+
: null,
|
| 324 |
+
})),
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
+
overlays.push({
|
| 328 |
+
id: 'indices',
|
| 329 |
+
label: 'Índices',
|
| 330 |
+
show: Boolean(payload?.show_indices),
|
| 331 |
+
markers: payload.market_points.map((item) => ({
|
| 332 |
+
lat: Number(item?.lat),
|
| 333 |
+
lon: Number(item?.lon),
|
| 334 |
+
marker_html: buildIndiceBadgeHtml(item?.indice),
|
| 335 |
+
icon_size: [72, 24],
|
| 336 |
+
icon_anchor: [0, 0],
|
| 337 |
+
class_name: 'mesa-indice-label',
|
| 338 |
+
interactive: false,
|
| 339 |
+
keyboard: false,
|
| 340 |
+
})),
|
| 341 |
+
})
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
if (Array.isArray(payload?.trabalhos_tecnicos_points) && payload.trabalhos_tecnicos_points.length) {
|
| 345 |
+
overlays.push({
|
| 346 |
+
id: 'avaliandos',
|
| 347 |
+
label: 'Avaliandos',
|
| 348 |
+
show: true,
|
| 349 |
+
markers: payload.trabalhos_tecnicos_points,
|
| 350 |
+
})
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
return overlays
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
async function readJsonSafely(response) {
|
| 357 |
+
const raw = await response.text()
|
| 358 |
+
if (!raw || !raw.trim()) return null
|
| 359 |
+
try {
|
| 360 |
+
return JSON.parse(raw)
|
| 361 |
+
} catch {
|
| 362 |
+
return null
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
export default function LeafletMapFrame({ payload, sessionId }) {
|
| 367 |
+
const hostRef = useRef(null)
|
| 368 |
+
const popupCacheRef = useRef(new Map())
|
| 369 |
+
const [runtimeError, setRuntimeError] = useState('')
|
| 370 |
+
|
| 371 |
+
useEffect(() => {
|
| 372 |
+
if (!hostRef.current || !payload) return undefined
|
| 373 |
+
let disposed = false
|
| 374 |
+
setRuntimeError('')
|
| 375 |
+
hostRef.current.innerHTML = ''
|
| 376 |
+
|
| 377 |
+
const map = L.map(hostRef.current, {
|
| 378 |
+
zoomControl: true,
|
| 379 |
+
preferCanvas: true,
|
| 380 |
+
})
|
| 381 |
+
let restoreMapInteractions = null
|
| 382 |
+
const bairrosPane = map.createPane('mesa-bairros-pane')
|
| 383 |
+
const marketPane = map.createPane('mesa-market-pane')
|
| 384 |
+
const trabalhosPane = map.createPane('mesa-trabalhos-pane')
|
| 385 |
+
const indicesPane = map.createPane('mesa-indices-pane')
|
| 386 |
+
bairrosPane.style.zIndex = '360'
|
| 387 |
+
marketPane.style.zIndex = '420'
|
| 388 |
+
trabalhosPane.style.zIndex = '430'
|
| 389 |
+
indicesPane.style.zIndex = '440'
|
| 390 |
+
|
| 391 |
+
const canvasRenderer = L.canvas({ padding: 0.5, pane: 'mesa-market-pane' })
|
| 392 |
+
const baseLayers = {}
|
| 393 |
+
const overlayLayers = {}
|
| 394 |
+
|
| 395 |
+
;(payload.tile_layers || []).forEach((layerDef, index) => {
|
| 396 |
+
const tileLayer = L.tileLayer(String(layerDef?.url || ''), {
|
| 397 |
+
attribution: index === 0
|
| 398 |
+
? '© OpenStreetMap contributors'
|
| 399 |
+
: '© OpenStreetMap contributors © CARTO',
|
| 400 |
+
})
|
| 401 |
+
const label = String(layerDef?.label || layerDef?.id || `Base ${index + 1}`)
|
| 402 |
+
baseLayers[label] = tileLayer
|
| 403 |
+
if (index === 0) {
|
| 404 |
+
tileLayer.addTo(map)
|
| 405 |
+
}
|
| 406 |
+
})
|
| 407 |
+
const overlaySpecs = Array.isArray(payload.overlay_layers) && payload.overlay_layers.length
|
| 408 |
+
? payload.overlay_layers
|
| 409 |
+
: buildLegacyOverlayLayers(payload)
|
| 410 |
+
const responsivePointContainers = []
|
| 411 |
+
|
| 412 |
+
const radiusBehavior = payload.radius_behavior || {}
|
| 413 |
+
const minRadius = Number(radiusBehavior.min_radius) || 1.6
|
| 414 |
+
const maxRadius = Number(radiusBehavior.max_radius) || 52.0
|
| 415 |
+
const referenceZoom = Number(radiusBehavior.reference_zoom) || 12.0
|
| 416 |
+
const growthFactor = Number(radiusBehavior.growth_factor) || 0.2
|
| 417 |
+
|
| 418 |
+
function clamp(value, min, max) {
|
| 419 |
+
return Math.max(min, Math.min(max, value))
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
function applyResponsiveRadius() {
|
| 423 |
+
const zoom = typeof map.getZoom === 'function' ? map.getZoom() : referenceZoom
|
| 424 |
+
const zoomDelta = zoom - referenceZoom
|
| 425 |
+
const expFactor = 2 ** (zoomDelta * growthFactor)
|
| 426 |
+
let floorScale = 1.0
|
| 427 |
+
if (zoomDelta >= 0) {
|
| 428 |
+
floorScale = 1 + zoomDelta * 0.22
|
| 429 |
+
if (zoom >= 15) {
|
| 430 |
+
floorScale += (zoom - 14) * 0.30
|
| 431 |
+
}
|
| 432 |
+
} else {
|
| 433 |
+
floorScale = Math.max(0.28, 1 + zoomDelta * 0.20)
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
responsivePointContainers.forEach((container) => {
|
| 437 |
+
if (!container || typeof container.eachLayer !== 'function') return
|
| 438 |
+
container.eachLayer((layer) => {
|
| 439 |
+
if (typeof layer.setRadius !== 'function') return
|
| 440 |
+
const base = Number(layer.options?.mesaBaseRadius || layer.options?.radius || 4)
|
| 441 |
+
const dynamicMin = Math.max(minRadius, base * floorScale)
|
| 442 |
+
const dynamicMax = Math.max(dynamicMin + 0.1, Math.min(maxRadius, base * 8.0))
|
| 443 |
+
layer.setRadius(clamp(base * expFactor, dynamicMin, dynamicMax))
|
| 444 |
+
})
|
| 445 |
+
})
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
async function carregarPopupVisualizacao(rowId, layer) {
|
| 449 |
+
const cacheKey = String(rowId)
|
| 450 |
+
const cached = popupCacheRef.current.get(cacheKey)
|
| 451 |
+
if (cached) {
|
| 452 |
+
const cachedHtml = typeof cached === 'string' ? cached : String(cached?.html || '')
|
| 453 |
+
const cachedWidth = typeof cached === 'string' ? 420 : (Number(cached?.width) || 420)
|
| 454 |
+
layer.unbindPopup()
|
| 455 |
+
layer.bindPopup(cachedHtml, { maxWidth: cachedWidth }).openPopup()
|
| 456 |
+
window.setTimeout(() => {
|
| 457 |
+
const root = layer.getPopup()?.getElement()?.querySelector('[data-pager]')
|
| 458 |
+
if (root) ensurePopupPager(root)
|
| 459 |
+
}, 0)
|
| 460 |
+
return
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
layer.bindPopup(
|
| 464 |
+
'<div style="font-family:\'Segoe UI\'; color:#6c757d; font-size:12px;">Carregando detalhes...</div>',
|
| 465 |
+
{ maxWidth: 360 },
|
| 466 |
+
).openPopup()
|
| 467 |
+
|
| 468 |
+
try {
|
| 469 |
+
const response = await fetch(apiUrl('/api/visualizacao/map/popup'), {
|
| 470 |
+
method: 'POST',
|
| 471 |
+
headers: {
|
| 472 |
+
'Content-Type': 'application/json',
|
| 473 |
+
...(getAuthToken() ? { 'X-Auth-Token': getAuthToken() } : {}),
|
| 474 |
+
},
|
| 475 |
+
body: JSON.stringify({
|
| 476 |
+
session_id: sessionId,
|
| 477 |
+
row_id: rowId,
|
| 478 |
+
}),
|
| 479 |
+
})
|
| 480 |
+
const payloadResp = await readJsonSafely(response)
|
| 481 |
+
if (!response.ok) {
|
| 482 |
+
throw new Error(String(payloadResp?.detail || 'Falha ao carregar os dados do registro.'))
|
| 483 |
+
}
|
| 484 |
+
if (!payloadResp || typeof payloadResp !== 'object') {
|
| 485 |
+
throw new Error('Resposta vazia ao carregar os dados do registro.')
|
| 486 |
+
}
|
| 487 |
+
const popupHtml = String(payloadResp?.popup_html || '')
|
| 488 |
+
const popupWidth = Number(payloadResp?.popup_width) || 420
|
| 489 |
+
if (!popupHtml.trim()) {
|
| 490 |
+
throw new Error('Detalhes do registro indisponíveis para este ponto.')
|
| 491 |
+
}
|
| 492 |
+
popupCacheRef.current.set(cacheKey, { html: popupHtml, width: popupWidth })
|
| 493 |
+
layer.unbindPopup()
|
| 494 |
+
layer.bindPopup(popupHtml, { maxWidth: popupWidth }).openPopup()
|
| 495 |
+
window.setTimeout(() => {
|
| 496 |
+
const root = layer.getPopup()?.getElement()?.querySelector('[data-pager]')
|
| 497 |
+
if (root) ensurePopupPager(root)
|
| 498 |
+
}, 0)
|
| 499 |
+
} catch (error) {
|
| 500 |
+
layer.unbindPopup()
|
| 501 |
+
layer.bindPopup(buildPopupErrorHtml(error?.message || 'Falha ao carregar os dados do registro.'), { maxWidth: 360 }).openPopup()
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
function bindPointPopup(marker, item) {
|
| 506 |
+
const popupHtml = String(item?.popup_html || '').trim()
|
| 507 |
+
if (popupHtml) {
|
| 508 |
+
marker.bindPopup(popupHtml, { maxWidth: Number(item?.popup_max_width) || 420 })
|
| 509 |
+
return
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
const popupRequest = item?.popup_request
|
| 513 |
+
if (popupRequest?.kind === 'visualizacao_row' && Number.isFinite(Number(popupRequest?.row_id)) && sessionId) {
|
| 514 |
+
marker.on('click', () => {
|
| 515 |
+
void carregarPopupVisualizacao(Number(popupRequest.row_id), marker)
|
| 516 |
+
})
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function addPointMarkers(layerGroup, items) {
|
| 521 |
+
if (!Array.isArray(items) || !items.length) return
|
| 522 |
+
responsivePointContainers.push(layerGroup)
|
| 523 |
+
items.forEach((item) => {
|
| 524 |
+
const marker = L.circleMarker([Number(item.lat), Number(item.lon)], {
|
| 525 |
+
renderer: canvasRenderer,
|
| 526 |
+
pane: String(item?.pane || 'mesa-market-pane'),
|
| 527 |
+
radius: Number(item?.base_radius) || 4,
|
| 528 |
+
color: String(item?.stroke_color || '#000000'),
|
| 529 |
+
weight: Number.isFinite(Number(item?.stroke_weight))
|
| 530 |
+
? Number(item.stroke_weight)
|
| 531 |
+
: (Number(item?.base_radius) > 4 ? 1 : 0.8),
|
| 532 |
+
fill: item?.fill !== false,
|
| 533 |
+
fillColor: String(item?.color || item?.fill_color || '#FF8C00'),
|
| 534 |
+
fillOpacity: Number.isFinite(Number(item?.fill_opacity)) ? Number(item.fill_opacity) : 0.68,
|
| 535 |
+
interactive: item?.interactive !== false,
|
| 536 |
+
bubblingMouseEvents: false,
|
| 537 |
+
})
|
| 538 |
+
marker.options.mesaBaseRadius = Number(item?.base_radius) || 4
|
| 539 |
+
const tooltipHtml = String(item?.tooltip_html || '').trim()
|
| 540 |
+
if (tooltipHtml) {
|
| 541 |
+
marker.bindTooltip(tooltipHtml, { sticky: item?.tooltip_sticky !== false })
|
| 542 |
+
} else if (item?.tooltip) {
|
| 543 |
+
marker.bindTooltip(buildTooltipHtml(item.tooltip), { sticky: item?.tooltip_sticky !== false })
|
| 544 |
+
}
|
| 545 |
+
bindPointPopup(marker, item)
|
| 546 |
+
layerGroup.addLayer(marker)
|
| 547 |
+
})
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
function addMarkerOverlays(layerGroup, items) {
|
| 551 |
+
if (!Array.isArray(items) || !items.length) return
|
| 552 |
+
items.forEach((item) => {
|
| 553 |
+
const icon = L.divIcon({
|
| 554 |
+
html: String(item?.marker_html || ''),
|
| 555 |
+
iconSize: Array.isArray(item?.icon_size) ? item.icon_size : [14, 14],
|
| 556 |
+
iconAnchor: Array.isArray(item?.icon_anchor) ? item.icon_anchor : [7, 7],
|
| 557 |
+
className: String(item?.class_name || 'mesa-map-marker'),
|
| 558 |
+
})
|
| 559 |
+
const marker = L.marker([Number(item.lat), Number(item.lon)], {
|
| 560 |
+
icon,
|
| 561 |
+
pane: String(item?.pane || (String(item?.class_name || '').includes('indice') ? 'mesa-indices-pane' : 'mesa-trabalhos-pane')),
|
| 562 |
+
bubblingMouseEvents: item?.bubbling_mouse_events === true,
|
| 563 |
+
interactive: item?.interactive !== false,
|
| 564 |
+
keyboard: item?.keyboard !== false,
|
| 565 |
+
})
|
| 566 |
+
if (item?.ignore_bounds) {
|
| 567 |
+
marker.options.mesaIgnoreBounds = true
|
| 568 |
+
}
|
| 569 |
+
if (item?.tooltip_html) {
|
| 570 |
+
marker.bindTooltip(String(item.tooltip_html), { sticky: item?.tooltip_sticky !== false })
|
| 571 |
+
}
|
| 572 |
+
if (item?.popup_html) {
|
| 573 |
+
marker.bindPopup(String(item.popup_html), { maxWidth: Number(item?.popup_max_width) || 360 })
|
| 574 |
+
}
|
| 575 |
+
layerGroup.addLayer(marker)
|
| 576 |
+
})
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
function addShapeOverlays(layerGroup, shapes) {
|
| 580 |
+
if (!Array.isArray(shapes) || !shapes.length) return
|
| 581 |
+
shapes.forEach((shape) => {
|
| 582 |
+
const shapeType = String(shape?.type || shape?.shape_type || '').trim().toLowerCase()
|
| 583 |
+
const style = {
|
| 584 |
+
color: String(shape?.color || '#1f6fb2'),
|
| 585 |
+
weight: Number.isFinite(Number(shape?.weight)) ? Number(shape.weight) : 2,
|
| 586 |
+
opacity: Number.isFinite(Number(shape?.opacity)) ? Number(shape.opacity) : 0.8,
|
| 587 |
+
fill: shape?.fill === true,
|
| 588 |
+
fillColor: String(shape?.fill_color || shape?.color || '#1f6fb2'),
|
| 589 |
+
fillOpacity: Number.isFinite(Number(shape?.fill_opacity)) ? Number(shape.fill_opacity) : 0.12,
|
| 590 |
+
dashArray: shape?.dash_array ? String(shape.dash_array) : undefined,
|
| 591 |
+
pane: String(shape?.pane || 'mesa-bairros-pane'),
|
| 592 |
+
}
|
| 593 |
+
let layer = null
|
| 594 |
+
|
| 595 |
+
if (shapeType === 'circle' && Array.isArray(shape?.center) && shape.center.length === 2) {
|
| 596 |
+
layer = L.circle(shape.center, { ...style, radius: Number(shape?.radius_m) || 0 })
|
| 597 |
+
} else if (shapeType === 'polyline' && Array.isArray(shape?.coords) && shape.coords.length) {
|
| 598 |
+
layer = L.polyline(shape.coords, style)
|
| 599 |
+
} else if (shapeType === 'polygon' && Array.isArray(shape?.coords) && shape.coords.length) {
|
| 600 |
+
layer = L.polygon(shape.coords, style)
|
| 601 |
+
} else if (shapeType === 'circlemarker' && Array.isArray(shape?.center) && shape.center.length === 2) {
|
| 602 |
+
layer = L.circleMarker(shape.center, { ...style, radius: Number(shape?.radius) || 6 })
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
if (!layer) return
|
| 606 |
+
if (shape?.ignore_bounds) {
|
| 607 |
+
layer.options.mesaIgnoreBounds = true
|
| 608 |
+
}
|
| 609 |
+
if (shape?.tooltip_html) {
|
| 610 |
+
layer.bindTooltip(String(shape.tooltip_html), { sticky: shape?.tooltip_sticky !== false })
|
| 611 |
+
}
|
| 612 |
+
if (shape?.popup_html) {
|
| 613 |
+
layer.bindPopup(String(shape.popup_html), { maxWidth: Number(shape?.popup_max_width) || 360 })
|
| 614 |
+
}
|
| 615 |
+
layerGroup.addLayer(layer)
|
| 616 |
+
})
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
function addGeoJsonOverlay(layerGroup, spec) {
|
| 620 |
+
const geojsonLayer = L.geoJSON(null, {
|
| 621 |
+
pane: String(spec?.geojson_pane || 'mesa-bairros-pane'),
|
| 622 |
+
style: () => ({
|
| 623 |
+
color: String(spec?.geojson_style?.color || '#4c6882'),
|
| 624 |
+
weight: Number.isFinite(Number(spec?.geojson_style?.weight)) ? Number(spec.geojson_style.weight) : 1.0,
|
| 625 |
+
fillColor: String(spec?.geojson_style?.fillColor || '#f39c12'),
|
| 626 |
+
fillOpacity: Number.isFinite(Number(spec?.geojson_style?.fillOpacity))
|
| 627 |
+
? Number(spec.geojson_style.fillOpacity)
|
| 628 |
+
: 0.04,
|
| 629 |
+
}),
|
| 630 |
+
onEachFeature: (feature, layer) => {
|
| 631 |
+
const props = feature?.properties || {}
|
| 632 |
+
const candidates = Array.isArray(spec?.geojson_tooltip_properties) ? spec.geojson_tooltip_properties : []
|
| 633 |
+
const value = candidates.map((key) => props?.[key]).find((entry) => String(entry || '').trim())
|
| 634 |
+
if (value) {
|
| 635 |
+
const prefix = String(spec?.geojson_tooltip_label || '').trim()
|
| 636 |
+
layer.bindTooltip(prefix ? `${prefix}: ${String(value)}` : String(value), { sticky: false })
|
| 637 |
+
}
|
| 638 |
+
},
|
| 639 |
+
})
|
| 640 |
+
layerGroup.addLayer(geojsonLayer)
|
| 641 |
+
|
| 642 |
+
if (spec?.geojson_data) {
|
| 643 |
+
geojsonLayer.addData(spec.geojson_data)
|
| 644 |
+
return
|
| 645 |
+
}
|
| 646 |
+
if (!spec?.geojson_url) return
|
| 647 |
+
void carregarBairrosGeojson(spec.geojson_url)
|
| 648 |
+
.then((geojson) => {
|
| 649 |
+
if (disposed || !geojson) return
|
| 650 |
+
geojsonLayer.addData(geojson)
|
| 651 |
+
})
|
| 652 |
+
.catch(() => {
|
| 653 |
+
// camada opcional; manter o mapa funcional sem ela
|
| 654 |
+
})
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
function addHeatmapOverlay(layerGroup, heatmapSpec) {
|
| 658 |
+
if (!heatmapSpec || typeof L.heatLayer !== 'function') return
|
| 659 |
+
const points = Array.isArray(heatmapSpec?.points)
|
| 660 |
+
? heatmapSpec.points
|
| 661 |
+
.map((item) => {
|
| 662 |
+
const lat = Number(item?.lat)
|
| 663 |
+
const lon = Number(item?.lon)
|
| 664 |
+
const weight = Number(item?.weight)
|
| 665 |
+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null
|
| 666 |
+
if (Number.isFinite(weight)) return [lat, lon, weight]
|
| 667 |
+
return [lat, lon]
|
| 668 |
+
})
|
| 669 |
+
.filter(Boolean)
|
| 670 |
+
: []
|
| 671 |
+
if (!points.length) return
|
| 672 |
+
const layer = L.heatLayer(points, {
|
| 673 |
+
radius: Number(heatmapSpec?.radius) || 20,
|
| 674 |
+
blur: Number(heatmapSpec?.blur) || 18,
|
| 675 |
+
minOpacity: Number.isFinite(Number(heatmapSpec?.min_opacity)) ? Number(heatmapSpec.min_opacity) : 0.28,
|
| 676 |
+
maxZoom: Number(heatmapSpec?.max_zoom) || 17,
|
| 677 |
+
gradient: heatmapSpec?.gradient || undefined,
|
| 678 |
+
})
|
| 679 |
+
layerGroup.addLayer(layer)
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
overlaySpecs.forEach((spec, index) => {
|
| 683 |
+
const label = String(spec?.label || spec?.id || `Camada ${index + 1}`)
|
| 684 |
+
const layerGroup = L.layerGroup()
|
| 685 |
+
overlayLayers[label] = layerGroup
|
| 686 |
+
if (spec?.show !== false) {
|
| 687 |
+
layerGroup.addTo(map)
|
| 688 |
+
}
|
| 689 |
+
if (spec?.geojson_url || spec?.geojson_data) {
|
| 690 |
+
addGeoJsonOverlay(layerGroup, spec)
|
| 691 |
+
}
|
| 692 |
+
addHeatmapOverlay(layerGroup, spec?.heatmap)
|
| 693 |
+
addShapeOverlays(layerGroup, spec?.shapes)
|
| 694 |
+
addPointMarkers(layerGroup, spec?.points)
|
| 695 |
+
addMarkerOverlays(layerGroup, spec?.markers)
|
| 696 |
+
})
|
| 697 |
+
|
| 698 |
+
if (payload.controls?.layer_control) {
|
| 699 |
+
L.control.layers(baseLayers, overlayLayers, { collapsed: true }).addTo(map)
|
| 700 |
+
}
|
| 701 |
+
if (payload.controls?.fullscreen && typeof L.control.fullscreen === 'function') {
|
| 702 |
+
L.control.fullscreen({ position: 'topleft' }).addTo(map)
|
| 703 |
+
}
|
| 704 |
+
if (payload.controls?.measure) {
|
| 705 |
+
const disableInteractionsForMeasure = () => {
|
| 706 |
+
if (restoreMapInteractions) return
|
| 707 |
+
const states = {
|
| 708 |
+
dragging: !!map.dragging?.enabled?.(),
|
| 709 |
+
doubleClickZoom: !!map.doubleClickZoom?.enabled?.(),
|
| 710 |
+
boxZoom: !!map.boxZoom?.enabled?.(),
|
| 711 |
+
keyboard: !!map.keyboard?.enabled?.(),
|
| 712 |
+
touchZoom: !!map.touchZoom?.enabled?.(),
|
| 713 |
+
scrollWheelZoom: !!map.scrollWheelZoom?.enabled?.(),
|
| 714 |
+
}
|
| 715 |
+
map.dragging?.disable?.()
|
| 716 |
+
map.doubleClickZoom?.disable?.()
|
| 717 |
+
map.boxZoom?.disable?.()
|
| 718 |
+
map.keyboard?.disable?.()
|
| 719 |
+
map.touchZoom?.disable?.()
|
| 720 |
+
map.scrollWheelZoom?.disable?.()
|
| 721 |
+
if (map.getContainer()) {
|
| 722 |
+
map.getContainer().style.cursor = 'crosshair'
|
| 723 |
+
}
|
| 724 |
+
restoreMapInteractions = () => {
|
| 725 |
+
if (states.dragging) map.dragging?.enable?.()
|
| 726 |
+
if (states.doubleClickZoom) map.doubleClickZoom?.enable?.()
|
| 727 |
+
if (states.boxZoom) map.boxZoom?.enable?.()
|
| 728 |
+
if (states.keyboard) map.keyboard?.enable?.()
|
| 729 |
+
if (states.touchZoom) map.touchZoom?.enable?.()
|
| 730 |
+
if (states.scrollWheelZoom) map.scrollWheelZoom?.enable?.()
|
| 731 |
+
if (map.getContainer()) {
|
| 732 |
+
map.getContainer().style.cursor = ''
|
| 733 |
+
}
|
| 734 |
+
restoreMapInteractions = null
|
| 735 |
+
}
|
| 736 |
+
}
|
| 737 |
+
const measureLayerGroup = L.layerGroup().addTo(map)
|
| 738 |
+
const measureState = {
|
| 739 |
+
panelOpen: false,
|
| 740 |
+
active: false,
|
| 741 |
+
points: [],
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
let measureRoot = null
|
| 745 |
+
let measureInteraction = null
|
| 746 |
+
let measureHint = null
|
| 747 |
+
let measureResults = null
|
| 748 |
+
let measureStartButton = null
|
| 749 |
+
let measureCancelButton = null
|
| 750 |
+
let measureFinishButton = null
|
| 751 |
+
let measureCloseButton = null
|
| 752 |
+
|
| 753 |
+
function renderMeasureGeometry() {
|
| 754 |
+
measureLayerGroup.clearLayers()
|
| 755 |
+
if (!measureState.points.length) return
|
| 756 |
+
|
| 757 |
+
if (measureState.points.length >= 2) {
|
| 758 |
+
L.polyline(measureState.points, {
|
| 759 |
+
color: '#1f6fb2',
|
| 760 |
+
weight: 3,
|
| 761 |
+
opacity: 0.9,
|
| 762 |
+
dashArray: measureState.active ? '8 6' : undefined,
|
| 763 |
+
}).addTo(measureLayerGroup)
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
if (!measureState.active && measureState.points.length >= 3) {
|
| 767 |
+
L.polygon(measureState.points, {
|
| 768 |
+
color: '#1f6fb2',
|
| 769 |
+
weight: 2,
|
| 770 |
+
opacity: 0.88,
|
| 771 |
+
fillColor: '#1f6fb2',
|
| 772 |
+
fillOpacity: 0.12,
|
| 773 |
+
}).addTo(measureLayerGroup)
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
measureState.points.forEach((latlng) => {
|
| 777 |
+
L.circleMarker(latlng, {
|
| 778 |
+
radius: 5,
|
| 779 |
+
color: '#ffffff',
|
| 780 |
+
weight: 2,
|
| 781 |
+
fillColor: '#1f6fb2',
|
| 782 |
+
fillOpacity: 1,
|
| 783 |
+
pane: 'mesa-indices-pane',
|
| 784 |
+
}).addTo(measureLayerGroup)
|
| 785 |
+
})
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function renderMeasurePanel() {
|
| 789 |
+
if (!measureRoot || !measureInteraction || !measureHint || !measureResults) return
|
| 790 |
+
measureRoot.classList.toggle('leaflet-control-measure-expanded', measureState.panelOpen)
|
| 791 |
+
measureInteraction.style.display = measureState.panelOpen ? 'block' : 'none'
|
| 792 |
+
if (!measureState.panelOpen) return
|
| 793 |
+
|
| 794 |
+
const pointCount = measureState.points.length
|
| 795 |
+
const lastPoint = pointCount ? measureState.points[pointCount - 1] : null
|
| 796 |
+
const distance = pointCount >= 2
|
| 797 |
+
? measureState.points.reduce((total, point, index, list) => {
|
| 798 |
+
if (index === 0) return total
|
| 799 |
+
return total + map.distance(list[index - 1], point)
|
| 800 |
+
}, 0)
|
| 801 |
+
: 0
|
| 802 |
+
const area = !measureState.active && pointCount >= 3 ? computePolygonArea(measureState.points) : 0
|
| 803 |
+
|
| 804 |
+
if (measureState.active) {
|
| 805 |
+
measureHint.textContent = pointCount
|
| 806 |
+
? 'Clique no mapa para adicionar pontos. Dê duplo clique ou finalize para encerrar.'
|
| 807 |
+
: 'Clique no mapa para iniciar a medição.'
|
| 808 |
+
} else if (pointCount) {
|
| 809 |
+
measureHint.textContent = 'Medição concluída.'
|
| 810 |
+
} else {
|
| 811 |
+
measureHint.textContent = 'Inicie uma nova medição.'
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
const parts = []
|
| 815 |
+
if (lastPoint) {
|
| 816 |
+
parts.push(
|
| 817 |
+
'<div class="mesa-leaflet-measure-row">'
|
| 818 |
+
+ '<strong>Último ponto</strong>'
|
| 819 |
+
+ `<span>${escapeHtml(formatLatLng(lastPoint))}</span>`
|
| 820 |
+
+ `<span>${escapeHtml(formatDecimalLatLng(lastPoint))}</span>`
|
| 821 |
+
+ '</div>',
|
| 822 |
+
)
|
| 823 |
+
}
|
| 824 |
+
if (distance > 0) {
|
| 825 |
+
parts.push(
|
| 826 |
+
'<div class="mesa-leaflet-measure-row">'
|
| 827 |
+
+ '<strong>Distância total</strong>'
|
| 828 |
+
+ `<span>${escapeHtml(formatDistance(distance))}</span>`
|
| 829 |
+
+ '</div>',
|
| 830 |
+
)
|
| 831 |
+
}
|
| 832 |
+
if (area > 0) {
|
| 833 |
+
parts.push(
|
| 834 |
+
'<div class="mesa-leaflet-measure-row">'
|
| 835 |
+
+ '<strong>Área</strong>'
|
| 836 |
+
+ `<span>${escapeHtml(formatArea(area))}</span>`
|
| 837 |
+
+ '</div>',
|
| 838 |
+
)
|
| 839 |
+
}
|
| 840 |
+
measureResults.innerHTML = parts.join('')
|
| 841 |
+
measureResults.style.display = parts.length ? 'block' : 'none'
|
| 842 |
+
measureStartButton.style.display = !measureState.active ? 'inline-flex' : 'none'
|
| 843 |
+
measureStartButton.textContent = measureState.points.length ? 'Nova medição' : 'Criar nova medição'
|
| 844 |
+
measureCancelButton.style.display = measureState.active || measureState.points.length ? 'inline-flex' : 'none'
|
| 845 |
+
measureFinishButton.style.display = measureState.active && measureState.points.length >= 2 ? 'inline-flex' : 'none'
|
| 846 |
+
measureCloseButton.style.display = !measureState.active ? 'inline-flex' : 'none'
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
function resetMeasureState({ closePanel = false } = {}) {
|
| 850 |
+
measureState.active = false
|
| 851 |
+
measureState.points = []
|
| 852 |
+
measureLayerGroup.clearLayers()
|
| 853 |
+
restoreMapInteractions?.()
|
| 854 |
+
if (closePanel) {
|
| 855 |
+
measureState.panelOpen = false
|
| 856 |
+
}
|
| 857 |
+
renderMeasurePanel()
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
function startMeasurement() {
|
| 861 |
+
measureState.panelOpen = true
|
| 862 |
+
measureState.active = true
|
| 863 |
+
measureState.points = []
|
| 864 |
+
measureLayerGroup.clearLayers()
|
| 865 |
+
disableInteractionsForMeasure()
|
| 866 |
+
renderMeasurePanel()
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
function finishMeasurement() {
|
| 870 |
+
if (!measureState.active) return
|
| 871 |
+
measureState.active = false
|
| 872 |
+
restoreMapInteractions?.()
|
| 873 |
+
renderMeasureGeometry()
|
| 874 |
+
renderMeasurePanel()
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
function onMeasureMapClick(event) {
|
| 878 |
+
if (!measureState.active || !event?.latlng) return
|
| 879 |
+
const lastPoint = measureState.points[measureState.points.length - 1]
|
| 880 |
+
if (lastPoint && typeof event.latlng.equals === 'function' && event.latlng.equals(lastPoint)) return
|
| 881 |
+
measureState.points = [...measureState.points, event.latlng]
|
| 882 |
+
renderMeasureGeometry()
|
| 883 |
+
renderMeasurePanel()
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
function onMeasureMapDblClick(event) {
|
| 887 |
+
if (!measureState.active) return
|
| 888 |
+
L.DomEvent.stop(event)
|
| 889 |
+
finishMeasurement()
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
map.on('click', onMeasureMapClick)
|
| 893 |
+
map.on('dblclick', onMeasureMapDblClick)
|
| 894 |
+
|
| 895 |
+
const measureControl = L.control({ position: 'topright' })
|
| 896 |
+
measureControl.onAdd = () => {
|
| 897 |
+
measureRoot = L.DomUtil.create('div', 'leaflet-control-measure mesa-leaflet-measure leaflet-bar leaflet-control')
|
| 898 |
+
measureRoot.setAttribute('aria-haspopup', 'true')
|
| 899 |
+
|
| 900 |
+
const toggle = L.DomUtil.create('a', 'leaflet-control-measure-toggle mesa-leaflet-measure-toggle', measureRoot)
|
| 901 |
+
toggle.href = '#'
|
| 902 |
+
toggle.title = 'Medir distâncias'
|
| 903 |
+
toggle.setAttribute('aria-label', 'Medir distâncias')
|
| 904 |
+
toggle.innerHTML = '<span class="mesa-sr-only">Medir distâncias</span>'
|
| 905 |
+
|
| 906 |
+
measureInteraction = L.DomUtil.create('div', 'leaflet-control-measure-interaction mesa-leaflet-measure-interaction', measureRoot)
|
| 907 |
+
measureInteraction.style.display = 'none'
|
| 908 |
+
|
| 909 |
+
const title = L.DomUtil.create('h3', 'mesa-leaflet-measure-title', measureInteraction)
|
| 910 |
+
title.textContent = 'Medir distâncias'
|
| 911 |
+
|
| 912 |
+
measureHint = L.DomUtil.create('p', 'mesa-leaflet-measure-hint', measureInteraction)
|
| 913 |
+
measureResults = L.DomUtil.create('div', 'mesa-leaflet-measure-results', measureInteraction)
|
| 914 |
+
|
| 915 |
+
const actions = L.DomUtil.create('div', 'mesa-leaflet-measure-actions', measureInteraction)
|
| 916 |
+
measureStartButton = L.DomUtil.create('button', 'mesa-leaflet-measure-btn is-primary', actions)
|
| 917 |
+
measureStartButton.type = 'button'
|
| 918 |
+
measureStartButton.textContent = 'Criar nova medição'
|
| 919 |
+
|
| 920 |
+
measureCancelButton = L.DomUtil.create('button', 'mesa-leaflet-measure-btn', actions)
|
| 921 |
+
measureCancelButton.type = 'button'
|
| 922 |
+
measureCancelButton.textContent = 'Cancelar'
|
| 923 |
+
|
| 924 |
+
measureFinishButton = L.DomUtil.create('button', 'mesa-leaflet-measure-btn', actions)
|
| 925 |
+
measureFinishButton.type = 'button'
|
| 926 |
+
measureFinishButton.textContent = 'Finalizar'
|
| 927 |
+
|
| 928 |
+
measureCloseButton = L.DomUtil.create('button', 'mesa-leaflet-measure-btn', actions)
|
| 929 |
+
measureCloseButton.type = 'button'
|
| 930 |
+
measureCloseButton.textContent = 'Fechar'
|
| 931 |
+
|
| 932 |
+
L.DomEvent.disableClickPropagation(measureRoot)
|
| 933 |
+
L.DomEvent.disableScrollPropagation(measureRoot)
|
| 934 |
+
L.DomEvent.on(toggle, 'click', (event) => {
|
| 935 |
+
L.DomEvent.stop(event)
|
| 936 |
+
if (measureState.active) return
|
| 937 |
+
measureState.panelOpen = !measureState.panelOpen
|
| 938 |
+
renderMeasurePanel()
|
| 939 |
+
})
|
| 940 |
+
L.DomEvent.on(measureStartButton, 'click', (event) => {
|
| 941 |
+
L.DomEvent.stop(event)
|
| 942 |
+
startMeasurement()
|
| 943 |
+
})
|
| 944 |
+
L.DomEvent.on(measureCancelButton, 'click', (event) => {
|
| 945 |
+
L.DomEvent.stop(event)
|
| 946 |
+
resetMeasureState()
|
| 947 |
+
})
|
| 948 |
+
L.DomEvent.on(measureFinishButton, 'click', (event) => {
|
| 949 |
+
L.DomEvent.stop(event)
|
| 950 |
+
finishMeasurement()
|
| 951 |
+
})
|
| 952 |
+
L.DomEvent.on(measureCloseButton, 'click', (event) => {
|
| 953 |
+
L.DomEvent.stop(event)
|
| 954 |
+
if (measureState.active) return
|
| 955 |
+
measureState.panelOpen = false
|
| 956 |
+
renderMeasurePanel()
|
| 957 |
+
})
|
| 958 |
+
|
| 959 |
+
renderMeasurePanel()
|
| 960 |
+
return measureRoot
|
| 961 |
+
}
|
| 962 |
+
measureControl.addTo(map)
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
if (payload.legend?.title) {
|
| 966 |
+
const legendControl = construirLegendaControl(payload.legend)
|
| 967 |
+
if (legendControl) {
|
| 968 |
+
legendControl.onAdd = () => {
|
| 969 |
+
const div = L.DomUtil.create('div', 'mesa-leaflet-legend')
|
| 970 |
+
div.innerHTML = buildLegendHtml(payload.legend)
|
| 971 |
+
return div
|
| 972 |
+
}
|
| 973 |
+
legendControl.addTo(map)
|
| 974 |
+
}
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
addNoticeControl(map, payload.notice)
|
| 978 |
+
|
| 979 |
+
map.on('zoomend overlayadd overlayremove', applyResponsiveRadius)
|
| 980 |
+
applyResponsiveRadius()
|
| 981 |
+
|
| 982 |
+
const bounds = Array.isArray(payload.bounds) ? payload.bounds : null
|
| 983 |
+
if (bounds && bounds.length === 2) {
|
| 984 |
+
map.fitBounds(bounds, { padding: [48, 48], maxZoom: 18, animate: false })
|
| 985 |
+
} else if (Array.isArray(payload.center) && payload.center.length === 2) {
|
| 986 |
+
map.setView(payload.center, 12)
|
| 987 |
+
} else {
|
| 988 |
+
setRuntimeError('Falha ao montar mapa interativo.')
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
return () => {
|
| 992 |
+
disposed = true
|
| 993 |
+
restoreMapInteractions?.()
|
| 994 |
+
map.remove()
|
| 995 |
+
}
|
| 996 |
+
}, [payload, sessionId])
|
| 997 |
+
|
| 998 |
+
return (
|
| 999 |
+
<div className="map-frame leaflet-map-host">
|
| 1000 |
+
<div ref={hostRef} className="leaflet-map-canvas" />
|
| 1001 |
+
{runtimeError ? <div className="leaflet-map-runtime-error">{runtimeError}</div> : null}
|
| 1002 |
+
</div>
|
| 1003 |
+
)
|
| 1004 |
+
}
|
frontend/src/components/MapFrame.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
|
|
| 2 |
|
| 3 |
function hashHtml(value) {
|
| 4 |
let hash = 0
|
|
@@ -10,7 +11,7 @@ function hashHtml(value) {
|
|
| 10 |
return `${value.length}-${Math.abs(hash)}`
|
| 11 |
}
|
| 12 |
|
| 13 |
-
export default function MapFrame({ html }) {
|
| 14 |
const iframeRef = useRef(null)
|
| 15 |
const timersRef = useRef([])
|
| 16 |
const frameKey = useMemo(() => hashHtml(html || ''), [html])
|
|
@@ -129,6 +130,10 @@ export default function MapFrame({ html }) {
|
|
| 129 |
}
|
| 130 |
}, [clearTimers])
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
if (!html) {
|
| 133 |
return <div className="empty-box">Mapa indisponivel.</div>
|
| 134 |
}
|
|
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
| 2 |
+
import LeafletMapFrame from './LeafletMapFrame'
|
| 3 |
|
| 4 |
function hashHtml(value) {
|
| 5 |
let hash = 0
|
|
|
|
| 11 |
return `${value.length}-${Math.abs(hash)}`
|
| 12 |
}
|
| 13 |
|
| 14 |
+
export default function MapFrame({ html, payload = null, sessionId = '' }) {
|
| 15 |
const iframeRef = useRef(null)
|
| 16 |
const timersRef = useRef([])
|
| 17 |
const frameKey = useMemo(() => hashHtml(html || ''), [html])
|
|
|
|
| 130 |
}
|
| 131 |
}, [clearTimers])
|
| 132 |
|
| 133 |
+
if (payload && payload.type === 'mesa_leaflet_payload') {
|
| 134 |
+
return <LeafletMapFrame payload={payload} sessionId={sessionId} />
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
if (!html) {
|
| 138 |
return <div className="empty-box">Mapa indisponivel.</div>
|
| 139 |
}
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -980,6 +980,7 @@ export default function PesquisaTab({
|
|
| 980 |
const [mapaError, setMapaError] = useState('')
|
| 981 |
const [mapaStatus, setMapaStatus] = useState('')
|
| 982 |
const [mapaHtmls, setMapaHtmls] = useState({ pontos: '', cobertura: '' })
|
|
|
|
| 983 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('pontos')
|
| 984 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState('selecionados_e_outras_versoes')
|
| 985 |
const [mapaTrabalhosTecnicosProximidadeModo, setMapaTrabalhosTecnicosProximidadeModo] = useState('sem_proximidade')
|
|
@@ -999,6 +1000,7 @@ export default function PesquisaTab({
|
|
| 999 |
const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null)
|
| 1000 |
const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null)
|
| 1001 |
const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('')
|
|
|
|
| 1002 |
const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
|
| 1003 |
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
|
| 1004 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
@@ -1008,6 +1010,8 @@ export default function PesquisaTab({
|
|
| 1008 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 1009 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 1010 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
|
|
|
|
|
|
| 1011 |
const sectionResultadosRef = useRef(null)
|
| 1012 |
const sectionMapaRef = useRef(null)
|
| 1013 |
const scrollMapaHandledRef = useRef('')
|
|
@@ -1015,6 +1019,8 @@ export default function PesquisaTab({
|
|
| 1015 |
const resultRequestSeqRef = useRef(0)
|
| 1016 |
const searchRequestSeqRef = useRef(0)
|
| 1017 |
const contextRequestSeqRef = useRef(0)
|
|
|
|
|
|
|
| 1018 |
|
| 1019 |
const sugestoes = result.sugestoes || {}
|
| 1020 |
const opcoesTipoModelo = useMemo(
|
|
@@ -1038,7 +1044,8 @@ export default function PesquisaTab({
|
|
| 1038 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 1039 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 1040 |
const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
|
| 1041 |
-
const
|
|
|
|
| 1042 |
const pesquisaShareAvaliando = !localizacaoMultipla ? (avaliandosGeoPayload[0] || null) : null
|
| 1043 |
const pesquisaShareHref = buildPesquisaLink(filters, pesquisaShareAvaliando)
|
| 1044 |
const pesquisaShareDisabled = localizacaoMultipla
|
|
@@ -1089,6 +1096,7 @@ export default function PesquisaTab({
|
|
| 1089 |
|
| 1090 |
function resetMapaPesquisa() {
|
| 1091 |
setMapaHtmls({ pontos: '', cobertura: '' })
|
|
|
|
| 1092 |
setMapaStatus('')
|
| 1093 |
setMapaError('')
|
| 1094 |
setMapaModoExibicao('pontos')
|
|
@@ -1126,6 +1134,7 @@ export default function PesquisaTab({
|
|
| 1126 |
|
| 1127 |
if (!idsValidos.length) {
|
| 1128 |
setMapaHtmls({ pontos: '', cobertura: '' })
|
|
|
|
| 1129 |
setMapaStatus('Nenhum modelo marcado para exibição no mapa.')
|
| 1130 |
setMapaError('')
|
| 1131 |
mapaTrabalhosTecnicosConfigRef.current = ''
|
|
@@ -1149,10 +1158,17 @@ export default function PesquisaTab({
|
|
| 1149 |
|| (modoExibicaoSolicitado === 'cobertura' ? response.mapa_html_cobertura : response.mapa_html_pontos)
|
| 1150 |
|| '',
|
| 1151 |
)
|
|
|
|
|
|
|
|
|
|
| 1152 |
setMapaHtmls({
|
| 1153 |
pontos: modoExibicaoSolicitado === 'pontos' ? mapaHtmlSolicitado : '',
|
| 1154 |
cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaHtmlSolicitado : '',
|
| 1155 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1156 |
setMapaStatus(response.status || '')
|
| 1157 |
mapaTrabalhosTecnicosConfigRef.current = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
|
| 1158 |
} catch (err) {
|
|
@@ -1422,25 +1438,125 @@ export default function PesquisaTab({
|
|
| 1422 |
}
|
| 1423 |
}
|
| 1424 |
|
| 1425 |
-
function
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
|
|
|
|
|
|
| 1436 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 1437 |
-
setModeloAbertoTrabalhosTecnicosModelosModo(
|
| 1438 |
-
setModeloAbertoTrabalhosTecnicos(
|
| 1439 |
-
setModeloAbertoPlotObsCalc(
|
| 1440 |
-
setModeloAbertoPlotResiduos(
|
| 1441 |
-
setModeloAbertoPlotHistograma(
|
| 1442 |
-
setModeloAbertoPlotCook(
|
| 1443 |
-
setModeloAbertoPlotCorr(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1444 |
}
|
| 1445 |
|
| 1446 |
async function onAbrirModelo(modelo) {
|
|
@@ -1456,17 +1572,29 @@ export default function PesquisaTab({
|
|
| 1456 |
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
|
| 1457 |
return
|
| 1458 |
}
|
|
|
|
|
|
|
|
|
|
| 1459 |
setModeloAbertoLoading(true)
|
| 1460 |
setModeloAbertoError('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1461 |
try {
|
| 1462 |
-
await api.visualizacaoRepositorioCarregar(sessionId, modelo.id)
|
| 1463 |
-
|
| 1464 |
-
preencherModeloAberto(resp)
|
| 1465 |
setModeloAbertoActiveTab('mapa')
|
| 1466 |
setModeloAbertoMeta({
|
| 1467 |
id: modelo.id,
|
| 1468 |
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
|
| 1469 |
-
observacao: String(resp?.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1470 |
})
|
| 1471 |
window.requestAnimationFrame(() => {
|
| 1472 |
scrollParaTopoDaPagina()
|
|
@@ -1479,9 +1607,12 @@ export default function PesquisaTab({
|
|
| 1479 |
}
|
| 1480 |
|
| 1481 |
function onVoltarPesquisa() {
|
|
|
|
| 1482 |
setModeloAbertoMeta(null)
|
| 1483 |
setModeloAbertoError('')
|
| 1484 |
setModeloAbertoActiveTab('mapa')
|
|
|
|
|
|
|
| 1485 |
scrollParaResultadosNoTopo()
|
| 1486 |
}
|
| 1487 |
|
|
@@ -1490,10 +1621,17 @@ export default function PesquisaTab({
|
|
| 1490 |
nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
|
| 1491 |
) {
|
| 1492 |
if (!sessionId) return
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1497 |
}
|
| 1498 |
|
| 1499 |
async function onModeloAbertoMapChange(nextVar) {
|
|
@@ -1528,6 +1666,11 @@ export default function PesquisaTab({
|
|
| 1528 |
}
|
| 1529 |
}
|
| 1530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1531 |
async function onGerarMapaSelecionados() {
|
| 1532 |
if (!selectedIds.length) {
|
| 1533 |
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
|
|
@@ -1540,7 +1683,7 @@ export default function PesquisaTab({
|
|
| 1540 |
function onMapaModoExibicaoChange(event) {
|
| 1541 |
const nextModo = String(event?.target?.value || 'pontos')
|
| 1542 |
setMapaModoExibicao(nextModo)
|
| 1543 |
-
if (!mapaFoiGerado || mapaLoading || !selectedIds.length || mapaHtmls[nextModo]) return
|
| 1544 |
void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
|
| 1545 |
}
|
| 1546 |
|
|
@@ -1657,7 +1800,7 @@ export default function PesquisaTab({
|
|
| 1657 |
key={tab.key}
|
| 1658 |
type="button"
|
| 1659 |
className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 1660 |
-
onClick={() =>
|
| 1661 |
>
|
| 1662 |
{tab.label}
|
| 1663 |
</button>
|
|
@@ -1666,7 +1809,10 @@ export default function PesquisaTab({
|
|
| 1666 |
|
| 1667 |
<div className="inner-tab-panel">
|
| 1668 |
{modeloAbertoActiveTab === 'mapa' ? (
|
| 1669 |
-
|
|
|
|
|
|
|
|
|
|
| 1670 |
<div className="row compact visualizacao-mapa-controls pesquisa-mapa-controls-row">
|
| 1671 |
<label className="pesquisa-field pesquisa-mapa-modo-field">
|
| 1672 |
Variável no mapa
|
|
@@ -1694,27 +1840,39 @@ export default function PesquisaTab({
|
|
| 1694 |
</select>
|
| 1695 |
</label>
|
| 1696 |
</div>
|
| 1697 |
-
<MapFrame html={modeloAbertoMapaHtml} />
|
| 1698 |
</>
|
|
|
|
| 1699 |
) : null}
|
| 1700 |
|
| 1701 |
{modeloAbertoActiveTab === 'trabalhos_tecnicos' ? (
|
| 1702 |
-
|
|
|
|
|
|
|
| 1703 |
) : null}
|
| 1704 |
|
| 1705 |
-
{modeloAbertoActiveTab === 'dados_mercado'
|
| 1706 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1707 |
|
| 1708 |
{modeloAbertoActiveTab === 'transformacoes' ? (
|
| 1709 |
-
|
|
|
|
| 1710 |
<div dangerouslySetInnerHTML={{ __html: modeloAbertoEscalasHtml }} />
|
| 1711 |
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 1712 |
<DataTable table={modeloAbertoDadosTransformados} />
|
| 1713 |
</>
|
|
|
|
|
|
|
|
|
|
| 1714 |
) : null}
|
| 1715 |
|
| 1716 |
{modeloAbertoActiveTab === 'resumo' ? (
|
| 1717 |
-
|
|
|
|
| 1718 |
<div className="equation-formats-section">
|
| 1719 |
<h4>Equações do Modelo</h4>
|
| 1720 |
<EquationFormatsPanel
|
|
@@ -1725,13 +1883,21 @@ export default function PesquisaTab({
|
|
| 1725 |
</div>
|
| 1726 |
<div dangerouslySetInnerHTML={{ __html: modeloAbertoResumoHtml }} />
|
| 1727 |
</>
|
|
|
|
|
|
|
|
|
|
| 1728 |
) : null}
|
| 1729 |
|
| 1730 |
-
{modeloAbertoActiveTab === 'coeficientes'
|
| 1731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
|
| 1733 |
{modeloAbertoActiveTab === 'graficos' ? (
|
| 1734 |
-
|
|
|
|
| 1735 |
<div className="plot-grid-2-fixed">
|
| 1736 |
<PlotFigure figure={modeloAbertoPlotObsCalc} title="Obs x Calc" />
|
| 1737 |
<PlotFigure figure={modeloAbertoPlotResiduos} title="Resíduos" />
|
|
@@ -1742,12 +1908,18 @@ export default function PesquisaTab({
|
|
| 1742 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
| 1743 |
</div>
|
| 1744 |
</>
|
|
|
|
|
|
|
|
|
|
| 1745 |
) : null}
|
| 1746 |
</div>
|
| 1747 |
|
| 1748 |
{modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null}
|
| 1749 |
</div>
|
| 1750 |
-
<LoadingOverlay
|
|
|
|
|
|
|
|
|
|
| 1751 |
</div>
|
| 1752 |
)
|
| 1753 |
}
|
|
@@ -2291,7 +2463,9 @@ export default function PesquisaTab({
|
|
| 2291 |
</div>
|
| 2292 |
) : null}
|
| 2293 |
|
| 2294 |
-
{mapaHtmlAtual
|
|
|
|
|
|
|
| 2295 |
</SectionBlock>
|
| 2296 |
</div>
|
| 2297 |
|
|
|
|
| 980 |
const [mapaError, setMapaError] = useState('')
|
| 981 |
const [mapaStatus, setMapaStatus] = useState('')
|
| 982 |
const [mapaHtmls, setMapaHtmls] = useState({ pontos: '', cobertura: '' })
|
| 983 |
+
const [mapaPayloads, setMapaPayloads] = useState({ pontos: null, cobertura: null })
|
| 984 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('pontos')
|
| 985 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState('selecionados_e_outras_versoes')
|
| 986 |
const [mapaTrabalhosTecnicosProximidadeModo, setMapaTrabalhosTecnicosProximidadeModo] = useState('sem_proximidade')
|
|
|
|
| 1000 |
const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null)
|
| 1001 |
const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null)
|
| 1002 |
const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('')
|
| 1003 |
+
const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
|
| 1004 |
const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
|
| 1005 |
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
|
| 1006 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
|
|
| 1010 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 1011 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 1012 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 1013 |
+
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 1014 |
+
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
| 1015 |
const sectionResultadosRef = useRef(null)
|
| 1016 |
const sectionMapaRef = useRef(null)
|
| 1017 |
const scrollMapaHandledRef = useRef('')
|
|
|
|
| 1019 |
const resultRequestSeqRef = useRef(0)
|
| 1020 |
const searchRequestSeqRef = useRef(0)
|
| 1021 |
const contextRequestSeqRef = useRef(0)
|
| 1022 |
+
const modeloAbertoPendingRequestsRef = useRef({})
|
| 1023 |
+
const modeloAbertoOpenVersionRef = useRef(0)
|
| 1024 |
|
| 1025 |
const sugestoes = result.sugestoes || {}
|
| 1026 |
const opcoesTipoModelo = useMemo(
|
|
|
|
| 1044 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 1045 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 1046 |
const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
|
| 1047 |
+
const mapaPayloadAtual = mapaPayloads[mapaModoExibicao] || null
|
| 1048 |
+
const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura || mapaPayloads.pontos || mapaPayloads.cobertura)
|
| 1049 |
const pesquisaShareAvaliando = !localizacaoMultipla ? (avaliandosGeoPayload[0] || null) : null
|
| 1050 |
const pesquisaShareHref = buildPesquisaLink(filters, pesquisaShareAvaliando)
|
| 1051 |
const pesquisaShareDisabled = localizacaoMultipla
|
|
|
|
| 1096 |
|
| 1097 |
function resetMapaPesquisa() {
|
| 1098 |
setMapaHtmls({ pontos: '', cobertura: '' })
|
| 1099 |
+
setMapaPayloads({ pontos: null, cobertura: null })
|
| 1100 |
setMapaStatus('')
|
| 1101 |
setMapaError('')
|
| 1102 |
setMapaModoExibicao('pontos')
|
|
|
|
| 1134 |
|
| 1135 |
if (!idsValidos.length) {
|
| 1136 |
setMapaHtmls({ pontos: '', cobertura: '' })
|
| 1137 |
+
setMapaPayloads({ pontos: null, cobertura: null })
|
| 1138 |
setMapaStatus('Nenhum modelo marcado para exibição no mapa.')
|
| 1139 |
setMapaError('')
|
| 1140 |
mapaTrabalhosTecnicosConfigRef.current = ''
|
|
|
|
| 1158 |
|| (modoExibicaoSolicitado === 'cobertura' ? response.mapa_html_cobertura : response.mapa_html_pontos)
|
| 1159 |
|| '',
|
| 1160 |
)
|
| 1161 |
+
const mapaPayloadSolicitado = response.mapa_payload
|
| 1162 |
+
|| (modoExibicaoSolicitado === 'cobertura' ? response.mapa_payload_cobertura : response.mapa_payload_pontos)
|
| 1163 |
+
|| null
|
| 1164 |
setMapaHtmls({
|
| 1165 |
pontos: modoExibicaoSolicitado === 'pontos' ? mapaHtmlSolicitado : '',
|
| 1166 |
cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaHtmlSolicitado : '',
|
| 1167 |
})
|
| 1168 |
+
setMapaPayloads({
|
| 1169 |
+
pontos: modoExibicaoSolicitado === 'pontos' ? mapaPayloadSolicitado : null,
|
| 1170 |
+
cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaPayloadSolicitado : null,
|
| 1171 |
+
})
|
| 1172 |
setMapaStatus(response.status || '')
|
| 1173 |
mapaTrabalhosTecnicosConfigRef.current = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
|
| 1174 |
} catch (err) {
|
|
|
|
| 1438 |
}
|
| 1439 |
}
|
| 1440 |
|
| 1441 |
+
function limparConteudoModeloAberto() {
|
| 1442 |
+
modeloAbertoPendingRequestsRef.current = {}
|
| 1443 |
+
setModeloAbertoDados(null)
|
| 1444 |
+
setModeloAbertoEstatisticas(null)
|
| 1445 |
+
setModeloAbertoEscalasHtml('')
|
| 1446 |
+
setModeloAbertoDadosTransformados(null)
|
| 1447 |
+
setModeloAbertoResumoHtml('')
|
| 1448 |
+
setModeloAbertoEquacoes(null)
|
| 1449 |
+
setModeloAbertoCoeficientes(null)
|
| 1450 |
+
setModeloAbertoObsCalc(null)
|
| 1451 |
+
setModeloAbertoMapaHtml('')
|
| 1452 |
+
setModeloAbertoMapaPayload(null)
|
| 1453 |
+
setModeloAbertoMapaChoices(['Visualização Padrão'])
|
| 1454 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 1455 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1456 |
+
setModeloAbertoTrabalhosTecnicos([])
|
| 1457 |
+
setModeloAbertoPlotObsCalc(null)
|
| 1458 |
+
setModeloAbertoPlotResiduos(null)
|
| 1459 |
+
setModeloAbertoPlotHistograma(null)
|
| 1460 |
+
setModeloAbertoPlotCook(null)
|
| 1461 |
+
setModeloAbertoPlotCorr(null)
|
| 1462 |
+
setModeloAbertoLoadedTabs({})
|
| 1463 |
+
setModeloAbertoLoadingTabs({})
|
| 1464 |
+
}
|
| 1465 |
+
|
| 1466 |
+
function aplicarSecaoModeloAberto(secao, resp) {
|
| 1467 |
+
const key = String(secao || '').trim()
|
| 1468 |
+
if (key === 'dados_mercado') {
|
| 1469 |
+
setModeloAbertoDados(resp?.dados || null)
|
| 1470 |
+
return
|
| 1471 |
+
}
|
| 1472 |
+
if (key === 'metricas') {
|
| 1473 |
+
setModeloAbertoEstatisticas(resp?.estatisticas || null)
|
| 1474 |
+
return
|
| 1475 |
+
}
|
| 1476 |
+
if (key === 'transformacoes') {
|
| 1477 |
+
setModeloAbertoEscalasHtml(resp?.escalas_html || '')
|
| 1478 |
+
setModeloAbertoDadosTransformados(resp?.dados_transformados || null)
|
| 1479 |
+
return
|
| 1480 |
+
}
|
| 1481 |
+
if (key === 'resumo') {
|
| 1482 |
+
setModeloAbertoResumoHtml(resp?.resumo_html || '')
|
| 1483 |
+
setModeloAbertoEquacoes(resp?.equacoes || null)
|
| 1484 |
+
return
|
| 1485 |
+
}
|
| 1486 |
+
if (key === 'coeficientes') {
|
| 1487 |
+
setModeloAbertoCoeficientes(resp?.coeficientes || null)
|
| 1488 |
+
return
|
| 1489 |
+
}
|
| 1490 |
+
if (key === 'obs_calc') {
|
| 1491 |
+
setModeloAbertoObsCalc(resp?.obs_calc || null)
|
| 1492 |
+
return
|
| 1493 |
+
}
|
| 1494 |
+
if (key === 'graficos') {
|
| 1495 |
+
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
| 1496 |
+
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
| 1497 |
+
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
| 1498 |
+
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
| 1499 |
+
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 1500 |
+
return
|
| 1501 |
+
}
|
| 1502 |
+
if (key === 'trabalhos_tecnicos') {
|
| 1503 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 1504 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1505 |
+
return
|
| 1506 |
+
}
|
| 1507 |
+
if (key === 'mapa') {
|
| 1508 |
+
const nextChoices = Array.isArray(resp?.mapa_choices) && resp.mapa_choices.length
|
| 1509 |
+
? resp.mapa_choices
|
| 1510 |
+
: ['Visualização Padrão']
|
| 1511 |
+
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 1512 |
+
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
|
| 1513 |
+
setModeloAbertoMapaChoices(nextChoices)
|
| 1514 |
+
setModeloAbertoMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
|
| 1515 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 1516 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1517 |
+
}
|
| 1518 |
+
}
|
| 1519 |
+
|
| 1520 |
+
async function garantirSecaoModeloAberto(secao, options = {}) {
|
| 1521 |
+
const secaoNormalizada = String(secao || '').trim()
|
| 1522 |
+
if (!sessionId || !secaoNormalizada) return
|
| 1523 |
+
if (!options.force && modeloAbertoLoadedTabs[secaoNormalizada]) return
|
| 1524 |
+
if (modeloAbertoPendingRequestsRef.current[secaoNormalizada]) {
|
| 1525 |
+
await modeloAbertoPendingRequestsRef.current[secaoNormalizada]
|
| 1526 |
+
return
|
| 1527 |
+
}
|
| 1528 |
+
|
| 1529 |
+
const expectedVersion = options.expectedVersion ?? modeloAbertoOpenVersionRef.current
|
| 1530 |
+
const trabalhosTecnicosModo = options.trabalhosTecnicosModelosModo || modeloAbertoTrabalhosTecnicosModelosModo
|
| 1531 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: true }))
|
| 1532 |
+
|
| 1533 |
+
const request = (async () => {
|
| 1534 |
+
try {
|
| 1535 |
+
const resp = await api.visualizacaoSection(sessionId, secaoNormalizada, trabalhosTecnicosModo)
|
| 1536 |
+
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
|
| 1537 |
+
aplicarSecaoModeloAberto(secaoNormalizada, resp)
|
| 1538 |
+
setModeloAbertoLoadedTabs((prev) => ({
|
| 1539 |
+
...prev,
|
| 1540 |
+
[secaoNormalizada]: true,
|
| 1541 |
+
...(secaoNormalizada === 'mapa' ? { trabalhos_tecnicos: true } : {}),
|
| 1542 |
+
}))
|
| 1543 |
+
} catch (err) {
|
| 1544 |
+
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
|
| 1545 |
+
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
| 1546 |
+
} finally {
|
| 1547 |
+
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
|
| 1548 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: false }))
|
| 1549 |
+
}
|
| 1550 |
+
})()
|
| 1551 |
+
|
| 1552 |
+
modeloAbertoPendingRequestsRef.current[secaoNormalizada] = request
|
| 1553 |
+
try {
|
| 1554 |
+
await request
|
| 1555 |
+
} finally {
|
| 1556 |
+
if (modeloAbertoPendingRequestsRef.current[secaoNormalizada] === request) {
|
| 1557 |
+
delete modeloAbertoPendingRequestsRef.current[secaoNormalizada]
|
| 1558 |
+
}
|
| 1559 |
+
}
|
| 1560 |
}
|
| 1561 |
|
| 1562 |
async function onAbrirModelo(modelo) {
|
|
|
|
| 1572 |
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
|
| 1573 |
return
|
| 1574 |
}
|
| 1575 |
+
modeloAbertoOpenVersionRef.current += 1
|
| 1576 |
+
const openVersion = modeloAbertoOpenVersionRef.current
|
| 1577 |
+
limparConteudoModeloAberto()
|
| 1578 |
setModeloAbertoLoading(true)
|
| 1579 |
setModeloAbertoError('')
|
| 1580 |
+
setModeloAbertoMeta({
|
| 1581 |
+
id: modelo.id,
|
| 1582 |
+
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
|
| 1583 |
+
observacao: '',
|
| 1584 |
+
})
|
| 1585 |
try {
|
| 1586 |
+
const resp = await api.visualizacaoRepositorioCarregar(sessionId, modelo.id)
|
| 1587 |
+
if (modeloAbertoOpenVersionRef.current !== openVersion) return
|
|
|
|
| 1588 |
setModeloAbertoActiveTab('mapa')
|
| 1589 |
setModeloAbertoMeta({
|
| 1590 |
id: modelo.id,
|
| 1591 |
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
|
| 1592 |
+
observacao: String(resp?.observacao_modelo || '').trim(),
|
| 1593 |
+
})
|
| 1594 |
+
await garantirSecaoModeloAberto('mapa', {
|
| 1595 |
+
force: true,
|
| 1596 |
+
expectedVersion: openVersion,
|
| 1597 |
+
trabalhosTecnicosModelosModo: MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO,
|
| 1598 |
})
|
| 1599 |
window.requestAnimationFrame(() => {
|
| 1600 |
scrollParaTopoDaPagina()
|
|
|
|
| 1607 |
}
|
| 1608 |
|
| 1609 |
function onVoltarPesquisa() {
|
| 1610 |
+
modeloAbertoOpenVersionRef.current += 1
|
| 1611 |
setModeloAbertoMeta(null)
|
| 1612 |
setModeloAbertoError('')
|
| 1613 |
setModeloAbertoActiveTab('mapa')
|
| 1614 |
+
setModeloAbertoLoading(false)
|
| 1615 |
+
limparConteudoModeloAberto()
|
| 1616 |
scrollParaResultadosNoTopo()
|
| 1617 |
}
|
| 1618 |
|
|
|
|
| 1621 |
nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
|
| 1622 |
) {
|
| 1623 |
if (!sessionId) return
|
| 1624 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
|
| 1625 |
+
try {
|
| 1626 |
+
const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
|
| 1627 |
+
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 1628 |
+
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
|
| 1629 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 1630 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || nextTrabalhosTecnicosModo)
|
| 1631 |
+
setModeloAbertoLoadedTabs((prev) => ({ ...prev, mapa: true, trabalhos_tecnicos: true }))
|
| 1632 |
+
} finally {
|
| 1633 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: false }))
|
| 1634 |
+
}
|
| 1635 |
}
|
| 1636 |
|
| 1637 |
async function onModeloAbertoMapChange(nextVar) {
|
|
|
|
| 1666 |
}
|
| 1667 |
}
|
| 1668 |
|
| 1669 |
+
function onModeloAbertoTabSelect(nextTab) {
|
| 1670 |
+
setModeloAbertoActiveTab(nextTab)
|
| 1671 |
+
void garantirSecaoModeloAberto(nextTab)
|
| 1672 |
+
}
|
| 1673 |
+
|
| 1674 |
async function onGerarMapaSelecionados() {
|
| 1675 |
if (!selectedIds.length) {
|
| 1676 |
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
|
|
|
|
| 1683 |
function onMapaModoExibicaoChange(event) {
|
| 1684 |
const nextModo = String(event?.target?.value || 'pontos')
|
| 1685 |
setMapaModoExibicao(nextModo)
|
| 1686 |
+
if (!mapaFoiGerado || mapaLoading || !selectedIds.length || mapaHtmls[nextModo] || mapaPayloads[nextModo]) return
|
| 1687 |
void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
|
| 1688 |
}
|
| 1689 |
|
|
|
|
| 1800 |
key={tab.key}
|
| 1801 |
type="button"
|
| 1802 |
className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 1803 |
+
onClick={() => onModeloAbertoTabSelect(tab.key)}
|
| 1804 |
>
|
| 1805 |
{tab.label}
|
| 1806 |
</button>
|
|
|
|
| 1809 |
|
| 1810 |
<div className="inner-tab-panel">
|
| 1811 |
{modeloAbertoActiveTab === 'mapa' ? (
|
| 1812 |
+
!modeloAbertoLoadedTabs.mapa ? (
|
| 1813 |
+
<div className="empty-box">Carregando mapa do modelo...</div>
|
| 1814 |
+
) : (
|
| 1815 |
+
<>
|
| 1816 |
<div className="row compact visualizacao-mapa-controls pesquisa-mapa-controls-row">
|
| 1817 |
<label className="pesquisa-field pesquisa-mapa-modo-field">
|
| 1818 |
Variável no mapa
|
|
|
|
| 1840 |
</select>
|
| 1841 |
</label>
|
| 1842 |
</div>
|
| 1843 |
+
<MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
|
| 1844 |
</>
|
| 1845 |
+
)
|
| 1846 |
) : null}
|
| 1847 |
|
| 1848 |
{modeloAbertoActiveTab === 'trabalhos_tecnicos' ? (
|
| 1849 |
+
modeloAbertoLoadedTabs.trabalhos_tecnicos
|
| 1850 |
+
? <ModeloTrabalhosTecnicosPanel trabalhos={modeloAbertoTrabalhosTecnicos} />
|
| 1851 |
+
: <div className="empty-box">Carregando trabalhos técnicos do modelo...</div>
|
| 1852 |
) : null}
|
| 1853 |
|
| 1854 |
+
{modeloAbertoActiveTab === 'dados_mercado'
|
| 1855 |
+
? (modeloAbertoLoadedTabs.dados_mercado ? <DataTable table={modeloAbertoDados} maxHeight={620} /> : <div className="empty-box">Carregando dados de mercado...</div>)
|
| 1856 |
+
: null}
|
| 1857 |
+
{modeloAbertoActiveTab === 'metricas'
|
| 1858 |
+
? (modeloAbertoLoadedTabs.metricas ? <DataTable table={modeloAbertoEstatisticas} maxHeight={620} /> : <div className="empty-box">Carregando métricas do modelo...</div>)
|
| 1859 |
+
: null}
|
| 1860 |
|
| 1861 |
{modeloAbertoActiveTab === 'transformacoes' ? (
|
| 1862 |
+
modeloAbertoLoadedTabs.transformacoes ? (
|
| 1863 |
+
<>
|
| 1864 |
<div dangerouslySetInnerHTML={{ __html: modeloAbertoEscalasHtml }} />
|
| 1865 |
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 1866 |
<DataTable table={modeloAbertoDadosTransformados} />
|
| 1867 |
</>
|
| 1868 |
+
) : (
|
| 1869 |
+
<div className="empty-box">Carregando transformações do modelo...</div>
|
| 1870 |
+
)
|
| 1871 |
) : null}
|
| 1872 |
|
| 1873 |
{modeloAbertoActiveTab === 'resumo' ? (
|
| 1874 |
+
modeloAbertoLoadedTabs.resumo ? (
|
| 1875 |
+
<>
|
| 1876 |
<div className="equation-formats-section">
|
| 1877 |
<h4>Equações do Modelo</h4>
|
| 1878 |
<EquationFormatsPanel
|
|
|
|
| 1883 |
</div>
|
| 1884 |
<div dangerouslySetInnerHTML={{ __html: modeloAbertoResumoHtml }} />
|
| 1885 |
</>
|
| 1886 |
+
) : (
|
| 1887 |
+
<div className="empty-box">Carregando resumo do modelo...</div>
|
| 1888 |
+
)
|
| 1889 |
) : null}
|
| 1890 |
|
| 1891 |
+
{modeloAbertoActiveTab === 'coeficientes'
|
| 1892 |
+
? (modeloAbertoLoadedTabs.coeficientes ? <DataTable table={modeloAbertoCoeficientes} maxHeight={620} /> : <div className="empty-box">Carregando coeficientes do modelo...</div>)
|
| 1893 |
+
: null}
|
| 1894 |
+
{modeloAbertoActiveTab === 'obs_calc'
|
| 1895 |
+
? (modeloAbertoLoadedTabs.obs_calc ? <DataTable table={modeloAbertoObsCalc} maxHeight={620} /> : <div className="empty-box">Carregando observados x calculados...</div>)
|
| 1896 |
+
: null}
|
| 1897 |
|
| 1898 |
{modeloAbertoActiveTab === 'graficos' ? (
|
| 1899 |
+
modeloAbertoLoadedTabs.graficos ? (
|
| 1900 |
+
<>
|
| 1901 |
<div className="plot-grid-2-fixed">
|
| 1902 |
<PlotFigure figure={modeloAbertoPlotObsCalc} title="Obs x Calc" />
|
| 1903 |
<PlotFigure figure={modeloAbertoPlotResiduos} title="Resíduos" />
|
|
|
|
| 1908 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
| 1909 |
</div>
|
| 1910 |
</>
|
| 1911 |
+
) : (
|
| 1912 |
+
<div className="empty-box">Carregando gráficos do modelo...</div>
|
| 1913 |
+
)
|
| 1914 |
) : null}
|
| 1915 |
</div>
|
| 1916 |
|
| 1917 |
{modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null}
|
| 1918 |
</div>
|
| 1919 |
+
<LoadingOverlay
|
| 1920 |
+
show={modeloAbertoLoading || Boolean(modeloAbertoLoadingTabs[modeloAbertoActiveTab])}
|
| 1921 |
+
label={modeloAbertoLoading ? 'Carregando modelo...' : 'Carregando seção do modelo...'}
|
| 1922 |
+
/>
|
| 1923 |
</div>
|
| 1924 |
)
|
| 1925 |
}
|
|
|
|
| 2463 |
</div>
|
| 2464 |
) : null}
|
| 2465 |
|
| 2466 |
+
{(mapaHtmlAtual || mapaPayloadAtual)
|
| 2467 |
+
? <MapFrame html={mapaHtmlAtual} payload={mapaPayloadAtual} sessionId={sessionId} />
|
| 2468 |
+
: <div className="empty-box">Nenhum mapa gerado ainda.</div>}
|
| 2469 |
</SectionBlock>
|
| 2470 |
</div>
|
| 2471 |
|
frontend/src/components/RepositorioTab.jsx
CHANGED
|
@@ -174,6 +174,7 @@ export default function RepositorioTab({
|
|
| 174 |
const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null)
|
| 175 |
const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null)
|
| 176 |
const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('')
|
|
|
|
| 177 |
const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
|
| 178 |
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
|
| 179 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
@@ -349,6 +350,7 @@ export default function RepositorioTab({
|
|
| 349 |
setModeloAbertoCoeficientes(null)
|
| 350 |
setModeloAbertoObsCalc(null)
|
| 351 |
setModeloAbertoMapaHtml('')
|
|
|
|
| 352 |
setModeloAbertoMapaChoices(['Visualização Padrão'])
|
| 353 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 354 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
@@ -408,6 +410,7 @@ export default function RepositorioTab({
|
|
| 408 |
? resp.mapa_choices
|
| 409 |
: ['Visualização Padrão']
|
| 410 |
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
|
|
|
| 411 |
setModeloAbertoMapaChoices(nextChoices)
|
| 412 |
setModeloAbertoMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
|
| 413 |
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
|
@@ -551,6 +554,7 @@ export default function RepositorioTab({
|
|
| 551 |
try {
|
| 552 |
const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
|
| 553 |
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
|
|
|
| 554 |
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 555 |
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || nextTrabalhosTecnicosModo)
|
| 556 |
setModeloAbertoLoadedTabs((prev) => ({ ...prev, mapa: true, trabalhos_tecnicos: true }))
|
|
@@ -692,7 +696,7 @@ export default function RepositorioTab({
|
|
| 692 |
</select>
|
| 693 |
</label>
|
| 694 |
</div>
|
| 695 |
-
<MapFrame html={modeloAbertoMapaHtml} />
|
| 696 |
</>
|
| 697 |
)
|
| 698 |
) : null}
|
|
|
|
| 174 |
const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null)
|
| 175 |
const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null)
|
| 176 |
const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('')
|
| 177 |
+
const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
|
| 178 |
const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
|
| 179 |
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
|
| 180 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
|
|
| 350 |
setModeloAbertoCoeficientes(null)
|
| 351 |
setModeloAbertoObsCalc(null)
|
| 352 |
setModeloAbertoMapaHtml('')
|
| 353 |
+
setModeloAbertoMapaPayload(null)
|
| 354 |
setModeloAbertoMapaChoices(['Visualização Padrão'])
|
| 355 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 356 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
|
|
|
| 410 |
? resp.mapa_choices
|
| 411 |
: ['Visualização Padrão']
|
| 412 |
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 413 |
+
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
|
| 414 |
setModeloAbertoMapaChoices(nextChoices)
|
| 415 |
setModeloAbertoMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
|
| 416 |
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
|
|
|
| 554 |
try {
|
| 555 |
const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
|
| 556 |
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 557 |
+
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
|
| 558 |
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 559 |
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || nextTrabalhosTecnicosModo)
|
| 560 |
setModeloAbertoLoadedTabs((prev) => ({ ...prev, mapa: true, trabalhos_tecnicos: true }))
|
|
|
|
| 696 |
</select>
|
| 697 |
</label>
|
| 698 |
</div>
|
| 699 |
+
<MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
|
| 700 |
</>
|
| 701 |
)
|
| 702 |
) : null}
|
frontend/src/components/TrabalhosTecnicosTab.jsx
CHANGED
|
@@ -167,6 +167,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 167 |
const [filtroAno, setFiltroAno] = useState('')
|
| 168 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('clusterizado')
|
| 169 |
const [mapaHtml, setMapaHtml] = useState('')
|
|
|
|
| 170 |
const [mapaStatus, setMapaStatus] = useState('')
|
| 171 |
const [mapaLoading, setMapaLoading] = useState(false)
|
| 172 |
const [mapaError, setMapaError] = useState('')
|
|
@@ -364,6 +365,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 364 |
|
| 365 |
if (!idsValidos.length) {
|
| 366 |
setMapaHtml('')
|
|
|
|
| 367 |
setMapaError('')
|
| 368 |
setMapaStatus('')
|
| 369 |
mapaRequestKeyRef.current = requestKey
|
|
@@ -375,11 +377,13 @@ export default function TrabalhosTecnicosTab({
|
|
| 375 |
try {
|
| 376 |
const resp = await api.trabalhosTecnicosMapa(idsValidos, mapaModoExibicao, avaliandoPayload)
|
| 377 |
setMapaHtml(resp?.mapa_html || '')
|
|
|
|
| 378 |
setMapaStatus(resp?.status || '')
|
| 379 |
mapaRequestKeyRef.current = requestKey
|
| 380 |
} catch (err) {
|
| 381 |
setMapaError(err.message || 'Falha ao gerar o mapa dos trabalhos técnicos.')
|
| 382 |
setMapaHtml('')
|
|
|
|
| 383 |
} finally {
|
| 384 |
setMapaLoading(false)
|
| 385 |
}
|
|
@@ -1402,7 +1406,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 1402 |
</div>
|
| 1403 |
{mapaError ? <div className="error-line inline-error">{mapaError}</div> : null}
|
| 1404 |
{mapaHtml ? (
|
| 1405 |
-
<MapFrame html={mapaHtml} />
|
| 1406 |
) : (
|
| 1407 |
<div className="empty-box">
|
| 1408 |
{mapaLoading
|
|
|
|
| 167 |
const [filtroAno, setFiltroAno] = useState('')
|
| 168 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('clusterizado')
|
| 169 |
const [mapaHtml, setMapaHtml] = useState('')
|
| 170 |
+
const [mapaPayload, setMapaPayload] = useState(null)
|
| 171 |
const [mapaStatus, setMapaStatus] = useState('')
|
| 172 |
const [mapaLoading, setMapaLoading] = useState(false)
|
| 173 |
const [mapaError, setMapaError] = useState('')
|
|
|
|
| 365 |
|
| 366 |
if (!idsValidos.length) {
|
| 367 |
setMapaHtml('')
|
| 368 |
+
setMapaPayload(null)
|
| 369 |
setMapaError('')
|
| 370 |
setMapaStatus('')
|
| 371 |
mapaRequestKeyRef.current = requestKey
|
|
|
|
| 377 |
try {
|
| 378 |
const resp = await api.trabalhosTecnicosMapa(idsValidos, mapaModoExibicao, avaliandoPayload)
|
| 379 |
setMapaHtml(resp?.mapa_html || '')
|
| 380 |
+
setMapaPayload(resp?.mapa_payload || null)
|
| 381 |
setMapaStatus(resp?.status || '')
|
| 382 |
mapaRequestKeyRef.current = requestKey
|
| 383 |
} catch (err) {
|
| 384 |
setMapaError(err.message || 'Falha ao gerar o mapa dos trabalhos técnicos.')
|
| 385 |
setMapaHtml('')
|
| 386 |
+
setMapaPayload(null)
|
| 387 |
} finally {
|
| 388 |
setMapaLoading(false)
|
| 389 |
}
|
|
|
|
| 1406 |
</div>
|
| 1407 |
{mapaError ? <div className="error-line inline-error">{mapaError}</div> : null}
|
| 1408 |
{mapaHtml ? (
|
| 1409 |
+
<MapFrame html={mapaHtml} payload={mapaPayload} />
|
| 1410 |
) : (
|
| 1411 |
<div className="empty-box">
|
| 1412 |
{mapaLoading
|
frontend/src/components/VisualizacaoTab.jsx
CHANGED
|
@@ -22,12 +22,46 @@ const INNER_TABS = [
|
|
| 22 |
]
|
| 23 |
const BASE_COMPARACAO_SEM_BASE = '__none__'
|
| 24 |
const TRABALHOS_TECNICOS_MODELOS_PADRAO = 'selecionados_e_outras_versoes'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
export default function VisualizacaoTab({ sessionId }) {
|
| 27 |
const [loading, setLoading] = useState(false)
|
| 28 |
const [error, setError] = useState('')
|
| 29 |
const [status, setStatus] = useState('')
|
| 30 |
const [badgeHtml, setBadgeHtml] = useState('')
|
|
|
|
| 31 |
|
| 32 |
const [uploadedFile, setUploadedFile] = useState(null)
|
| 33 |
const [uploadDragOver, setUploadDragOver] = useState(false)
|
|
@@ -53,6 +87,7 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 53 |
const [plotCorr, setPlotCorr] = useState(null)
|
| 54 |
|
| 55 |
const [mapaHtml, setMapaHtml] = useState('')
|
|
|
|
| 56 |
const [mapaChoices, setMapaChoices] = useState(['Visualização Padrão'])
|
| 57 |
const [mapaVar, setMapaVar] = useState('Visualização Padrão')
|
| 58 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState(TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
|
@@ -66,8 +101,12 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 66 |
const [baseValue, setBaseValue] = useState('')
|
| 67 |
|
| 68 |
const [activeInnerTab, setActiveInnerTab] = useState('mapa')
|
|
|
|
|
|
|
| 69 |
const deleteConfirmTimersRef = useRef({})
|
| 70 |
const uploadInputRef = useRef(null)
|
|
|
|
|
|
|
| 71 |
const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
|
| 72 |
const repoModeloOptions = useMemo(
|
| 73 |
() => (repoModelos || []).map((item) => ({
|
|
@@ -79,6 +118,8 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 79 |
)
|
| 80 |
|
| 81 |
function resetConteudoVisualizacao() {
|
|
|
|
|
|
|
| 82 |
setDados(null)
|
| 83 |
setEstatisticas(null)
|
| 84 |
setEscalasHtml('')
|
|
@@ -95,6 +136,7 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 95 |
setPlotCorr(null)
|
| 96 |
|
| 97 |
setMapaHtml('')
|
|
|
|
| 98 |
setMapaChoices(['Visualização Padrão'])
|
| 99 |
setMapaVar('Visualização Padrão')
|
| 100 |
setMapaTrabalhosTecnicosModelosModo(TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
|
@@ -108,32 +150,15 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 108 |
setBaseValue('')
|
| 109 |
|
| 110 |
setActiveInnerTab('mapa')
|
|
|
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
-
function
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
setEscalasHtml(resp.escalas_html || '')
|
| 117 |
-
setDadosTransformados(resp.dados_transformados || null)
|
| 118 |
-
setResumoHtml(resp.resumo_html || '')
|
| 119 |
-
setEquacoes(resp.equacoes || null)
|
| 120 |
-
setCoeficientes(resp.coeficientes || null)
|
| 121 |
-
setObsCalc(resp.obs_calc || null)
|
| 122 |
-
|
| 123 |
-
setPlotObsCalc(resp.grafico_obs_calc || null)
|
| 124 |
-
setPlotResiduos(resp.grafico_residuos || null)
|
| 125 |
-
setPlotHistograma(resp.grafico_histograma || null)
|
| 126 |
-
setPlotCook(resp.grafico_cook || null)
|
| 127 |
-
setPlotCorr(resp.grafico_correlacao || null)
|
| 128 |
-
|
| 129 |
-
setMapaHtml(resp.mapa_html || '')
|
| 130 |
-
setMapaChoices(resp.mapa_choices || ['Visualização Padrão'])
|
| 131 |
-
setMapaVar('Visualização Padrão')
|
| 132 |
-
setMapaTrabalhosTecnicosModelosModo(resp.trabalhos_tecnicos_modelos_modo || TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
| 133 |
-
|
| 134 |
-
setCamposAvaliacao(resp.campos_avaliacao || [])
|
| 135 |
const values = {}
|
| 136 |
-
;(resp.campos_avaliacao || []).forEach((campo) => {
|
| 137 |
values[campo.coluna] = ''
|
| 138 |
})
|
| 139 |
valoresAvaliacaoRef.current = values
|
|
@@ -142,6 +167,93 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 142 |
setResultadoAvaliacaoHtml('')
|
| 143 |
setBaseChoices([])
|
| 144 |
setBaseValue('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
async function withBusy(fn) {
|
|
@@ -229,12 +341,20 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 229 |
setModeloLoadSource('upload')
|
| 230 |
await withBusy(async () => {
|
| 231 |
resetConteudoVisualizacao()
|
|
|
|
|
|
|
| 232 |
const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
|
|
|
|
| 233 |
setStatus(uploadResp.status || '')
|
| 234 |
setBadgeHtml(uploadResp.badge_html || '')
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
})
|
| 239 |
}
|
| 240 |
|
|
@@ -243,12 +363,20 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 243 |
setModeloLoadSource('repo')
|
| 244 |
await withBusy(async () => {
|
| 245 |
resetConteudoVisualizacao()
|
|
|
|
|
|
|
| 246 |
const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
|
|
|
|
| 247 |
setStatus(uploadResp.status || '')
|
| 248 |
setBadgeHtml(uploadResp.badge_html || '')
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
setUploadedFile(null)
|
| 253 |
})
|
| 254 |
}
|
|
@@ -291,9 +419,16 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 291 |
trabalhosTecnicosModelosModo = mapaTrabalhosTecnicosModelosModo,
|
| 292 |
) {
|
| 293 |
if (!sessionId) return
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
}
|
| 298 |
|
| 299 |
async function onMapChange(value) {
|
|
@@ -416,6 +551,13 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 416 |
})
|
| 417 |
}
|
| 418 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
return (
|
| 420 |
<div className="tab-content">
|
| 421 |
<SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
|
|
@@ -511,7 +653,7 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 511 |
{badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
|
| 512 |
</SectionBlock>
|
| 513 |
|
| 514 |
-
{
|
| 515 |
<SectionBlock step="2" title="Conteúdo do Modelo" subtitle="Carregue o modelo no topo e navegue pelas abas internas abaixo.">
|
| 516 |
<div className="inner-tabs" role="tablist" aria-label="Abas internas de visualização">
|
| 517 |
{INNER_TABS.map((tab) => (
|
|
@@ -519,7 +661,7 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 519 |
key={tab.key}
|
| 520 |
type="button"
|
| 521 |
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 522 |
-
onClick={() =>
|
| 523 |
>
|
| 524 |
{tab.label}
|
| 525 |
</button>
|
|
@@ -528,7 +670,10 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 528 |
|
| 529 |
<div className="inner-tab-panel">
|
| 530 |
{activeInnerTab === 'mapa' ? (
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
| 532 |
<div className="row compact visualizacao-mapa-controls pesquisa-mapa-controls-row">
|
| 533 |
<label className="pesquisa-field pesquisa-mapa-modo-field">
|
| 534 |
Variável no mapa
|
|
@@ -552,28 +697,34 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 552 |
</select>
|
| 553 |
</label>
|
| 554 |
</div>
|
| 555 |
-
<MapFrame html={mapaHtml} />
|
| 556 |
</>
|
|
|
|
| 557 |
) : null}
|
| 558 |
|
| 559 |
{activeInnerTab === 'dados_mercado' ? (
|
| 560 |
-
<DataTable table={dados} maxHeight={620} />
|
| 561 |
) : null}
|
| 562 |
|
| 563 |
{activeInnerTab === 'metricas' ? (
|
| 564 |
-
<DataTable table={estatisticas} maxHeight={620} />
|
| 565 |
) : null}
|
| 566 |
|
| 567 |
{activeInnerTab === 'transformacoes' ? (
|
| 568 |
-
|
|
|
|
| 569 |
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
|
| 570 |
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 571 |
<DataTable table={dadosTransformados} />
|
| 572 |
</>
|
|
|
|
|
|
|
|
|
|
| 573 |
) : null}
|
| 574 |
|
| 575 |
{activeInnerTab === 'resumo' ? (
|
| 576 |
-
|
|
|
|
| 577 |
<div className="equation-formats-section">
|
| 578 |
<h4>Equações do Modelo</h4>
|
| 579 |
<EquationFormatsPanel
|
|
@@ -584,18 +735,22 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 584 |
</div>
|
| 585 |
<div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
|
| 586 |
</>
|
|
|
|
|
|
|
|
|
|
| 587 |
) : null}
|
| 588 |
|
| 589 |
{activeInnerTab === 'coeficientes' ? (
|
| 590 |
-
<DataTable table={coeficientes} maxHeight={620} />
|
| 591 |
) : null}
|
| 592 |
|
| 593 |
{activeInnerTab === 'obs_calc' ? (
|
| 594 |
-
<DataTable table={obsCalc} maxHeight={620} />
|
| 595 |
) : null}
|
| 596 |
|
| 597 |
{activeInnerTab === 'graficos' ? (
|
| 598 |
-
|
|
|
|
| 599 |
<div className="plot-grid-2-fixed">
|
| 600 |
<PlotFigure figure={plotObsCalc} title="Obs x Calc" />
|
| 601 |
<PlotFigure figure={plotResiduos} title="Resíduos" />
|
|
@@ -606,6 +761,9 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 606 |
<PlotFigure figure={plotCorr} title="Correlação" className="plot-correlation-card" />
|
| 607 |
</div>
|
| 608 |
</>
|
|
|
|
|
|
|
|
|
|
| 609 |
) : null}
|
| 610 |
|
| 611 |
{activeInnerTab === 'avaliacao' ? (
|
|
@@ -709,7 +867,10 @@ export default function VisualizacaoTab({ sessionId }) {
|
|
| 709 |
</SectionBlock>
|
| 710 |
) : null}
|
| 711 |
|
| 712 |
-
<LoadingOverlay
|
|
|
|
|
|
|
|
|
|
| 713 |
{error ? <div className="error-line">{error}</div> : null}
|
| 714 |
</div>
|
| 715 |
)
|
|
|
|
| 22 |
]
|
| 23 |
const BASE_COMPARACAO_SEM_BASE = '__none__'
|
| 24 |
const TRABALHOS_TECNICOS_MODELOS_PADRAO = 'selecionados_e_outras_versoes'
|
| 25 |
+
const SECTION_TABS = new Set([
|
| 26 |
+
'mapa',
|
| 27 |
+
'dados_mercado',
|
| 28 |
+
'metricas',
|
| 29 |
+
'transformacoes',
|
| 30 |
+
'resumo',
|
| 31 |
+
'coeficientes',
|
| 32 |
+
'obs_calc',
|
| 33 |
+
'graficos',
|
| 34 |
+
])
|
| 35 |
+
|
| 36 |
+
function getVisualizacaoLoadingLabel(tabKey) {
|
| 37 |
+
switch (String(tabKey || '').trim()) {
|
| 38 |
+
case 'mapa':
|
| 39 |
+
return 'Carregando mapa do modelo...'
|
| 40 |
+
case 'dados_mercado':
|
| 41 |
+
return 'Carregando dados de mercado...'
|
| 42 |
+
case 'metricas':
|
| 43 |
+
return 'Carregando métricas do modelo...'
|
| 44 |
+
case 'transformacoes':
|
| 45 |
+
return 'Carregando transformações do modelo...'
|
| 46 |
+
case 'resumo':
|
| 47 |
+
return 'Carregando resumo do modelo...'
|
| 48 |
+
case 'coeficientes':
|
| 49 |
+
return 'Carregando coeficientes do modelo...'
|
| 50 |
+
case 'obs_calc':
|
| 51 |
+
return 'Carregando observados x calculados...'
|
| 52 |
+
case 'graficos':
|
| 53 |
+
return 'Carregando gráficos do modelo...'
|
| 54 |
+
default:
|
| 55 |
+
return 'Processando dados...'
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
|
| 59 |
export default function VisualizacaoTab({ sessionId }) {
|
| 60 |
const [loading, setLoading] = useState(false)
|
| 61 |
const [error, setError] = useState('')
|
| 62 |
const [status, setStatus] = useState('')
|
| 63 |
const [badgeHtml, setBadgeHtml] = useState('')
|
| 64 |
+
const [modeloCarregado, setModeloCarregado] = useState(false)
|
| 65 |
|
| 66 |
const [uploadedFile, setUploadedFile] = useState(null)
|
| 67 |
const [uploadDragOver, setUploadDragOver] = useState(false)
|
|
|
|
| 87 |
const [plotCorr, setPlotCorr] = useState(null)
|
| 88 |
|
| 89 |
const [mapaHtml, setMapaHtml] = useState('')
|
| 90 |
+
const [mapaPayload, setMapaPayload] = useState(null)
|
| 91 |
const [mapaChoices, setMapaChoices] = useState(['Visualização Padrão'])
|
| 92 |
const [mapaVar, setMapaVar] = useState('Visualização Padrão')
|
| 93 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState(TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
|
|
|
| 101 |
const [baseValue, setBaseValue] = useState('')
|
| 102 |
|
| 103 |
const [activeInnerTab, setActiveInnerTab] = useState('mapa')
|
| 104 |
+
const [loadedTabs, setLoadedTabs] = useState({})
|
| 105 |
+
const [loadingTabs, setLoadingTabs] = useState({})
|
| 106 |
const deleteConfirmTimersRef = useRef({})
|
| 107 |
const uploadInputRef = useRef(null)
|
| 108 |
+
const pendingTabRequestsRef = useRef({})
|
| 109 |
+
const modeloLoadVersionRef = useRef(0)
|
| 110 |
const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
|
| 111 |
const repoModeloOptions = useMemo(
|
| 112 |
() => (repoModelos || []).map((item) => ({
|
|
|
|
| 118 |
)
|
| 119 |
|
| 120 |
function resetConteudoVisualizacao() {
|
| 121 |
+
pendingTabRequestsRef.current = {}
|
| 122 |
+
setModeloCarregado(false)
|
| 123 |
setDados(null)
|
| 124 |
setEstatisticas(null)
|
| 125 |
setEscalasHtml('')
|
|
|
|
| 136 |
setPlotCorr(null)
|
| 137 |
|
| 138 |
setMapaHtml('')
|
| 139 |
+
setMapaPayload(null)
|
| 140 |
setMapaChoices(['Visualização Padrão'])
|
| 141 |
setMapaVar('Visualização Padrão')
|
| 142 |
setMapaTrabalhosTecnicosModelosModo(TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
|
|
|
| 150 |
setBaseValue('')
|
| 151 |
|
| 152 |
setActiveInnerTab('mapa')
|
| 153 |
+
setLoadedTabs({})
|
| 154 |
+
setLoadingTabs({})
|
| 155 |
}
|
| 156 |
|
| 157 |
+
function applyEvaluationContext(resp) {
|
| 158 |
+
setCamposAvaliacao(resp?.campos_avaliacao || [])
|
| 159 |
+
setEquacoes(resp?.equacoes || null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
const values = {}
|
| 161 |
+
;(resp?.campos_avaliacao || []).forEach((campo) => {
|
| 162 |
values[campo.coluna] = ''
|
| 163 |
})
|
| 164 |
valoresAvaliacaoRef.current = values
|
|
|
|
| 167 |
setResultadoAvaliacaoHtml('')
|
| 168 |
setBaseChoices([])
|
| 169 |
setBaseValue('')
|
| 170 |
+
setModeloCarregado(true)
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function applyVisualizacaoSection(secao, resp) {
|
| 174 |
+
const key = String(secao || '').trim()
|
| 175 |
+
if (key === 'dados_mercado') {
|
| 176 |
+
setDados(resp?.dados || null)
|
| 177 |
+
return
|
| 178 |
+
}
|
| 179 |
+
if (key === 'metricas') {
|
| 180 |
+
setEstatisticas(resp?.estatisticas || null)
|
| 181 |
+
return
|
| 182 |
+
}
|
| 183 |
+
if (key === 'transformacoes') {
|
| 184 |
+
setEscalasHtml(resp?.escalas_html || '')
|
| 185 |
+
setDadosTransformados(resp?.dados_transformados || null)
|
| 186 |
+
return
|
| 187 |
+
}
|
| 188 |
+
if (key === 'resumo') {
|
| 189 |
+
setResumoHtml(resp?.resumo_html || '')
|
| 190 |
+
setEquacoes(resp?.equacoes || null)
|
| 191 |
+
return
|
| 192 |
+
}
|
| 193 |
+
if (key === 'coeficientes') {
|
| 194 |
+
setCoeficientes(resp?.coeficientes || null)
|
| 195 |
+
return
|
| 196 |
+
}
|
| 197 |
+
if (key === 'obs_calc') {
|
| 198 |
+
setObsCalc(resp?.obs_calc || null)
|
| 199 |
+
return
|
| 200 |
+
}
|
| 201 |
+
if (key === 'graficos') {
|
| 202 |
+
setPlotObsCalc(resp?.grafico_obs_calc || null)
|
| 203 |
+
setPlotResiduos(resp?.grafico_residuos || null)
|
| 204 |
+
setPlotHistograma(resp?.grafico_histograma || null)
|
| 205 |
+
setPlotCook(resp?.grafico_cook || null)
|
| 206 |
+
setPlotCorr(resp?.grafico_correlacao || null)
|
| 207 |
+
return
|
| 208 |
+
}
|
| 209 |
+
if (key === 'mapa') {
|
| 210 |
+
const nextChoices = Array.isArray(resp?.mapa_choices) && resp.mapa_choices.length
|
| 211 |
+
? resp.mapa_choices
|
| 212 |
+
: ['Visualização Padrão']
|
| 213 |
+
setMapaHtml(resp?.mapa_html || '')
|
| 214 |
+
setMapaPayload(resp?.mapa_payload || null)
|
| 215 |
+
setMapaChoices(nextChoices)
|
| 216 |
+
setMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
|
| 217 |
+
setMapaTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || TRABALHOS_TECNICOS_MODELOS_PADRAO)
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
async function ensureVisualizacaoSection(secao, options = {}) {
|
| 222 |
+
const secaoNormalizada = String(secao || '').trim()
|
| 223 |
+
if (!sessionId || !SECTION_TABS.has(secaoNormalizada)) return
|
| 224 |
+
if (!options.force && loadedTabs[secaoNormalizada]) return
|
| 225 |
+
if (pendingTabRequestsRef.current[secaoNormalizada]) {
|
| 226 |
+
await pendingTabRequestsRef.current[secaoNormalizada]
|
| 227 |
+
return
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
const expectedVersion = options.expectedVersion ?? modeloLoadVersionRef.current
|
| 231 |
+
const trabalhosTecnicosModo = options.trabalhosTecnicosModelosModo || mapaTrabalhosTecnicosModelosModo
|
| 232 |
+
setLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: true }))
|
| 233 |
+
|
| 234 |
+
const request = (async () => {
|
| 235 |
+
try {
|
| 236 |
+
const resp = await api.visualizacaoSection(sessionId, secaoNormalizada, trabalhosTecnicosModo)
|
| 237 |
+
if (modeloLoadVersionRef.current !== expectedVersion) return
|
| 238 |
+
applyVisualizacaoSection(secaoNormalizada, resp)
|
| 239 |
+
setLoadedTabs((prev) => ({ ...prev, [secaoNormalizada]: true }))
|
| 240 |
+
} catch (err) {
|
| 241 |
+
if (modeloLoadVersionRef.current !== expectedVersion) return
|
| 242 |
+
setError(err.message || 'Falha ao carregar dados do modelo.')
|
| 243 |
+
} finally {
|
| 244 |
+
if (modeloLoadVersionRef.current !== expectedVersion) return
|
| 245 |
+
setLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: false }))
|
| 246 |
+
}
|
| 247 |
+
})()
|
| 248 |
+
|
| 249 |
+
pendingTabRequestsRef.current[secaoNormalizada] = request
|
| 250 |
+
try {
|
| 251 |
+
await request
|
| 252 |
+
} finally {
|
| 253 |
+
if (pendingTabRequestsRef.current[secaoNormalizada] === request) {
|
| 254 |
+
delete pendingTabRequestsRef.current[secaoNormalizada]
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
}
|
| 258 |
|
| 259 |
async function withBusy(fn) {
|
|
|
|
| 341 |
setModeloLoadSource('upload')
|
| 342 |
await withBusy(async () => {
|
| 343 |
resetConteudoVisualizacao()
|
| 344 |
+
modeloLoadVersionRef.current += 1
|
| 345 |
+
const openVersion = modeloLoadVersionRef.current
|
| 346 |
const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
|
| 347 |
+
if (modeloLoadVersionRef.current !== openVersion) return
|
| 348 |
setStatus(uploadResp.status || '')
|
| 349 |
setBadgeHtml(uploadResp.badge_html || '')
|
| 350 |
+
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 351 |
+
if (modeloLoadVersionRef.current !== openVersion) return
|
| 352 |
+
applyEvaluationContext(contextoResp)
|
| 353 |
+
await ensureVisualizacaoSection('mapa', {
|
| 354 |
+
force: true,
|
| 355 |
+
expectedVersion: openVersion,
|
| 356 |
+
trabalhosTecnicosModelosModo: TRABALHOS_TECNICOS_MODELOS_PADRAO,
|
| 357 |
+
})
|
| 358 |
})
|
| 359 |
}
|
| 360 |
|
|
|
|
| 363 |
setModeloLoadSource('repo')
|
| 364 |
await withBusy(async () => {
|
| 365 |
resetConteudoVisualizacao()
|
| 366 |
+
modeloLoadVersionRef.current += 1
|
| 367 |
+
const openVersion = modeloLoadVersionRef.current
|
| 368 |
const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
|
| 369 |
+
if (modeloLoadVersionRef.current !== openVersion) return
|
| 370 |
setStatus(uploadResp.status || '')
|
| 371 |
setBadgeHtml(uploadResp.badge_html || '')
|
| 372 |
+
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 373 |
+
if (modeloLoadVersionRef.current !== openVersion) return
|
| 374 |
+
applyEvaluationContext(contextoResp)
|
| 375 |
+
await ensureVisualizacaoSection('mapa', {
|
| 376 |
+
force: true,
|
| 377 |
+
expectedVersion: openVersion,
|
| 378 |
+
trabalhosTecnicosModelosModo: TRABALHOS_TECNICOS_MODELOS_PADRAO,
|
| 379 |
+
})
|
| 380 |
setUploadedFile(null)
|
| 381 |
})
|
| 382 |
}
|
|
|
|
| 419 |
trabalhosTecnicosModelosModo = mapaTrabalhosTecnicosModelosModo,
|
| 420 |
) {
|
| 421 |
if (!sessionId) return
|
| 422 |
+
setLoadingTabs((prev) => ({ ...prev, mapa: true }))
|
| 423 |
+
try {
|
| 424 |
+
const resp = await api.updateVisualizacaoMap(sessionId, variavelMapa, trabalhosTecnicosModelosModo)
|
| 425 |
+
setMapaHtml(resp?.mapa_html || '')
|
| 426 |
+
setMapaPayload(resp?.mapa_payload || null)
|
| 427 |
+
setMapaTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || trabalhosTecnicosModelosModo)
|
| 428 |
+
setLoadedTabs((prev) => ({ ...prev, mapa: true }))
|
| 429 |
+
} finally {
|
| 430 |
+
setLoadingTabs((prev) => ({ ...prev, mapa: false }))
|
| 431 |
+
}
|
| 432 |
}
|
| 433 |
|
| 434 |
async function onMapChange(value) {
|
|
|
|
| 551 |
})
|
| 552 |
}
|
| 553 |
|
| 554 |
+
function onInnerTabSelect(nextTab) {
|
| 555 |
+
setActiveInnerTab(nextTab)
|
| 556 |
+
if (SECTION_TABS.has(nextTab)) {
|
| 557 |
+
void ensureVisualizacaoSection(nextTab)
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
return (
|
| 562 |
<div className="tab-content">
|
| 563 |
<SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
|
|
|
|
| 653 |
{badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
|
| 654 |
</SectionBlock>
|
| 655 |
|
| 656 |
+
{modeloCarregado ? (
|
| 657 |
<SectionBlock step="2" title="Conteúdo do Modelo" subtitle="Carregue o modelo no topo e navegue pelas abas internas abaixo.">
|
| 658 |
<div className="inner-tabs" role="tablist" aria-label="Abas internas de visualização">
|
| 659 |
{INNER_TABS.map((tab) => (
|
|
|
|
| 661 |
key={tab.key}
|
| 662 |
type="button"
|
| 663 |
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 664 |
+
onClick={() => onInnerTabSelect(tab.key)}
|
| 665 |
>
|
| 666 |
{tab.label}
|
| 667 |
</button>
|
|
|
|
| 670 |
|
| 671 |
<div className="inner-tab-panel">
|
| 672 |
{activeInnerTab === 'mapa' ? (
|
| 673 |
+
!loadedTabs.mapa ? (
|
| 674 |
+
<div className="empty-box">Carregando mapa do modelo...</div>
|
| 675 |
+
) : (
|
| 676 |
+
<>
|
| 677 |
<div className="row compact visualizacao-mapa-controls pesquisa-mapa-controls-row">
|
| 678 |
<label className="pesquisa-field pesquisa-mapa-modo-field">
|
| 679 |
Variável no mapa
|
|
|
|
| 697 |
</select>
|
| 698 |
</label>
|
| 699 |
</div>
|
| 700 |
+
<MapFrame html={mapaHtml} payload={mapaPayload} sessionId={sessionId} />
|
| 701 |
</>
|
| 702 |
+
)
|
| 703 |
) : null}
|
| 704 |
|
| 705 |
{activeInnerTab === 'dados_mercado' ? (
|
| 706 |
+
loadedTabs.dados_mercado ? <DataTable table={dados} maxHeight={620} /> : <div className="empty-box">Carregando dados de mercado...</div>
|
| 707 |
) : null}
|
| 708 |
|
| 709 |
{activeInnerTab === 'metricas' ? (
|
| 710 |
+
loadedTabs.metricas ? <DataTable table={estatisticas} maxHeight={620} /> : <div className="empty-box">Carregando métricas do modelo...</div>
|
| 711 |
) : null}
|
| 712 |
|
| 713 |
{activeInnerTab === 'transformacoes' ? (
|
| 714 |
+
loadedTabs.transformacoes ? (
|
| 715 |
+
<>
|
| 716 |
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
|
| 717 |
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 718 |
<DataTable table={dadosTransformados} />
|
| 719 |
</>
|
| 720 |
+
) : (
|
| 721 |
+
<div className="empty-box">Carregando transformações do modelo...</div>
|
| 722 |
+
)
|
| 723 |
) : null}
|
| 724 |
|
| 725 |
{activeInnerTab === 'resumo' ? (
|
| 726 |
+
loadedTabs.resumo ? (
|
| 727 |
+
<>
|
| 728 |
<div className="equation-formats-section">
|
| 729 |
<h4>Equações do Modelo</h4>
|
| 730 |
<EquationFormatsPanel
|
|
|
|
| 735 |
</div>
|
| 736 |
<div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
|
| 737 |
</>
|
| 738 |
+
) : (
|
| 739 |
+
<div className="empty-box">Carregando resumo do modelo...</div>
|
| 740 |
+
)
|
| 741 |
) : null}
|
| 742 |
|
| 743 |
{activeInnerTab === 'coeficientes' ? (
|
| 744 |
+
loadedTabs.coeficientes ? <DataTable table={coeficientes} maxHeight={620} /> : <div className="empty-box">Carregando coeficientes do modelo...</div>
|
| 745 |
) : null}
|
| 746 |
|
| 747 |
{activeInnerTab === 'obs_calc' ? (
|
| 748 |
+
loadedTabs.obs_calc ? <DataTable table={obsCalc} maxHeight={620} /> : <div className="empty-box">Carregando observados x calculados...</div>
|
| 749 |
) : null}
|
| 750 |
|
| 751 |
{activeInnerTab === 'graficos' ? (
|
| 752 |
+
loadedTabs.graficos ? (
|
| 753 |
+
<>
|
| 754 |
<div className="plot-grid-2-fixed">
|
| 755 |
<PlotFigure figure={plotObsCalc} title="Obs x Calc" />
|
| 756 |
<PlotFigure figure={plotResiduos} title="Resíduos" />
|
|
|
|
| 761 |
<PlotFigure figure={plotCorr} title="Correlação" className="plot-correlation-card" />
|
| 762 |
</div>
|
| 763 |
</>
|
| 764 |
+
) : (
|
| 765 |
+
<div className="empty-box">Carregando gráficos do modelo...</div>
|
| 766 |
+
)
|
| 767 |
) : null}
|
| 768 |
|
| 769 |
{activeInnerTab === 'avaliacao' ? (
|
|
|
|
| 867 |
</SectionBlock>
|
| 868 |
) : null}
|
| 869 |
|
| 870 |
+
<LoadingOverlay
|
| 871 |
+
show={loading || Boolean(loadingTabs[activeInnerTab])}
|
| 872 |
+
label={loading ? 'Processando dados...' : getVisualizacaoLoadingLabel(activeInnerTab)}
|
| 873 |
+
/>
|
| 874 |
{error ? <div className="error-line">{error}</div> : null}
|
| 875 |
</div>
|
| 876 |
)
|
frontend/src/main.jsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import React from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
|
|
|
|
|
|
| 3 |
import App from './App'
|
| 4 |
import './styles.css'
|
| 5 |
|
|
|
|
| 1 |
import React from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import 'leaflet/dist/leaflet.css'
|
| 4 |
+
import 'leaflet.fullscreen/dist/Control.FullScreen.css'
|
| 5 |
import App from './App'
|
| 6 |
import './styles.css'
|
| 7 |
|
frontend/src/styles.css
CHANGED
|
@@ -1464,6 +1464,8 @@ textarea {
|
|
| 1464 |
border-radius: 12px;
|
| 1465 |
background: #fff;
|
| 1466 |
padding: 14px;
|
|
|
|
|
|
|
| 1467 |
}
|
| 1468 |
|
| 1469 |
.trabalhos-mapa-panel {
|
|
@@ -3560,8 +3562,8 @@ button.pesquisa-coluna-remove:hover {
|
|
| 3560 |
}
|
| 3561 |
|
| 3562 |
.pesquisa-card-values-modal {
|
| 3563 |
-
width:
|
| 3564 |
-
max-width: min(
|
| 3565 |
}
|
| 3566 |
|
| 3567 |
.pesquisa-card-values-modal-actions {
|
|
@@ -3572,7 +3574,9 @@ button.pesquisa-coluna-remove:hover {
|
|
| 3572 |
}
|
| 3573 |
|
| 3574 |
.pesquisa-card-values-content {
|
| 3575 |
-
|
|
|
|
|
|
|
| 3576 |
padding: 12px 14px;
|
| 3577 |
border: 1px solid #d4e0eb;
|
| 3578 |
border-radius: 12px;
|
|
@@ -4929,6 +4933,361 @@ button.btn-upload-select {
|
|
| 4929 |
background: #fff;
|
| 4930 |
}
|
| 4931 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4932 |
.table-wrapper {
|
| 4933 |
overflow: auto;
|
| 4934 |
border: 1px solid #d8e2ec;
|
|
|
|
| 1464 |
border-radius: 12px;
|
| 1465 |
background: #fff;
|
| 1466 |
padding: 14px;
|
| 1467 |
+
width: 100%;
|
| 1468 |
+
box-sizing: border-box;
|
| 1469 |
}
|
| 1470 |
|
| 1471 |
.trabalhos-mapa-panel {
|
|
|
|
| 3562 |
}
|
| 3563 |
|
| 3564 |
.pesquisa-card-values-modal {
|
| 3565 |
+
width: min(920px, calc(100vw - 40px));
|
| 3566 |
+
max-width: min(920px, calc(100vw - 40px));
|
| 3567 |
}
|
| 3568 |
|
| 3569 |
.pesquisa-card-values-modal-actions {
|
|
|
|
| 3574 |
}
|
| 3575 |
|
| 3576 |
.pesquisa-card-values-content {
|
| 3577 |
+
width: 100%;
|
| 3578 |
+
max-width: 100%;
|
| 3579 |
+
box-sizing: border-box;
|
| 3580 |
padding: 12px 14px;
|
| 3581 |
border: 1px solid #d4e0eb;
|
| 3582 |
border-radius: 12px;
|
|
|
|
| 4933 |
background: #fff;
|
| 4934 |
}
|
| 4935 |
|
| 4936 |
+
.leaflet-map-host {
|
| 4937 |
+
position: relative;
|
| 4938 |
+
overflow: hidden;
|
| 4939 |
+
}
|
| 4940 |
+
|
| 4941 |
+
.leaflet-map-canvas {
|
| 4942 |
+
width: 100%;
|
| 4943 |
+
min-height: 560px;
|
| 4944 |
+
}
|
| 4945 |
+
|
| 4946 |
+
.leaflet-map-host .leaflet-container {
|
| 4947 |
+
width: 100%;
|
| 4948 |
+
min-height: 560px;
|
| 4949 |
+
border-radius: 12px;
|
| 4950 |
+
font-family: 'Sora', sans-serif;
|
| 4951 |
+
}
|
| 4952 |
+
|
| 4953 |
+
.leaflet-map-runtime-error {
|
| 4954 |
+
position: absolute;
|
| 4955 |
+
right: 14px;
|
| 4956 |
+
bottom: 14px;
|
| 4957 |
+
max-width: min(360px, calc(100% - 28px));
|
| 4958 |
+
border-radius: 10px;
|
| 4959 |
+
background: rgba(255, 248, 248, 0.95);
|
| 4960 |
+
border: 1px solid rgba(177, 63, 63, 0.22);
|
| 4961 |
+
color: #8a2d2d;
|
| 4962 |
+
padding: 10px 12px;
|
| 4963 |
+
font-size: 0.82rem;
|
| 4964 |
+
box-shadow: 0 10px 28px rgba(40, 63, 91, 0.12);
|
| 4965 |
+
z-index: 600;
|
| 4966 |
+
}
|
| 4967 |
+
|
| 4968 |
+
.mesa-leaflet-legend {
|
| 4969 |
+
min-width: 168px;
|
| 4970 |
+
border-radius: 12px;
|
| 4971 |
+
padding: 10px 12px;
|
| 4972 |
+
background: rgba(255, 255, 255, 0.94);
|
| 4973 |
+
border: 1px solid rgba(194, 208, 222, 0.9);
|
| 4974 |
+
box-shadow: 0 10px 26px rgba(29, 52, 78, 0.14);
|
| 4975 |
+
color: #24405b;
|
| 4976 |
+
backdrop-filter: blur(8px);
|
| 4977 |
+
}
|
| 4978 |
+
|
| 4979 |
+
.mesa-leaflet-legend strong {
|
| 4980 |
+
display: block;
|
| 4981 |
+
margin-bottom: 8px;
|
| 4982 |
+
font-size: 0.78rem;
|
| 4983 |
+
letter-spacing: 0.03em;
|
| 4984 |
+
text-transform: uppercase;
|
| 4985 |
+
}
|
| 4986 |
+
|
| 4987 |
+
.mesa-leaflet-legend-bar {
|
| 4988 |
+
height: 10px;
|
| 4989 |
+
border-radius: 999px;
|
| 4990 |
+
margin-bottom: 7px;
|
| 4991 |
+
}
|
| 4992 |
+
|
| 4993 |
+
.mesa-leaflet-legend-ticks {
|
| 4994 |
+
position: relative;
|
| 4995 |
+
height: 16px;
|
| 4996 |
+
margin-top: -2px;
|
| 4997 |
+
margin-bottom: 6px;
|
| 4998 |
+
}
|
| 4999 |
+
|
| 5000 |
+
.mesa-leaflet-legend-tick {
|
| 5001 |
+
position: absolute;
|
| 5002 |
+
top: 0;
|
| 5003 |
+
transform: translateX(-50%);
|
| 5004 |
+
font-size: 0.68rem;
|
| 5005 |
+
color: #4a6077;
|
| 5006 |
+
white-space: nowrap;
|
| 5007 |
+
}
|
| 5008 |
+
|
| 5009 |
+
.mesa-leaflet-legend-tick:first-child {
|
| 5010 |
+
transform: translateX(0);
|
| 5011 |
+
}
|
| 5012 |
+
|
| 5013 |
+
.mesa-leaflet-legend-tick:last-child {
|
| 5014 |
+
transform: translateX(-100%);
|
| 5015 |
+
}
|
| 5016 |
+
|
| 5017 |
+
.mesa-leaflet-legend-scale {
|
| 5018 |
+
display: flex;
|
| 5019 |
+
justify-content: space-between;
|
| 5020 |
+
gap: 10px;
|
| 5021 |
+
font-size: 0.76rem;
|
| 5022 |
+
color: #4a6077;
|
| 5023 |
+
}
|
| 5024 |
+
|
| 5025 |
+
.mesa-leaflet-notice {
|
| 5026 |
+
max-width: min(320px, calc(100vw - 48px));
|
| 5027 |
+
border-radius: 10px;
|
| 5028 |
+
padding: 8px 10px;
|
| 5029 |
+
background: rgba(248, 251, 255, 0.94);
|
| 5030 |
+
border: 1px solid rgba(194, 208, 222, 0.92);
|
| 5031 |
+
box-shadow: 0 10px 26px rgba(29, 52, 78, 0.14);
|
| 5032 |
+
color: #35506b;
|
| 5033 |
+
font-size: 0.76rem;
|
| 5034 |
+
line-height: 1.35;
|
| 5035 |
+
}
|
| 5036 |
+
|
| 5037 |
+
.leaflet-map-host .leaflet-control-layers {
|
| 5038 |
+
border: 1px solid rgba(188, 201, 214, 0.92);
|
| 5039 |
+
border-radius: 12px;
|
| 5040 |
+
overflow: hidden;
|
| 5041 |
+
background: rgba(255, 255, 255, 0.96);
|
| 5042 |
+
box-shadow: 0 10px 24px rgba(24, 46, 71, 0.14);
|
| 5043 |
+
backdrop-filter: blur(6px);
|
| 5044 |
+
}
|
| 5045 |
+
|
| 5046 |
+
.leaflet-map-host .leaflet-control-layers-toggle {
|
| 5047 |
+
width: 38px;
|
| 5048 |
+
height: 38px;
|
| 5049 |
+
position: relative;
|
| 5050 |
+
display: block;
|
| 5051 |
+
background-color: rgba(255, 255, 255, 0.95);
|
| 5052 |
+
background-image: none !important;
|
| 5053 |
+
background-size: 0 0;
|
| 5054 |
+
}
|
| 5055 |
+
|
| 5056 |
+
.leaflet-map-host .leaflet-control-layers-toggle::before {
|
| 5057 |
+
content: '';
|
| 5058 |
+
position: absolute;
|
| 5059 |
+
inset: 0;
|
| 5060 |
+
margin: auto;
|
| 5061 |
+
width: 18px;
|
| 5062 |
+
height: 18px;
|
| 5063 |
+
background-repeat: no-repeat;
|
| 5064 |
+
background-position: center;
|
| 5065 |
+
background-size: 18px 18px;
|
| 5066 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2324405b' d='M12 2 3 7l9 5 9-5-9-5Zm-7.5 8.6L12 15l7.5-4.4L21 12l-9 5-9-5 1.5-1.4Zm0 4.8L12 19.8l7.5-4.4L21 17l-9 5-9-5 1.5-1.6Z'/%3E%3C/svg%3E");
|
| 5067 |
+
}
|
| 5068 |
+
|
| 5069 |
+
.leaflet-map-host .leaflet-control-layers-toggle:hover {
|
| 5070 |
+
background-color: #eef5fb;
|
| 5071 |
+
}
|
| 5072 |
+
|
| 5073 |
+
.leaflet-map-host .leaflet-control-layers-expanded {
|
| 5074 |
+
padding: 6px 9px;
|
| 5075 |
+
min-width: 220px;
|
| 5076 |
+
max-width: min(260px, calc(100vw - 36px));
|
| 5077 |
+
overflow: visible;
|
| 5078 |
+
}
|
| 5079 |
+
|
| 5080 |
+
.leaflet-map-host .leaflet-control-layers .leaflet-control-layers-list {
|
| 5081 |
+
display: none;
|
| 5082 |
+
}
|
| 5083 |
+
|
| 5084 |
+
.leaflet-map-host .leaflet-control-layers-expanded .leaflet-control-layers-list {
|
| 5085 |
+
display: block;
|
| 5086 |
+
position: relative;
|
| 5087 |
+
}
|
| 5088 |
+
|
| 5089 |
+
.leaflet-map-host .leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
| 5090 |
+
display: none;
|
| 5091 |
+
}
|
| 5092 |
+
|
| 5093 |
+
.leaflet-map-host .leaflet-control-layers label {
|
| 5094 |
+
display: flex;
|
| 5095 |
+
align-items: center;
|
| 5096 |
+
gap: 6px;
|
| 5097 |
+
margin: 0;
|
| 5098 |
+
padding: 1px 4px;
|
| 5099 |
+
border: none;
|
| 5100 |
+
border-radius: 8px;
|
| 5101 |
+
font-size: 0.74rem;
|
| 5102 |
+
font-weight: 500;
|
| 5103 |
+
color: #27445f;
|
| 5104 |
+
cursor: pointer;
|
| 5105 |
+
transition: background-color 0.18s ease, color 0.18s ease;
|
| 5106 |
+
line-height: 1.12;
|
| 5107 |
+
white-space: normal;
|
| 5108 |
+
}
|
| 5109 |
+
|
| 5110 |
+
.leaflet-map-host .leaflet-control-layers label:hover {
|
| 5111 |
+
background: #f3f7fb;
|
| 5112 |
+
transform: none;
|
| 5113 |
+
color: #1f5f9f;
|
| 5114 |
+
}
|
| 5115 |
+
|
| 5116 |
+
.leaflet-map-host .leaflet-control-layers input {
|
| 5117 |
+
accent-color: #1f6fb2;
|
| 5118 |
+
width: 16px;
|
| 5119 |
+
height: 16px;
|
| 5120 |
+
margin: 0;
|
| 5121 |
+
flex: 0 0 auto;
|
| 5122 |
+
align-self: center;
|
| 5123 |
+
}
|
| 5124 |
+
|
| 5125 |
+
.leaflet-map-host .leaflet-control-layers-selector {
|
| 5126 |
+
margin-top: 0;
|
| 5127 |
+
top: 0;
|
| 5128 |
+
position: static;
|
| 5129 |
+
}
|
| 5130 |
+
|
| 5131 |
+
.leaflet-map-host .leaflet-control-layers label > span {
|
| 5132 |
+
display: flex;
|
| 5133 |
+
align-items: center;
|
| 5134 |
+
gap: 5px;
|
| 5135 |
+
width: 100%;
|
| 5136 |
+
}
|
| 5137 |
+
|
| 5138 |
+
.leaflet-map-host .leaflet-control-layers label > span > span {
|
| 5139 |
+
display: inline-block;
|
| 5140 |
+
flex: 1 1 auto;
|
| 5141 |
+
min-width: 0;
|
| 5142 |
+
padding-top: 0;
|
| 5143 |
+
}
|
| 5144 |
+
|
| 5145 |
+
.leaflet-map-host .leaflet-control-layers-separator {
|
| 5146 |
+
margin: 3px -9px 4px;
|
| 5147 |
+
border-top-color: rgba(207, 217, 227, 0.95);
|
| 5148 |
+
}
|
| 5149 |
+
|
| 5150 |
+
.leaflet-map-host .leaflet-control-fullscreen,
|
| 5151 |
+
.leaflet-map-host .leaflet-control-measure {
|
| 5152 |
+
border: none;
|
| 5153 |
+
box-shadow: 0 10px 24px rgba(27, 48, 72, 0.16);
|
| 5154 |
+
}
|
| 5155 |
+
|
| 5156 |
+
.mesa-sr-only {
|
| 5157 |
+
position: absolute;
|
| 5158 |
+
width: 1px;
|
| 5159 |
+
height: 1px;
|
| 5160 |
+
padding: 0;
|
| 5161 |
+
margin: -1px;
|
| 5162 |
+
overflow: hidden;
|
| 5163 |
+
clip: rect(0, 0, 0, 0);
|
| 5164 |
+
white-space: nowrap;
|
| 5165 |
+
border: 0;
|
| 5166 |
+
}
|
| 5167 |
+
|
| 5168 |
+
.leaflet-map-host .leaflet-bar a,
|
| 5169 |
+
.leaflet-map-host .leaflet-bar button {
|
| 5170 |
+
border-color: rgba(193, 207, 220, 0.95);
|
| 5171 |
+
color: #24405b;
|
| 5172 |
+
}
|
| 5173 |
+
|
| 5174 |
+
.leaflet-map-host .mesa-leaflet-measure {
|
| 5175 |
+
background: transparent;
|
| 5176 |
+
border: none;
|
| 5177 |
+
box-shadow: none;
|
| 5178 |
+
}
|
| 5179 |
+
|
| 5180 |
+
.leaflet-map-host .mesa-leaflet-measure .mesa-leaflet-measure-toggle {
|
| 5181 |
+
width: 38px;
|
| 5182 |
+
height: 38px;
|
| 5183 |
+
position: relative;
|
| 5184 |
+
display: block;
|
| 5185 |
+
background: rgba(255, 255, 255, 0.96);
|
| 5186 |
+
border: 1px solid rgba(193, 207, 220, 0.95);
|
| 5187 |
+
border-radius: 10px;
|
| 5188 |
+
box-shadow: 0 10px 24px rgba(27, 48, 72, 0.16);
|
| 5189 |
+
}
|
| 5190 |
+
|
| 5191 |
+
.leaflet-map-host .mesa-leaflet-measure .mesa-leaflet-measure-toggle::before {
|
| 5192 |
+
content: '';
|
| 5193 |
+
position: absolute;
|
| 5194 |
+
inset: 0;
|
| 5195 |
+
margin: auto;
|
| 5196 |
+
width: 18px;
|
| 5197 |
+
height: 18px;
|
| 5198 |
+
background-repeat: no-repeat;
|
| 5199 |
+
background-position: center;
|
| 5200 |
+
background-size: 18px 18px;
|
| 5201 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2324405b' d='M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25Zm2.92 2.33H5v-.92l8.12-8.12.92.92-8.12 8.12ZM20.71 7.04a1.003 1.003 0 0 0 0-1.42l-2.34-2.33a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.83Z'/%3E%3C/svg%3E");
|
| 5202 |
+
}
|
| 5203 |
+
|
| 5204 |
+
.leaflet-map-host .mesa-leaflet-measure .mesa-leaflet-measure-toggle:hover {
|
| 5205 |
+
background: #eef5fb;
|
| 5206 |
+
}
|
| 5207 |
+
|
| 5208 |
+
.leaflet-map-host .mesa-leaflet-measure-interaction {
|
| 5209 |
+
width: min(270px, calc(100vw - 36px));
|
| 5210 |
+
margin-top: 8px;
|
| 5211 |
+
padding: 10px 12px 12px;
|
| 5212 |
+
background: rgba(255, 255, 255, 0.97);
|
| 5213 |
+
border: 1px solid rgba(188, 201, 214, 0.92);
|
| 5214 |
+
border-radius: 12px;
|
| 5215 |
+
box-shadow: 0 12px 28px rgba(24, 46, 71, 0.16);
|
| 5216 |
+
backdrop-filter: blur(6px);
|
| 5217 |
+
}
|
| 5218 |
+
|
| 5219 |
+
.leaflet-map-host .mesa-leaflet-measure-title {
|
| 5220 |
+
margin: 0 0 6px;
|
| 5221 |
+
font-size: 0.84rem;
|
| 5222 |
+
font-weight: 700;
|
| 5223 |
+
color: #24405b;
|
| 5224 |
+
}
|
| 5225 |
+
|
| 5226 |
+
.leaflet-map-host .mesa-leaflet-measure-hint {
|
| 5227 |
+
margin: 0;
|
| 5228 |
+
font-size: 0.74rem;
|
| 5229 |
+
line-height: 1.4;
|
| 5230 |
+
color: #4a6077;
|
| 5231 |
+
}
|
| 5232 |
+
|
| 5233 |
+
.leaflet-map-host .mesa-leaflet-measure-results {
|
| 5234 |
+
margin-top: 10px;
|
| 5235 |
+
display: grid;
|
| 5236 |
+
gap: 8px;
|
| 5237 |
+
}
|
| 5238 |
+
|
| 5239 |
+
.leaflet-map-host .mesa-leaflet-measure-row {
|
| 5240 |
+
display: grid;
|
| 5241 |
+
gap: 2px;
|
| 5242 |
+
padding: 7px 8px;
|
| 5243 |
+
border-radius: 10px;
|
| 5244 |
+
background: #f5f8fc;
|
| 5245 |
+
border: 1px solid #dce5ef;
|
| 5246 |
+
color: #24405b;
|
| 5247 |
+
font-size: 0.74rem;
|
| 5248 |
+
line-height: 1.35;
|
| 5249 |
+
}
|
| 5250 |
+
|
| 5251 |
+
.leaflet-map-host .mesa-leaflet-measure-row strong {
|
| 5252 |
+
font-size: 0.73rem;
|
| 5253 |
+
color: #1f4d7a;
|
| 5254 |
+
}
|
| 5255 |
+
|
| 5256 |
+
.leaflet-map-host .mesa-leaflet-measure-actions {
|
| 5257 |
+
display: flex;
|
| 5258 |
+
flex-wrap: wrap;
|
| 5259 |
+
gap: 6px;
|
| 5260 |
+
margin-top: 10px;
|
| 5261 |
+
}
|
| 5262 |
+
|
| 5263 |
+
.leaflet-map-host .mesa-leaflet-measure-btn {
|
| 5264 |
+
appearance: none;
|
| 5265 |
+
border: 1px solid #c8d6e4;
|
| 5266 |
+
border-radius: 8px;
|
| 5267 |
+
background: #fff;
|
| 5268 |
+
color: #24405b;
|
| 5269 |
+
font: inherit;
|
| 5270 |
+
font-size: 0.73rem;
|
| 5271 |
+
font-weight: 600;
|
| 5272 |
+
line-height: 1;
|
| 5273 |
+
padding: 7px 10px;
|
| 5274 |
+
cursor: pointer;
|
| 5275 |
+
}
|
| 5276 |
+
|
| 5277 |
+
.leaflet-map-host .mesa-leaflet-measure-btn:hover {
|
| 5278 |
+
background: #f3f7fb;
|
| 5279 |
+
}
|
| 5280 |
+
|
| 5281 |
+
.leaflet-map-host .mesa-leaflet-measure-btn.is-primary {
|
| 5282 |
+
background: #1f6fb2;
|
| 5283 |
+
border-color: #1f6fb2;
|
| 5284 |
+
color: #fff;
|
| 5285 |
+
}
|
| 5286 |
+
|
| 5287 |
+
.leaflet-map-host .mesa-leaflet-measure-btn.is-primary:hover {
|
| 5288 |
+
background: #175a90;
|
| 5289 |
+
}
|
| 5290 |
+
|
| 5291 |
.table-wrapper {
|
| 5292 |
overflow: auto;
|
| 5293 |
border: 1px solid #d8e2ec;
|