Spaces:
Sleeping
Sleeping
Guilherme Silberfarb Costa commited on
Commit ·
3e507b3
1
Parent(s): 1c85047
alteracoes generalizadas
Browse files- backend/app/api/elaboracao.py +11 -0
- backend/app/api/pesquisa.py +2 -0
- backend/app/core/elaboracao/charts.py +3 -68
- backend/app/core/elaboracao/formatadores.py +36 -1
- backend/app/core/map_jitter.py +79 -0
- backend/app/core/map_layers.py +221 -58
- backend/app/core/visualizacao/app.py +14 -64
- backend/app/core/visualizacao/map_payload.py +245 -102
- backend/app/services/elaboracao_service.py +68 -8
- backend/app/services/pesquisa_service.py +118 -24
- backend/app/services/trabalhos_tecnicos_service.py +4 -4
- backend/app/services/visualizacao_service.py +113 -9
- frontend/src/api.js +2 -0
- frontend/src/components/AvaliacaoTab.jsx +38 -0
- frontend/src/components/ElaboracaoTab.jsx +74 -7
- frontend/src/components/LeafletMapFrame.jsx +185 -16
- frontend/src/components/PesquisaTab.jsx +68 -4
- frontend/src/components/RepositorioTab.jsx +37 -4
- frontend/src/styles.css +171 -3
backend/app/api/elaboracao.py
CHANGED
|
@@ -36,6 +36,11 @@ class MapCoordsPayload(SessionPayload):
|
|
| 36 |
col_lon: str
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
class GeocodePayload(SessionPayload):
|
| 40 |
col_cdlog: str
|
| 41 |
col_num: str
|
|
@@ -562,6 +567,12 @@ def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
|
| 562 |
return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa, payload.modo_mapa)
|
| 563 |
|
| 564 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
@router.post("/residuos/map/update")
|
| 566 |
def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
| 567 |
session = session_store.get(payload.session_id)
|
|
|
|
| 36 |
col_lon: str
|
| 37 |
|
| 38 |
|
| 39 |
+
class MapaPopupPayload(SessionPayload):
|
| 40 |
+
row_id: int
|
| 41 |
+
source: str | None = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
class GeocodePayload(SessionPayload):
|
| 45 |
col_cdlog: str
|
| 46 |
col_num: str
|
|
|
|
| 567 |
return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa, payload.modo_mapa)
|
| 568 |
|
| 569 |
|
| 570 |
+
@router.post("/map/popup")
|
| 571 |
+
def map_popup(payload: MapaPopupPayload) -> dict[str, Any]:
|
| 572 |
+
session = session_store.get(payload.session_id)
|
| 573 |
+
return elaboracao_service.carregar_popup_ponto_mapa(session, payload.row_id, payload.source)
|
| 574 |
+
|
| 575 |
+
|
| 576 |
@router.post("/residuos/map/update")
|
| 577 |
def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
| 578 |
session = session_store.get(payload.session_id)
|
backend/app/api/pesquisa.py
CHANGED
|
@@ -55,6 +55,7 @@ class MapaModelosPayload(BaseModel):
|
|
| 55 |
avaliando_lon: Any = None
|
| 56 |
avaliandos: list[AvaliandoGeoPayload] | None = None
|
| 57 |
modo_exibicao: Any = "pontos"
|
|
|
|
| 58 |
criterio_espacial: Any = None
|
| 59 |
trabalhos_tecnicos_modelos_modo: Any = None
|
| 60 |
trabalhos_tecnicos_proximidade_modo: Any = None
|
|
@@ -217,6 +218,7 @@ def pesquisar_mapa_modelos(payload: MapaModelosPayload) -> dict:
|
|
| 217 |
avaliando_lon=payload.avaliando_lon,
|
| 218 |
avaliandos=[item.model_dump() for item in (payload.avaliandos or [])],
|
| 219 |
modo_exibicao=payload.modo_exibicao,
|
|
|
|
| 220 |
criterio_espacial=payload.criterio_espacial,
|
| 221 |
trabalhos_tecnicos_modelos_modo=modelos_modo,
|
| 222 |
trabalhos_tecnicos_proximidade_modo=proximidade_modo,
|
|
|
|
| 55 |
avaliando_lon: Any = None
|
| 56 |
avaliandos: list[AvaliandoGeoPayload] | None = None
|
| 57 |
modo_exibicao: Any = "pontos"
|
| 58 |
+
agrupar_pontos_mercado: Any = False
|
| 59 |
criterio_espacial: Any = None
|
| 60 |
trabalhos_tecnicos_modelos_modo: Any = None
|
| 61 |
trabalhos_tecnicos_proximidade_modo: Any = None
|
|
|
|
| 218 |
avaliando_lon=payload.avaliando_lon,
|
| 219 |
avaliandos=[item.model_dump() for item in (payload.avaliandos or [])],
|
| 220 |
modo_exibicao=payload.modo_exibicao,
|
| 221 |
+
agrupar_pontos_mercado=payload.agrupar_pontos_mercado,
|
| 222 |
criterio_espacial=payload.criterio_espacial,
|
| 223 |
trabalhos_tecnicos_modelos_modo=modelos_modo,
|
| 224 |
trabalhos_tecnicos_proximidade_modo=proximidade_modo,
|
backend/app/core/elaboracao/charts.py
CHANGED
|
@@ -17,6 +17,7 @@ import branca.colormap as cm
|
|
| 17 |
from branca.element import Element
|
| 18 |
from html import escape
|
| 19 |
from typing import Any
|
|
|
|
| 20 |
from app.core.map_layers import (
|
| 21 |
add_bairros_layer,
|
| 22 |
add_indice_marker,
|
|
@@ -622,72 +623,6 @@ def _mascara_dentro_poligono(x_grid: np.ndarray, y_grid: np.ndarray, poligono: n
|
|
| 622 |
return inside
|
| 623 |
|
| 624 |
|
| 625 |
-
def _aplicar_jitter_sobrepostos(
|
| 626 |
-
df_mapa: pd.DataFrame,
|
| 627 |
-
lat_col: str,
|
| 628 |
-
lon_col: str,
|
| 629 |
-
lat_plot_col: str,
|
| 630 |
-
lon_plot_col: str,
|
| 631 |
-
) -> pd.DataFrame:
|
| 632 |
-
"""
|
| 633 |
-
Aplica jitter visual apenas em pontos com coordenadas idênticas.
|
| 634 |
-
Mantém as coordenadas originais intactas para cálculos e filtros.
|
| 635 |
-
"""
|
| 636 |
-
df_plot = df_mapa.copy()
|
| 637 |
-
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 638 |
-
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
| 639 |
-
|
| 640 |
-
if len(df_plot) <= 1:
|
| 641 |
-
return df_plot
|
| 642 |
-
|
| 643 |
-
chave_lat = df_plot[lat_col].round(7)
|
| 644 |
-
chave_lon = df_plot[lon_col].round(7)
|
| 645 |
-
grupos = df_plot.groupby([chave_lat, chave_lon], sort=False)
|
| 646 |
-
|
| 647 |
-
passo_metros = 4.0
|
| 648 |
-
max_raio_metros = 22.0
|
| 649 |
-
metros_por_grau_lat = 111_320.0
|
| 650 |
-
|
| 651 |
-
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 652 |
-
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 653 |
-
|
| 654 |
-
for _, idx_labels in grupos.indices.items():
|
| 655 |
-
posicoes = np.asarray(idx_labels, dtype=int)
|
| 656 |
-
if posicoes.size <= 1:
|
| 657 |
-
continue
|
| 658 |
-
base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos])
|
| 659 |
-
base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos])
|
| 660 |
-
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 661 |
-
continue
|
| 662 |
-
|
| 663 |
-
seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360
|
| 664 |
-
angulo_base = math.radians(seed_val)
|
| 665 |
-
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 666 |
-
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 667 |
-
|
| 668 |
-
for pos, pos_idx in enumerate(posicoes):
|
| 669 |
-
if pos == 0:
|
| 670 |
-
continue
|
| 671 |
-
|
| 672 |
-
pos_ring = pos - 1
|
| 673 |
-
ring = 1
|
| 674 |
-
while pos_ring >= (6 * ring):
|
| 675 |
-
pos_ring -= 6 * ring
|
| 676 |
-
ring += 1
|
| 677 |
-
|
| 678 |
-
slots_ring = max(6 * ring, 1)
|
| 679 |
-
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 680 |
-
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 681 |
-
|
| 682 |
-
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 683 |
-
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 684 |
-
|
| 685 |
-
df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat
|
| 686 |
-
df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon
|
| 687 |
-
|
| 688 |
-
return df_plot
|
| 689 |
-
|
| 690 |
-
|
| 691 |
def _montar_popup_registro_em_colunas(
|
| 692 |
idx: Any,
|
| 693 |
row: pd.Series,
|
|
@@ -1100,8 +1035,8 @@ def criar_mapa(
|
|
| 1100 |
)
|
| 1101 |
|
| 1102 |
# Camadas base
|
| 1103 |
-
folium.TileLayer(tiles="
|
| 1104 |
-
folium.TileLayer(tiles="
|
| 1105 |
add_bairros_layer(m, show=True)
|
| 1106 |
|
| 1107 |
modo_normalizado = str(modo or "pontos").strip().lower()
|
|
|
|
| 17 |
from branca.element import Element
|
| 18 |
from html import escape
|
| 19 |
from typing import Any
|
| 20 |
+
from app.core.map_jitter import aplicar_jitter_dados_mercado as _aplicar_jitter_sobrepostos
|
| 21 |
from app.core.map_layers import (
|
| 22 |
add_bairros_layer,
|
| 23 |
add_indice_marker,
|
|
|
|
| 623 |
return inside
|
| 624 |
|
| 625 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
def _montar_popup_registro_em_colunas(
|
| 627 |
idx: Any,
|
| 628 |
row: pd.Series,
|
|
|
|
| 1035 |
)
|
| 1036 |
|
| 1037 |
# Camadas base
|
| 1038 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(m)
|
| 1039 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(m)
|
| 1040 |
add_bairros_layer(m, show=True)
|
| 1041 |
|
| 1042 |
modo_normalizado = str(modo or "pontos").strip().lower()
|
backend/app/core/elaboracao/formatadores.py
CHANGED
|
@@ -7,6 +7,7 @@ Sem dependência de Gradio.
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
|
|
|
| 10 |
from html import escape
|
| 11 |
import numpy as np
|
| 12 |
import pandas as pd
|
|
@@ -23,11 +24,44 @@ TITULO = """
|
|
| 23 |
---
|
| 24 |
"""
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# ============================================================
|
| 28 |
# FUNÇÕES DE FORMATAÇÃO
|
| 29 |
# ============================================================
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def arredondar_df(df, decimais=4):
|
| 32 |
"""Arredonda apenas colunas numéricas de um DataFrame e adiciona coluna Índice."""
|
| 33 |
if df is None:
|
|
@@ -143,7 +177,8 @@ def formatar_diagnosticos_html(diagnosticos):
|
|
| 143 |
|
| 144 |
# Teste de Normalidade (Curva Normal)
|
| 145 |
html += '<div class="section-title-orange">Teste de Normalidade (Comparação com a Curva Normal)</div>'
|
| 146 |
-
|
|
|
|
| 147 |
html += '<div class="interpretation-label">Interpretação</div>'
|
| 148 |
html += '<div class="interpretation-item">• Ideal 68% → aceitável entre 64% e 75%</div>'
|
| 149 |
html += '<div class="interpretation-item">• Ideal 90% → aceitável entre 88% e 95%</div>'
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
+
import re
|
| 11 |
from html import escape
|
| 12 |
import numpy as np
|
| 13 |
import pandas as pd
|
|
|
|
| 24 |
---
|
| 25 |
"""
|
| 26 |
|
| 27 |
+
CURVA_NORMAL_PERCENTUAL_FAIXAS = (
|
| 28 |
+
(64, 75),
|
| 29 |
+
(88, 95),
|
| 30 |
+
(95, 100),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
|
| 34 |
# ============================================================
|
| 35 |
# FUNÇÕES DE FORMATAÇÃO
|
| 36 |
# ============================================================
|
| 37 |
|
| 38 |
+
def _formatar_percentuais_curva_normal_html(perc_resid):
|
| 39 |
+
texto = str(perc_resid or "").strip()
|
| 40 |
+
if not texto:
|
| 41 |
+
return "-"
|
| 42 |
+
|
| 43 |
+
partes = [parte.strip() for parte in texto.split(",") if parte.strip()]
|
| 44 |
+
if not partes:
|
| 45 |
+
return escape(texto)
|
| 46 |
+
|
| 47 |
+
formatados = []
|
| 48 |
+
for idx, parte in enumerate(partes):
|
| 49 |
+
parte_html = escape(parte)
|
| 50 |
+
faixa = CURVA_NORMAL_PERCENTUAL_FAIXAS[idx] if idx < len(CURVA_NORMAL_PERCENTUAL_FAIXAS) else None
|
| 51 |
+
match = re.search(r"-?\d+(?:[.,]\d+)?", parte)
|
| 52 |
+
valor = None
|
| 53 |
+
if match:
|
| 54 |
+
try:
|
| 55 |
+
valor = float(match.group(0).replace(",", "."))
|
| 56 |
+
except Exception:
|
| 57 |
+
valor = None
|
| 58 |
+
fora_da_faixa = faixa is not None and valor is not None and (valor < faixa[0] or valor > faixa[1])
|
| 59 |
+
if fora_da_faixa:
|
| 60 |
+
parte_html = f'<span style="color:#b42318;font-weight:800;">{parte_html}</span>'
|
| 61 |
+
formatados.append(parte_html)
|
| 62 |
+
|
| 63 |
+
return ", ".join(formatados)
|
| 64 |
+
|
| 65 |
def arredondar_df(df, decimais=4):
|
| 66 |
"""Arredonda apenas colunas numéricas de um DataFrame e adiciona coluna Índice."""
|
| 67 |
if df is None:
|
|
|
|
| 177 |
|
| 178 |
# Teste de Normalidade (Curva Normal)
|
| 179 |
html += '<div class="section-title-orange">Teste de Normalidade (Comparação com a Curva Normal)</div>'
|
| 180 |
+
perc_resid_html = _formatar_percentuais_curva_normal_html(diagnosticos.get("perc_resid"))
|
| 181 |
+
html += f'''<div class="field-row"><span class="field-row-label">Percentuais Atingidos</span><span class="field-row-value">{perc_resid_html}</span></div>'''
|
| 182 |
html += '<div class="interpretation-label">Interpretação</div>'
|
| 183 |
html += '<div class="interpretation-item">• Ideal 68% → aceitável entre 64% e 75%</div>'
|
| 184 |
html += '<div class="interpretation-item">• Ideal 90% → aceitável entre 88% e 95%</div>'
|
backend/app/core/map_jitter.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pandas as pd
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
JITTER_MERCADO_PASSO_METROS = 5.0
|
| 10 |
+
JITTER_MERCADO_MAX_RAIO_METROS = 28.0
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def aplicar_jitter_dados_mercado(
|
| 14 |
+
df_mapa: pd.DataFrame,
|
| 15 |
+
lat_col: str,
|
| 16 |
+
lon_col: str,
|
| 17 |
+
lat_plot_col: str = "__mesa_lat_plot__",
|
| 18 |
+
lon_plot_col: str = "__mesa_lon_plot__",
|
| 19 |
+
*,
|
| 20 |
+
precisao: int = 7,
|
| 21 |
+
passo_metros: float = JITTER_MERCADO_PASSO_METROS,
|
| 22 |
+
max_raio_metros: float = JITTER_MERCADO_MAX_RAIO_METROS,
|
| 23 |
+
) -> pd.DataFrame:
|
| 24 |
+
"""
|
| 25 |
+
Aplica jitter visual determinístico apenas em pontos de mercado sobrepostos.
|
| 26 |
+
|
| 27 |
+
As coordenadas originais permanecem nas colunas de entrada; as coordenadas
|
| 28 |
+
deslocadas são gravadas em lat_plot_col/lon_plot_col.
|
| 29 |
+
"""
|
| 30 |
+
df_plot = df_mapa.copy()
|
| 31 |
+
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 32 |
+
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
| 33 |
+
|
| 34 |
+
if len(df_plot) <= 1:
|
| 35 |
+
return df_plot
|
| 36 |
+
|
| 37 |
+
chave_lat = df_plot[lat_col].round(precisao)
|
| 38 |
+
chave_lon = df_plot[lon_col].round(precisao)
|
| 39 |
+
grupos = df_plot.groupby([chave_lat, chave_lon], sort=False)
|
| 40 |
+
|
| 41 |
+
metros_por_grau_lat = 111_320.0
|
| 42 |
+
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 43 |
+
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 44 |
+
|
| 45 |
+
for _, idx_labels in grupos.indices.items():
|
| 46 |
+
posicoes = np.asarray(idx_labels, dtype=int)
|
| 47 |
+
if posicoes.size <= 1:
|
| 48 |
+
continue
|
| 49 |
+
|
| 50 |
+
base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos])
|
| 51 |
+
base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos])
|
| 52 |
+
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360
|
| 56 |
+
angulo_base = math.radians(seed_val)
|
| 57 |
+
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 58 |
+
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 59 |
+
|
| 60 |
+
for pos, pos_idx in enumerate(posicoes):
|
| 61 |
+
if pos == 0:
|
| 62 |
+
continue
|
| 63 |
+
|
| 64 |
+
pos_ring = pos - 1
|
| 65 |
+
ring = 1
|
| 66 |
+
while pos_ring >= (6 * ring):
|
| 67 |
+
pos_ring -= 6 * ring
|
| 68 |
+
ring += 1
|
| 69 |
+
|
| 70 |
+
slots_ring = max(6 * ring, 1)
|
| 71 |
+
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 72 |
+
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 73 |
+
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 74 |
+
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 75 |
+
|
| 76 |
+
df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat
|
| 77 |
+
df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon
|
| 78 |
+
|
| 79 |
+
return df_plot
|
backend/app/core/map_layers.py
CHANGED
|
@@ -292,6 +292,201 @@ def apply_marker_payload_jitter(
|
|
| 292 |
return payloads
|
| 293 |
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
def build_trabalhos_tecnicos_marker_payloads(
|
| 296 |
trabalhos: list[dict[str, Any]] | None,
|
| 297 |
*,
|
|
@@ -299,6 +494,7 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 299 |
marker_style: str = "estrela",
|
| 300 |
ignore_bounds: bool = True,
|
| 301 |
apply_jitter: bool = True,
|
|
|
|
| 302 |
) -> list[dict[str, Any]]:
|
| 303 |
payloads: list[dict[str, Any]] = []
|
| 304 |
for item in trabalhos or []:
|
|
@@ -337,24 +533,13 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 337 |
)
|
| 338 |
distancia_min_label = str(item.get("distancia_label_min") or "").strip()
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
"style='color:#7d5a00;text-decoration:underline;font-weight:700;' "
|
| 348 |
-
f"onclick='try{{(window.parent || window).postMessage({payload_json}, \"*\");}}catch(_error){{}} return false;'>"
|
| 349 |
-
f"{escape(trabalho_nome)}"
|
| 350 |
-
"</a>"
|
| 351 |
-
)
|
| 352 |
-
else:
|
| 353 |
-
trabalho_nome_html = (
|
| 354 |
-
"<span style='font-weight:700; color:#7d5a00;'>"
|
| 355 |
-
f"{escape(trabalho_nome)}"
|
| 356 |
-
"</span>"
|
| 357 |
-
)
|
| 358 |
|
| 359 |
detalhes_html = (
|
| 360 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.55; min-width:260px;'>"
|
|
@@ -375,58 +560,34 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 375 |
+ "</div>"
|
| 376 |
)
|
| 377 |
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 381 |
-
"width:14px;height:14px;'>"
|
| 382 |
-
"<svg width='14' height='14' viewBox='0 0 24 24' aria-hidden='true'>"
|
| 383 |
-
"<polygon points='12,1.8 15.2,8.2 22.2,9.2 17.1,14.1 18.3,21.1 "
|
| 384 |
-
"12,17.8 5.7,21.1 6.9,14.1 1.8,9.2 8.8,8.2' "
|
| 385 |
-
"fill='#c62828' stroke='#000000' stroke-width='1.4' stroke-linejoin='round'/>"
|
| 386 |
-
"</svg></div>"
|
| 387 |
-
)
|
| 388 |
-
payloads.append(
|
| 389 |
-
{
|
| 390 |
-
"lat": lat,
|
| 391 |
-
"lon": lon,
|
| 392 |
-
"trabalho_id": trabalho_id,
|
| 393 |
-
"trabalho_nome": trabalho_nome,
|
| 394 |
-
"tooltip_html": detalhes_html,
|
| 395 |
-
"popup_html": detalhes_html,
|
| 396 |
-
"source_overlay_ids": modelos_origem_ids,
|
| 397 |
-
"marker_html": marker_html,
|
| 398 |
-
"marker_style": "estrela",
|
| 399 |
-
"ignore_bounds": bool(ignore_bounds),
|
| 400 |
-
"icon_size": [14, 14],
|
| 401 |
-
"icon_anchor": [7, 7],
|
| 402 |
-
"class_name": "mesa-trabalho-tecnico-marker",
|
| 403 |
-
}
|
| 404 |
-
)
|
| 405 |
-
continue
|
| 406 |
-
|
| 407 |
-
marker_html = (
|
| 408 |
-
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 409 |
-
"width:8px;height:8px;border-radius:999px;background:#1f6fb2;"
|
| 410 |
-
"border:1px solid #ffffff;box-shadow:0 0 0 1px rgba(20,42,66,0.20);'></div>"
|
| 411 |
-
)
|
| 412 |
payloads.append(
|
| 413 |
{
|
| 414 |
"lat": lat,
|
| 415 |
"lon": lon,
|
| 416 |
"trabalho_id": trabalho_id,
|
| 417 |
"trabalho_nome": trabalho_nome,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
"tooltip_html": detalhes_html,
|
| 419 |
"popup_html": detalhes_html,
|
| 420 |
"source_overlay_ids": modelos_origem_ids,
|
| 421 |
"marker_html": marker_html,
|
| 422 |
-
"marker_style":
|
| 423 |
"ignore_bounds": bool(ignore_bounds),
|
| 424 |
-
"icon_size": [
|
| 425 |
-
"icon_anchor": [
|
| 426 |
-
"class_name": "mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-marker-
|
| 427 |
}
|
| 428 |
)
|
| 429 |
|
|
|
|
|
|
|
|
|
|
| 430 |
if apply_jitter:
|
| 431 |
apply_marker_payload_jitter(payloads)
|
| 432 |
|
|
@@ -440,8 +601,8 @@ def add_marker_payloads(
|
|
| 440 |
for item in payloads or []:
|
| 441 |
marcador = folium.Marker(
|
| 442 |
location=[float(item["lat"]), float(item["lon"])],
|
| 443 |
-
tooltip=folium.Tooltip(str(item.get("tooltip_html") or ""), sticky=
|
| 444 |
-
popup=folium.Popup(str(item.get("popup_html") or ""), max_width=360),
|
| 445 |
icon=folium.DivIcon(
|
| 446 |
html=str(item.get("marker_html") or ""),
|
| 447 |
icon_size=tuple(item.get("icon_size") or [14, 14]),
|
|
@@ -461,6 +622,7 @@ def add_trabalhos_tecnicos_markers(
|
|
| 461 |
marker_style: str = "estrela",
|
| 462 |
ignore_bounds: bool = True,
|
| 463 |
apply_jitter: bool = True,
|
|
|
|
| 464 |
) -> None:
|
| 465 |
payloads = build_trabalhos_tecnicos_marker_payloads(
|
| 466 |
trabalhos,
|
|
@@ -468,6 +630,7 @@ def add_trabalhos_tecnicos_markers(
|
|
| 468 |
marker_style=marker_style,
|
| 469 |
ignore_bounds=ignore_bounds,
|
| 470 |
apply_jitter=apply_jitter,
|
|
|
|
| 471 |
)
|
| 472 |
add_marker_payloads(camada, payloads)
|
| 473 |
|
|
|
|
| 292 |
return payloads
|
| 293 |
|
| 294 |
|
| 295 |
+
def _marker_payload_coord_key(item: dict[str, Any], lat_key: str = "lat", lon_key: str = "lon") -> tuple[float, float] | None:
|
| 296 |
+
try:
|
| 297 |
+
lat = float(item.get(lat_key))
|
| 298 |
+
lon = float(item.get(lon_key))
|
| 299 |
+
except Exception:
|
| 300 |
+
return None
|
| 301 |
+
if not math.isfinite(lat) or not math.isfinite(lon):
|
| 302 |
+
return None
|
| 303 |
+
return (round(lat, 7), round(lon, 7))
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def _dedupe_ordered(values: list[Any]) -> list[str]:
|
| 307 |
+
resultado: list[str] = []
|
| 308 |
+
vistos: set[str] = set()
|
| 309 |
+
for value in values:
|
| 310 |
+
texto = str(value or "").strip()
|
| 311 |
+
if not texto or texto in vistos:
|
| 312 |
+
continue
|
| 313 |
+
vistos.add(texto)
|
| 314 |
+
resultado.append(texto)
|
| 315 |
+
return resultado
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _trabalho_tecnico_group_sort_key(item: dict[str, Any]) -> tuple[str, str, str]:
|
| 319 |
+
return (
|
| 320 |
+
str(item.get("trabalho_nome") or "").strip().casefold(),
|
| 321 |
+
str(item.get("endereco_texto") or "").strip().casefold(),
|
| 322 |
+
str(item.get("trabalho_id") or "").strip().casefold(),
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def _trabalho_tecnico_group_marker_html(total: int, marker_style: str) -> str:
|
| 327 |
+
total_label = "99+" if total > 99 else str(total)
|
| 328 |
+
return (
|
| 329 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 330 |
+
"width:24px;height:24px;box-sizing:border-box;border-radius:999px;"
|
| 331 |
+
"background:#c62828;border:2px solid #ffffff;color:#fff;"
|
| 332 |
+
"box-shadow:0 0 0 1px rgba(22,38,52,0.22),0 4px 12px rgba(21,35,50,0.28);"
|
| 333 |
+
"font-family:\"Segoe UI\",Arial,sans-serif;font-size:11px;font-weight:800;line-height:1;'>"
|
| 334 |
+
"<span style='display:block;min-width:14px;padding:1px 2px;text-align:center;"
|
| 335 |
+
"pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,0.45);'>"
|
| 336 |
+
f"{escape(total_label)}"
|
| 337 |
+
"</span></div>"
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def _trabalho_tecnico_single_marker_html() -> str:
|
| 342 |
+
return (
|
| 343 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 344 |
+
"width:14px;height:14px;box-sizing:border-box;border-radius:999px;background:#c62828;"
|
| 345 |
+
"border:2px solid #ffffff;box-shadow:0 0 0 1px rgba(22,38,52,0.22),0 3px 9px rgba(21,35,50,0.24);'>"
|
| 346 |
+
"</div>"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def _trabalho_tecnico_link_html(item: dict[str, Any]) -> str:
|
| 351 |
+
trabalho_id = str(item.get("trabalho_id") or "").strip()
|
| 352 |
+
trabalho_nome = str(item.get("trabalho_nome") or trabalho_id or "Trabalho tecnico").strip()
|
| 353 |
+
origem = str(item.get("origem") or "pesquisa_mapa").strip() or "pesquisa_mapa"
|
| 354 |
+
if not trabalho_id:
|
| 355 |
+
return f"<span style='font-weight:700; color:#7d5a00;'>{escape(trabalho_nome)}</span>"
|
| 356 |
+
payload_json = json.dumps(
|
| 357 |
+
{"type": "mesa:open-trabalho-tecnico", "trabalhoId": trabalho_id, "origem": origem},
|
| 358 |
+
ensure_ascii=True,
|
| 359 |
+
)
|
| 360 |
+
return (
|
| 361 |
+
"<a href='#' "
|
| 362 |
+
"style='color:#7d5a00;text-decoration:underline;font-weight:700;' "
|
| 363 |
+
f"onclick='try{{(window.parent || window).postMessage({payload_json}, \"*\");}}catch(_error){{}} return false;'>"
|
| 364 |
+
f"{escape(trabalho_nome)}"
|
| 365 |
+
"</a>"
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _trabalho_tecnico_group_tooltip_html(itens: list[dict[str, Any]]) -> str:
|
| 370 |
+
total = len(itens)
|
| 371 |
+
preview = itens[:8]
|
| 372 |
+
linhas = []
|
| 373 |
+
for item in preview:
|
| 374 |
+
nome = str(item.get("trabalho_nome") or item.get("trabalho_id") or "Trabalho tecnico").strip()
|
| 375 |
+
endereco = str(item.get("endereco_texto") or "").strip()
|
| 376 |
+
linhas.append(
|
| 377 |
+
"<li style='margin:2px 0;'>"
|
| 378 |
+
f"<strong>{escape(nome)}</strong>"
|
| 379 |
+
+ (f"<br><span style='color:#666;'>{escape(endereco)}</span>" if endereco else "")
|
| 380 |
+
+ "</li>"
|
| 381 |
+
)
|
| 382 |
+
restante = total - len(preview)
|
| 383 |
+
if restante > 0:
|
| 384 |
+
linhas.append(f"<li style='margin:2px 0;color:#666;'>+ {restante} trabalho(s)</li>")
|
| 385 |
+
return (
|
| 386 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.45; min-width:260px;'>"
|
| 387 |
+
f"<div style='margin-bottom:6px;'><b>{total} trabalhos técnicos neste local</b></div>"
|
| 388 |
+
"<ul style='margin:0;padding-left:16px;'>"
|
| 389 |
+
+ "".join(linhas)
|
| 390 |
+
+ "</ul></div>"
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def _trabalho_tecnico_group_popup_html(itens: list[dict[str, Any]]) -> str:
|
| 395 |
+
total = len(itens)
|
| 396 |
+
linhas = []
|
| 397 |
+
for item in itens:
|
| 398 |
+
endereco = str(item.get("endereco_texto") or "Endereco nao informado").strip()
|
| 399 |
+
tipo_label = str(item.get("tipo_label") or "").strip()
|
| 400 |
+
distancia = str(item.get("distancia_min_label") or "").strip()
|
| 401 |
+
modelos = str(item.get("modelos_texto") or "").strip()
|
| 402 |
+
linhas.append(
|
| 403 |
+
"<div style='padding:9px 0;border-top:1px solid #e6edf3;'>"
|
| 404 |
+
f"<div style='margin-bottom:4px;'>{_trabalho_tecnico_link_html(item)}</div>"
|
| 405 |
+
f"<div><span style='color:#666;'>Endereço:</span> {escape(endereco)}</div>"
|
| 406 |
+
+ (f"<div><span style='color:#666;'>Tipo:</span> {escape(tipo_label)}</div>" if tipo_label else "")
|
| 407 |
+
+ (f"<div><span style='color:#666;'>Menor distância:</span> {escape(distancia)}</div>" if distancia else "")
|
| 408 |
+
+ (f"<div><span style='color:#666;'>Modelos:</span> {escape(modelos)}</div>" if modelos else "")
|
| 409 |
+
+ "</div>"
|
| 410 |
+
)
|
| 411 |
+
lista_style = (
|
| 412 |
+
"max-height:260px;overflow-y:auto;padding-right:6px;"
|
| 413 |
+
if total >= 3
|
| 414 |
+
else ""
|
| 415 |
+
)
|
| 416 |
+
return (
|
| 417 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.5; min-width:320px; max-width:430px;'>"
|
| 418 |
+
f"<div style='margin-bottom:8px;'><b>{total} trabalhos técnicos neste local</b></div>"
|
| 419 |
+
f"<div style='{lista_style}'>"
|
| 420 |
+
+ "".join(linhas)
|
| 421 |
+
+ "</div>"
|
| 422 |
+
+ "</div>"
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _agrupar_trabalhos_tecnicos_marker_payloads(
|
| 427 |
+
payloads: list[dict[str, Any]],
|
| 428 |
+
*,
|
| 429 |
+
marker_style: str = "estrela",
|
| 430 |
+
) -> list[dict[str, Any]]:
|
| 431 |
+
if len(payloads) <= 1:
|
| 432 |
+
return payloads
|
| 433 |
+
|
| 434 |
+
grupos: dict[tuple[float, float], list[dict[str, Any]]] = {}
|
| 435 |
+
ordem: list[tuple[float, float]] = []
|
| 436 |
+
for item in payloads:
|
| 437 |
+
chave = _marker_payload_coord_key(item)
|
| 438 |
+
if chave is None:
|
| 439 |
+
continue
|
| 440 |
+
if chave not in grupos:
|
| 441 |
+
ordem.append(chave)
|
| 442 |
+
grupos.setdefault(chave, []).append(item)
|
| 443 |
+
|
| 444 |
+
if not grupos:
|
| 445 |
+
return payloads
|
| 446 |
+
|
| 447 |
+
resultado: list[dict[str, Any]] = []
|
| 448 |
+
agrupados = {chave for chave, itens in grupos.items() if len(itens) > 1}
|
| 449 |
+
for item in payloads:
|
| 450 |
+
chave = _marker_payload_coord_key(item)
|
| 451 |
+
if chave not in agrupados:
|
| 452 |
+
resultado.append(item)
|
| 453 |
+
|
| 454 |
+
for chave in ordem:
|
| 455 |
+
itens = grupos.get(chave) or []
|
| 456 |
+
if len(itens) <= 1:
|
| 457 |
+
continue
|
| 458 |
+
itens_ordenados = sorted(itens, key=_trabalho_tecnico_group_sort_key)
|
| 459 |
+
base = itens_ordenados[0]
|
| 460 |
+
source_overlay_ids = _dedupe_ordered(
|
| 461 |
+
[
|
| 462 |
+
source_id
|
| 463 |
+
for item in itens_ordenados
|
| 464 |
+
for source_id in (item.get("source_overlay_ids") or [])
|
| 465 |
+
]
|
| 466 |
+
)
|
| 467 |
+
resultado.append(
|
| 468 |
+
{
|
| 469 |
+
"lat": float(base["lat"]),
|
| 470 |
+
"lon": float(base["lon"]),
|
| 471 |
+
"trabalho_id": "",
|
| 472 |
+
"trabalho_nome": f"{len(itens_ordenados)} trabalhos técnicos",
|
| 473 |
+
"tooltip_html": _trabalho_tecnico_group_tooltip_html(itens_ordenados),
|
| 474 |
+
"tooltip_sticky": False,
|
| 475 |
+
"popup_html": _trabalho_tecnico_group_popup_html(itens_ordenados),
|
| 476 |
+
"popup_max_width": 460,
|
| 477 |
+
"source_overlay_ids": source_overlay_ids,
|
| 478 |
+
"marker_html": _trabalho_tecnico_group_marker_html(len(itens_ordenados), marker_style),
|
| 479 |
+
"marker_style": f"{str(marker_style or 'estrela').strip().lower()}-grupo",
|
| 480 |
+
"ignore_bounds": all(bool(item.get("ignore_bounds")) for item in itens_ordenados),
|
| 481 |
+
"icon_size": [24, 24],
|
| 482 |
+
"icon_anchor": [12, 12],
|
| 483 |
+
"class_name": "mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-group-marker",
|
| 484 |
+
}
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
return resultado
|
| 488 |
+
|
| 489 |
+
|
| 490 |
def build_trabalhos_tecnicos_marker_payloads(
|
| 491 |
trabalhos: list[dict[str, Any]] | None,
|
| 492 |
*,
|
|
|
|
| 494 |
marker_style: str = "estrela",
|
| 495 |
ignore_bounds: bool = True,
|
| 496 |
apply_jitter: bool = True,
|
| 497 |
+
group_overlaps: bool = True,
|
| 498 |
) -> list[dict[str, Any]]:
|
| 499 |
payloads: list[dict[str, Any]] = []
|
| 500 |
for item in trabalhos or []:
|
|
|
|
| 533 |
)
|
| 534 |
distancia_min_label = str(item.get("distancia_label_min") or "").strip()
|
| 535 |
|
| 536 |
+
trabalho_nome_html = _trabalho_tecnico_link_html(
|
| 537 |
+
{
|
| 538 |
+
"trabalho_id": trabalho_id,
|
| 539 |
+
"trabalho_nome": trabalho_nome,
|
| 540 |
+
"origem": origem,
|
| 541 |
+
}
|
| 542 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
detalhes_html = (
|
| 545 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:13px; line-height:1.55; min-width:260px;'>"
|
|
|
|
| 560 |
+ "</div>"
|
| 561 |
)
|
| 562 |
|
| 563 |
+
marker_html = _trabalho_tecnico_single_marker_html()
|
| 564 |
+
marker_style_norm = str(marker_style or "ponto").strip().lower() or "ponto"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
payloads.append(
|
| 566 |
{
|
| 567 |
"lat": lat,
|
| 568 |
"lon": lon,
|
| 569 |
"trabalho_id": trabalho_id,
|
| 570 |
"trabalho_nome": trabalho_nome,
|
| 571 |
+
"origem": origem,
|
| 572 |
+
"tipo_label": tipo_label,
|
| 573 |
+
"endereco_texto": endereco_texto,
|
| 574 |
+
"distancia_min_label": distancia_min_label,
|
| 575 |
+
"modelos_texto": modelos_texto,
|
| 576 |
"tooltip_html": detalhes_html,
|
| 577 |
"popup_html": detalhes_html,
|
| 578 |
"source_overlay_ids": modelos_origem_ids,
|
| 579 |
"marker_html": marker_html,
|
| 580 |
+
"marker_style": marker_style_norm,
|
| 581 |
"ignore_bounds": bool(ignore_bounds),
|
| 582 |
+
"icon_size": [14, 14],
|
| 583 |
+
"icon_anchor": [7, 7],
|
| 584 |
+
"class_name": "mesa-trabalho-tecnico-marker mesa-trabalho-tecnico-marker-circle",
|
| 585 |
}
|
| 586 |
)
|
| 587 |
|
| 588 |
+
if group_overlaps:
|
| 589 |
+
return _agrupar_trabalhos_tecnicos_marker_payloads(payloads, marker_style=marker_style)
|
| 590 |
+
|
| 591 |
if apply_jitter:
|
| 592 |
apply_marker_payload_jitter(payloads)
|
| 593 |
|
|
|
|
| 601 |
for item in payloads or []:
|
| 602 |
marcador = folium.Marker(
|
| 603 |
location=[float(item["lat"]), float(item["lon"])],
|
| 604 |
+
tooltip=folium.Tooltip(str(item.get("tooltip_html") or ""), sticky=item.get("tooltip_sticky") is not False),
|
| 605 |
+
popup=folium.Popup(str(item.get("popup_html") or ""), max_width=int(item.get("popup_max_width") or 360)),
|
| 606 |
icon=folium.DivIcon(
|
| 607 |
html=str(item.get("marker_html") or ""),
|
| 608 |
icon_size=tuple(item.get("icon_size") or [14, 14]),
|
|
|
|
| 622 |
marker_style: str = "estrela",
|
| 623 |
ignore_bounds: bool = True,
|
| 624 |
apply_jitter: bool = True,
|
| 625 |
+
group_overlaps: bool = True,
|
| 626 |
) -> None:
|
| 627 |
payloads = build_trabalhos_tecnicos_marker_payloads(
|
| 628 |
trabalhos,
|
|
|
|
| 630 |
marker_style=marker_style,
|
| 631 |
ignore_bounds=ignore_bounds,
|
| 632 |
apply_jitter=apply_jitter,
|
| 633 |
+
group_overlaps=group_overlaps,
|
| 634 |
)
|
| 635 |
add_marker_payloads(camada, payloads)
|
| 636 |
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -15,6 +15,7 @@ from folium import plugins
|
|
| 15 |
from scipy import stats
|
| 16 |
from statsmodels.stats.outliers_influence import OLSInfluence
|
| 17 |
|
|
|
|
| 18 |
from app.core.map_layers import (
|
| 19 |
add_bairros_layer,
|
| 20 |
add_indice_marker,
|
|
@@ -163,7 +164,7 @@ def _criar_histograma_residuos(residuos):
|
|
| 163 |
return None
|
| 164 |
|
| 165 |
|
| 166 |
-
def _criar_grafico_cook(modelos_sm):
|
| 167 |
try:
|
| 168 |
if modelos_sm is None:
|
| 169 |
return None
|
|
@@ -171,15 +172,16 @@ def _criar_grafico_cook(modelos_sm):
|
|
| 171 |
influence = OLSInfluence(modelos_sm)
|
| 172 |
cooks_d = influence.cooks_distance[0]
|
| 173 |
n = len(cooks_d)
|
| 174 |
-
indices = np.arange(1, n + 1)
|
|
|
|
| 175 |
limite = 4 / n
|
| 176 |
|
| 177 |
fig = go.Figure()
|
| 178 |
-
for
|
| 179 |
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 180 |
fig.add_trace(
|
| 181 |
go.Scatter(
|
| 182 |
-
x=[
|
| 183 |
y=[0, valor],
|
| 184 |
mode="lines",
|
| 185 |
line=dict(color=cor, width=1.5),
|
|
@@ -191,12 +193,13 @@ def _criar_grafico_cook(modelos_sm):
|
|
| 191 |
cores_pontos = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d]
|
| 192 |
fig.add_trace(
|
| 193 |
go.Scatter(
|
| 194 |
-
x=
|
| 195 |
y=cooks_d,
|
| 196 |
mode="markers",
|
| 197 |
marker=dict(color=cores_pontos, size=8, line=dict(color="black", width=1)),
|
| 198 |
name="Distância de Cook",
|
| 199 |
-
|
|
|
|
| 200 |
)
|
| 201 |
)
|
| 202 |
fig.add_hline(
|
|
@@ -208,12 +211,12 @@ def _criar_grafico_cook(modelos_sm):
|
|
| 208 |
)
|
| 209 |
fig.update_layout(
|
| 210 |
title=dict(text="Distância de Cook", x=0.5),
|
| 211 |
-
xaxis_title="Observação",
|
| 212 |
yaxis_title="Distância de Cook",
|
| 213 |
plot_bgcolor="white",
|
| 214 |
margin=dict(l=60, r=40, t=60, b=60),
|
| 215 |
)
|
| 216 |
-
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 217 |
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 218 |
return fig
|
| 219 |
except Exception as exc:
|
|
@@ -371,7 +374,7 @@ def gerar_todos_graficos(pacote):
|
|
| 371 |
if residuos is not None:
|
| 372 |
graficos["hist"] = _criar_histograma_residuos(residuos)
|
| 373 |
if modelos_sm is not None:
|
| 374 |
-
graficos["cook"] = _criar_grafico_cook(modelos_sm)
|
| 375 |
graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y)
|
| 376 |
|
| 377 |
return graficos
|
|
@@ -524,59 +527,6 @@ def formatar_escalas_html(escalas_raw):
|
|
| 524 |
)
|
| 525 |
|
| 526 |
|
| 527 |
-
def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plot_col):
|
| 528 |
-
df_plot = df_mapa.copy()
|
| 529 |
-
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 530 |
-
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
| 531 |
-
|
| 532 |
-
if len(df_plot) <= 1:
|
| 533 |
-
return df_plot
|
| 534 |
-
|
| 535 |
-
chave_lat = df_plot[lat_col].round(7)
|
| 536 |
-
chave_lon = df_plot[lon_col].round(7)
|
| 537 |
-
grupos = df_plot.groupby([chave_lat, chave_lon], sort=False)
|
| 538 |
-
|
| 539 |
-
passo_metros = 4.0
|
| 540 |
-
max_raio_metros = 22.0
|
| 541 |
-
metros_por_grau_lat = 111_320.0
|
| 542 |
-
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 543 |
-
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 544 |
-
|
| 545 |
-
for _, idx_labels in grupos.indices.items():
|
| 546 |
-
posicoes = np.asarray(idx_labels, dtype=int)
|
| 547 |
-
if posicoes.size <= 1:
|
| 548 |
-
continue
|
| 549 |
-
base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos])
|
| 550 |
-
base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos])
|
| 551 |
-
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 552 |
-
continue
|
| 553 |
-
|
| 554 |
-
seed_val = int((abs(base_lat) * 1_000_000.0) + (abs(base_lon) * 1_000_000.0) * 3.0) % 360
|
| 555 |
-
angulo_base = math.radians(seed_val)
|
| 556 |
-
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 557 |
-
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 558 |
-
|
| 559 |
-
for pos, pos_idx in enumerate(posicoes):
|
| 560 |
-
if pos == 0:
|
| 561 |
-
continue
|
| 562 |
-
pos_ring = pos - 1
|
| 563 |
-
ring = 1
|
| 564 |
-
while pos_ring >= (6 * ring):
|
| 565 |
-
pos_ring -= 6 * ring
|
| 566 |
-
ring += 1
|
| 567 |
-
|
| 568 |
-
slots_ring = max(6 * ring, 1)
|
| 569 |
-
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 570 |
-
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 571 |
-
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 572 |
-
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 573 |
-
|
| 574 |
-
df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat
|
| 575 |
-
df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon
|
| 576 |
-
|
| 577 |
-
return df_plot
|
| 578 |
-
|
| 579 |
-
|
| 580 |
def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
| 581 |
def _limitar_texto(valor: str, limite: int) -> str:
|
| 582 |
txt = str(valor)
|
|
@@ -787,8 +737,8 @@ def criar_mapa(
|
|
| 787 |
prefer_canvas=True,
|
| 788 |
control_scale=True,
|
| 789 |
)
|
| 790 |
-
folium.TileLayer(tiles="
|
| 791 |
-
folium.TileLayer(tiles="
|
| 792 |
add_bairros_layer(mapa, show=True)
|
| 793 |
|
| 794 |
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
|
|
|
|
| 15 |
from scipy import stats
|
| 16 |
from statsmodels.stats.outliers_influence import OLSInfluence
|
| 17 |
|
| 18 |
+
from app.core.map_jitter import aplicar_jitter_dados_mercado as _aplicar_jitter_sobrepostos
|
| 19 |
from app.core.map_layers import (
|
| 20 |
add_bairros_layer,
|
| 21 |
add_indice_marker,
|
|
|
|
| 164 |
return None
|
| 165 |
|
| 166 |
|
| 167 |
+
def _criar_grafico_cook(modelos_sm, indices=None):
|
| 168 |
try:
|
| 169 |
if modelos_sm is None:
|
| 170 |
return None
|
|
|
|
| 172 |
influence = OLSInfluence(modelos_sm)
|
| 173 |
cooks_d = influence.cooks_distance[0]
|
| 174 |
n = len(cooks_d)
|
| 175 |
+
indices_reais = np.array(indices) if indices is not None and len(indices) == n else np.arange(1, n + 1)
|
| 176 |
+
posicoes = np.arange(1, n + 1)
|
| 177 |
limite = 4 / n
|
| 178 |
|
| 179 |
fig = go.Figure()
|
| 180 |
+
for posicao, valor in zip(posicoes, cooks_d):
|
| 181 |
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 182 |
fig.add_trace(
|
| 183 |
go.Scatter(
|
| 184 |
+
x=[posicao, posicao],
|
| 185 |
y=[0, valor],
|
| 186 |
mode="lines",
|
| 187 |
line=dict(color=cor, width=1.5),
|
|
|
|
| 193 |
cores_pontos = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d]
|
| 194 |
fig.add_trace(
|
| 195 |
go.Scatter(
|
| 196 |
+
x=posicoes,
|
| 197 |
y=cooks_d,
|
| 198 |
mode="markers",
|
| 199 |
marker=dict(color=cores_pontos, size=8, line=dict(color="black", width=1)),
|
| 200 |
name="Distância de Cook",
|
| 201 |
+
customdata=indices_reais,
|
| 202 |
+
hovertemplate="<b>Índice:</b> %{customdata}<br><b>Cook:</b> %{y:.4f}<extra></extra>",
|
| 203 |
)
|
| 204 |
)
|
| 205 |
fig.add_hline(
|
|
|
|
| 211 |
)
|
| 212 |
fig.update_layout(
|
| 213 |
title=dict(text="Distância de Cook", x=0.5),
|
| 214 |
+
xaxis_title="Observação (ver índice no hover)",
|
| 215 |
yaxis_title="Distância de Cook",
|
| 216 |
plot_bgcolor="white",
|
| 217 |
margin=dict(l=60, r=40, t=60, b=60),
|
| 218 |
)
|
| 219 |
+
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black", showticklabels=False)
|
| 220 |
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 221 |
return fig
|
| 222 |
except Exception as exc:
|
|
|
|
| 374 |
if residuos is not None:
|
| 375 |
graficos["hist"] = _criar_histograma_residuos(residuos)
|
| 376 |
if modelos_sm is not None:
|
| 377 |
+
graficos["cook"] = _criar_grafico_cook(modelos_sm, indices)
|
| 378 |
graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y)
|
| 379 |
|
| 380 |
return graficos
|
|
|
|
| 527 |
)
|
| 528 |
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
| 531 |
def _limitar_texto(valor: str, limite: int) -> str:
|
| 532 |
txt = str(valor)
|
|
|
|
| 737 |
prefer_canvas=True,
|
| 738 |
control_scale=True,
|
| 739 |
)
|
| 740 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(mapa)
|
| 741 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(mapa)
|
| 742 |
add_bairros_layer(mapa, show=True)
|
| 743 |
|
| 744 |
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
|
backend/app/core/visualizacao/map_payload.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from typing import Any
|
| 4 |
|
| 5 |
import branca.colormap as cm
|
|
@@ -10,18 +11,27 @@ from scipy.interpolate import griddata
|
|
| 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,
|
| 18 |
|
| 19 |
|
| 20 |
_LAT_ALIASES = {"lat", "latitude", "siat_latitude"}
|
| 21 |
_LON_ALIASES = {"lon", "longitude", "long", "siat_longitude"}
|
| 22 |
_TILE_LAYERS = [
|
| 23 |
-
{
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
]
|
| 26 |
|
| 27 |
|
|
@@ -59,6 +69,42 @@ def _formatar_tooltip_valor(coluna: str | None, valor: Any) -> str:
|
|
| 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:
|
|
@@ -217,6 +263,7 @@ def build_elaboracao_map_payload(
|
|
| 217 |
cor_tick_values: list[float] | None = None,
|
| 218 |
cor_tick_labels: list[str] | None = None,
|
| 219 |
bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
|
|
|
|
| 220 |
) -> dict[str, Any] | None:
|
| 221 |
modo_normalizado = str(modo or "pontos").strip().lower()
|
| 222 |
|
|
@@ -237,7 +284,10 @@ def build_elaboracao_map_payload(
|
|
| 237 |
if lat_real is None or lon_real is None:
|
| 238 |
return None
|
| 239 |
|
|
|
|
| 240 |
df_mapa = df.copy()
|
|
|
|
|
|
|
| 241 |
df_mapa[lat_real] = pd.to_numeric(df_mapa[lat_real], errors="coerce")
|
| 242 |
df_mapa[lon_real] = pd.to_numeric(df_mapa[lon_real], errors="coerce")
|
| 243 |
df_mapa = df_mapa.dropna(subset=[lat_real, lon_real])
|
|
@@ -327,13 +377,9 @@ def build_elaboracao_map_payload(
|
|
| 327 |
lat_plot_col = "__mesa_lat_plot__"
|
| 328 |
lon_plot_col = "__mesa_lon_plot__"
|
| 329 |
if not modo_calor and not modo_superficie:
|
| 330 |
-
df_plot_pontos =
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
lon_col=str(lon_real),
|
| 334 |
-
lat_plot_col=lat_plot_col,
|
| 335 |
-
lon_plot_col=lon_plot_col,
|
| 336 |
-
)
|
| 337 |
else:
|
| 338 |
df_plot_pontos = df_mapa.copy()
|
| 339 |
df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
|
|
@@ -538,72 +584,112 @@ def build_elaboracao_map_payload(
|
|
| 538 |
}
|
| 539 |
)
|
| 540 |
else:
|
| 541 |
-
popup_cols: list[str]
|
| 542 |
-
if len(df_plot_pontos) <= 1200:
|
| 543 |
-
popup_cols = [str(c) for c in df_plot_pontos.columns]
|
| 544 |
-
elif tamanho_col and tamanho_col in df_plot_pontos.columns:
|
| 545 |
-
popup_cols = [str(tamanho_col)]
|
| 546 |
-
else:
|
| 547 |
-
popup_cols = []
|
| 548 |
-
|
| 549 |
market_points: list[dict[str, Any]] = []
|
| 550 |
indices_markers: list[dict[str, Any]] = []
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>"
|
| 562 |
f"<b>Índice {idx_display}</b>"
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
f"{float(valor_tooltip):.2f}"
|
| 568 |
-
if isinstance(valor_tooltip, (int, float, np.integer, np.floating)) and np.isfinite(float(valor_tooltip))
|
| 569 |
-
else str(valor_tooltip)
|
| 570 |
)
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
)
|
| 575 |
-
tooltip_html += "</div>"
|
| 576 |
-
|
| 577 |
-
cor = COR_PRINCIPAL
|
| 578 |
-
if colormap and cor_col_resolvida and cor_col_resolvida in df_mapa.columns:
|
| 579 |
-
valor_cor = pd.to_numeric(pd.Series([row.get(cor_col_resolvida)]), errors="coerce").iloc[0]
|
| 580 |
-
if pd.notna(valor_cor):
|
| 581 |
-
cor = str(colormap(float(valor_cor)))
|
| 582 |
-
|
| 583 |
-
if idx == indice_destacado:
|
| 584 |
-
raio = raio_max + 4.0
|
| 585 |
-
elif tamanho_func and tamanho_col and tamanho_col in row.index and pd.notna(row[tamanho_col]):
|
| 586 |
-
raio = float(tamanho_func(float(row[tamanho_col])))
|
| 587 |
else:
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
fill_opacity = 0.8 if idx == indice_destacado else 0.6
|
| 591 |
-
|
| 592 |
-
market_points.append(
|
| 593 |
-
{
|
| 594 |
-
"lat": float(row[lat_plot_col]),
|
| 595 |
-
"lon": float(row[lon_plot_col]),
|
| 596 |
-
"indice": idx_display,
|
| 597 |
-
"color": cor,
|
| 598 |
-
"base_radius": float(max(1.0, raio)),
|
| 599 |
-
"stroke_color": "#000000",
|
| 600 |
-
"stroke_weight": float(stroke_weight),
|
| 601 |
-
"fill_opacity": float(fill_opacity),
|
| 602 |
-
"tooltip_html": tooltip_html,
|
| 603 |
-
"popup_html": popup_html,
|
| 604 |
-
"popup_max_width": int(popup_width),
|
| 605 |
-
}
|
| 606 |
-
)
|
| 607 |
|
| 608 |
if mostrar_indices:
|
| 609 |
indices_markers.append(
|
|
@@ -615,10 +701,10 @@ def build_elaboracao_map_payload(
|
|
| 615 |
+ 'border: 1px solid rgba(28, 45, 66, 0.45);border-radius: 10px;padding: 1px 6px;font-size: 11px;'
|
| 616 |
+ 'font-weight: 700;line-height: 1.2;color: #1f2f44;white-space: nowrap;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);'
|
| 617 |
+ 'pointer-events: none;">'
|
| 618 |
-
+
|
| 619 |
+ "</div>"
|
| 620 |
),
|
| 621 |
-
"icon_size": [72, 24],
|
| 622 |
"icon_anchor": [0, 0],
|
| 623 |
"class_name": "mesa-indice-label",
|
| 624 |
"interactive": False,
|
|
@@ -749,13 +835,9 @@ def build_visualizacao_map_payload(
|
|
| 749 |
show_indices = False
|
| 750 |
lat_plot_key = "__mesa_lat_plot__"
|
| 751 |
lon_plot_key = "__mesa_lon_plot__"
|
| 752 |
-
df_plot_pontos =
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
lon_col=lon_key,
|
| 756 |
-
lat_plot_col=lat_plot_key,
|
| 757 |
-
lon_plot_col=lon_plot_key,
|
| 758 |
-
)
|
| 759 |
|
| 760 |
tooltip_col = None
|
| 761 |
tooltip_key = None
|
|
@@ -774,35 +856,96 @@ def build_visualizacao_map_payload(
|
|
| 774 |
raio_padrao = 4.0 if total_pontos_plot <= 2500 else 3.0
|
| 775 |
|
| 776 |
market_points: list[dict[str, Any]] = []
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
|
| 784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 785 |
tooltip_payload = {
|
| 786 |
"title": f"Índice {idx_display}",
|
| 787 |
"label": str(tooltip_col or ""),
|
| 788 |
-
"value":
|
| 789 |
-
_formatar_tooltip_valor(str(tooltip_col or ""), row[tooltip_key])
|
| 790 |
-
if tooltip_col and tooltip_key and tooltip_key in row.index
|
| 791 |
-
else ""
|
| 792 |
-
),
|
| 793 |
}
|
| 794 |
row_id_raw = row["__mesa_row_id__"] if "__mesa_row_id__" in row.index else None
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
|
| 807 |
return {
|
| 808 |
"type": "mesa_leaflet_payload",
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from html import escape
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
import branca.colormap as cm
|
|
|
|
| 11 |
from app.core.elaboracao.charts import (
|
| 12 |
_contorno_convexo_lng_lat,
|
| 13 |
_mascara_dentro_poligono,
|
|
|
|
| 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, formatar_monetario
|
| 18 |
|
| 19 |
|
| 20 |
_LAT_ALIASES = {"lat", "latitude", "siat_latitude"}
|
| 21 |
_LON_ALIASES = {"lon", "longitude", "long", "siat_longitude"}
|
| 22 |
_TILE_LAYERS = [
|
| 23 |
+
{
|
| 24 |
+
"id": "positron",
|
| 25 |
+
"label": "Positron",
|
| 26 |
+
"url": "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
| 27 |
+
"attribution": "© OpenStreetMap contributors © CARTO",
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "osm",
|
| 31 |
+
"label": "OpenStreetMap",
|
| 32 |
+
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
| 33 |
+
"attribution": "© OpenStreetMap contributors",
|
| 34 |
+
},
|
| 35 |
]
|
| 36 |
|
| 37 |
|
|
|
|
| 69 |
return str(valor)
|
| 70 |
|
| 71 |
|
| 72 |
+
def _formatar_valor_resumo_mapa(coluna: str | None, valor: Any) -> str:
|
| 73 |
+
return _formatar_tooltip_valor(coluna, valor)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _formatar_indices_tooltip(indices: list[Any]) -> str:
|
| 77 |
+
return ", ".join(str(item) for item in indices)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _formatar_indices_badge(indices: list[Any], limite: int = 28) -> str:
|
| 81 |
+
texto = ", ".join(str(item) for item in indices)
|
| 82 |
+
if len(texto) <= limite:
|
| 83 |
+
return texto
|
| 84 |
+
return texto[: max(0, limite - 1)].rstrip(" ,") + "…"
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _tooltip_html_grupo_mercado(indices: list[Any], label: str | None = None, valores: list[str] | None = None) -> str:
|
| 88 |
+
total = len(indices)
|
| 89 |
+
indices_txt = escape(_formatar_indices_tooltip(indices))
|
| 90 |
+
titulo = f"{total} dados neste local" if total != 1 else "1 dado neste local"
|
| 91 |
+
html = (
|
| 92 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>"
|
| 93 |
+
f"<b>{escape(titulo)}</b>"
|
| 94 |
+
f"<br><span style='color:#555;'>Índices:</span> <b>{indices_txt}</b>"
|
| 95 |
+
)
|
| 96 |
+
valores_limpos = [str(item) for item in valores or [] if str(item).strip()]
|
| 97 |
+
if label and valores_limpos:
|
| 98 |
+
unicos = list(dict.fromkeys(valores_limpos))
|
| 99 |
+
if len(unicos) == 1:
|
| 100 |
+
resumo = unicos[0]
|
| 101 |
+
else:
|
| 102 |
+
resumo = "valores diferentes"
|
| 103 |
+
html += f"<br><span style='color:#555;'>{escape(str(label))}:</span> <b>{escape(str(resumo))}</b>"
|
| 104 |
+
html += "</div>"
|
| 105 |
+
return html
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def _resolver_bounds(df_mapa: pd.DataFrame, lat_key: str, lon_key: str) -> list[list[float]]:
|
| 109 |
df_bounds = df_mapa
|
| 110 |
if len(df_mapa) >= 8:
|
|
|
|
| 263 |
cor_tick_values: list[float] | None = None,
|
| 264 |
cor_tick_labels: list[str] | None = None,
|
| 265 |
bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
|
| 266 |
+
popup_source: str | None = "mercado",
|
| 267 |
) -> dict[str, Any] | None:
|
| 268 |
modo_normalizado = str(modo or "pontos").strip().lower()
|
| 269 |
|
|
|
|
| 284 |
if lat_real is None or lon_real is None:
|
| 285 |
return None
|
| 286 |
|
| 287 |
+
row_id_col = "__mesa_row_id__"
|
| 288 |
df_mapa = df.copy()
|
| 289 |
+
if popup_source and row_id_col not in df_mapa.columns:
|
| 290 |
+
df_mapa[row_id_col] = np.arange(len(df_mapa), dtype=int)
|
| 291 |
df_mapa[lat_real] = pd.to_numeric(df_mapa[lat_real], errors="coerce")
|
| 292 |
df_mapa[lon_real] = pd.to_numeric(df_mapa[lon_real], errors="coerce")
|
| 293 |
df_mapa = df_mapa.dropna(subset=[lat_real, lon_real])
|
|
|
|
| 377 |
lat_plot_col = "__mesa_lat_plot__"
|
| 378 |
lon_plot_col = "__mesa_lon_plot__"
|
| 379 |
if not modo_calor and not modo_superficie:
|
| 380 |
+
df_plot_pontos = df_mapa.copy()
|
| 381 |
+
df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
|
| 382 |
+
df_plot_pontos[lon_plot_col] = df_plot_pontos[lon_real]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
else:
|
| 384 |
df_plot_pontos = df_mapa.copy()
|
| 385 |
df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
|
|
|
|
| 584 |
}
|
| 585 |
)
|
| 586 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
market_points: list[dict[str, Any]] = []
|
| 588 |
indices_markers: list[dict[str, Any]] = []
|
| 589 |
+
grupos_coord = df_plot_pontos.groupby(
|
| 590 |
+
[df_plot_pontos[lat_real].round(7), df_plot_pontos[lon_real].round(7)],
|
| 591 |
+
sort=False,
|
| 592 |
+
).indices
|
| 593 |
+
for marker_ordem, posicoes_raw in enumerate(grupos_coord.values()):
|
| 594 |
+
posicoes = list(posicoes_raw)
|
| 595 |
+
rows_grupo = [df_plot_pontos.iloc[int(pos)] for pos in posicoes]
|
| 596 |
+
row = rows_grupo[0]
|
| 597 |
+
idx = row.name
|
| 598 |
+
registros_grupo: list[dict[str, Any]] = []
|
| 599 |
+
indices_display: list[Any] = []
|
| 600 |
+
valores_tooltip: list[str] = []
|
| 601 |
+
cores_grupo: list[str] = []
|
| 602 |
+
raios_grupo: list[float] = []
|
| 603 |
+
destaque_grupo = False
|
| 604 |
+
|
| 605 |
+
for pos in posicoes:
|
| 606 |
+
row_item = df_plot_pontos.iloc[int(pos)]
|
| 607 |
+
idx_item = row_item.name
|
| 608 |
+
idx_display_item = int(idx_item) if isinstance(idx_item, (int, np.integer)) else int(pos) + 1
|
| 609 |
+
indices_display.append(idx_display_item)
|
| 610 |
+
valor_texto = ""
|
| 611 |
+
if tamanho_col and tamanho_col in df_plot_pontos.columns:
|
| 612 |
+
valor_texto = _formatar_valor_resumo_mapa(str(tamanho_col), row_item[tamanho_col])
|
| 613 |
+
valores_tooltip.append(valor_texto)
|
| 614 |
+
|
| 615 |
+
cor_item = COR_PRINCIPAL
|
| 616 |
+
if colormap and cor_col_resolvida and cor_col_resolvida in df_mapa.columns:
|
| 617 |
+
valor_cor = pd.to_numeric(pd.Series([row_item.get(cor_col_resolvida)]), errors="coerce").iloc[0]
|
| 618 |
+
if pd.notna(valor_cor):
|
| 619 |
+
cor_item = str(colormap(float(valor_cor)))
|
| 620 |
+
cores_grupo.append(cor_item)
|
| 621 |
+
|
| 622 |
+
if idx_item == indice_destacado:
|
| 623 |
+
raio_item = raio_max + 4.0
|
| 624 |
+
destaque_grupo = True
|
| 625 |
+
elif tamanho_func and tamanho_col and tamanho_col in row_item.index and pd.notna(row_item[tamanho_col]):
|
| 626 |
+
raio_item = float(tamanho_func(float(row_item[tamanho_col])))
|
| 627 |
+
else:
|
| 628 |
+
raio_item = 4.0
|
| 629 |
+
raios_grupo.append(float(max(1.0, raio_item)))
|
| 630 |
+
|
| 631 |
+
row_id_raw_item = row_item[row_id_col] if popup_source and row_id_col in row_item.index else None
|
| 632 |
+
popup_request_item = None
|
| 633 |
+
if row_id_raw_item is not None and pd.notna(row_id_raw_item):
|
| 634 |
+
popup_request_item = {
|
| 635 |
+
"kind": "elaboracao_row",
|
| 636 |
+
"row_id": int(row_id_raw_item),
|
| 637 |
+
"source": str(popup_source),
|
| 638 |
+
}
|
| 639 |
+
registros_grupo.append(
|
| 640 |
+
{
|
| 641 |
+
"indice": idx_display_item,
|
| 642 |
+
"label": f"Índice {idx_display_item}",
|
| 643 |
+
"value_label": str(tamanho_col or ""),
|
| 644 |
+
"value": valor_texto,
|
| 645 |
+
"popup_request": popup_request_item,
|
| 646 |
+
}
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
cor = cores_grupo[0] if cores_grupo else COR_PRINCIPAL
|
| 650 |
+
raio = max(raios_grupo) if raios_grupo else 4.0
|
| 651 |
+
stroke_weight = 3.0 if destaque_grupo else 1.0
|
| 652 |
+
fill_opacity = 0.8 if destaque_grupo else 0.6
|
| 653 |
+
is_grouped = len(rows_grupo) > 1
|
| 654 |
+
idx_display = indices_display[0] if indices_display else marker_ordem + 1
|
| 655 |
+
tooltip_html = _tooltip_html_grupo_mercado(
|
| 656 |
+
indices_display,
|
| 657 |
+
label=str(tamanho_col or "") if tamanho_col else None,
|
| 658 |
+
valores=valores_tooltip,
|
| 659 |
+
) if is_grouped else (
|
| 660 |
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>"
|
| 661 |
f"<b>Índice {idx_display}</b>"
|
| 662 |
+
+ (
|
| 663 |
+
f"<br><span style='color:#555;'>{escape(str(tamanho_col))}:</span> <b>{escape(valores_tooltip[0])}</b>"
|
| 664 |
+
if tamanho_col and valores_tooltip
|
| 665 |
+
else ""
|
|
|
|
|
|
|
|
|
|
| 666 |
)
|
| 667 |
+
+ "</div>"
|
| 668 |
+
)
|
| 669 |
+
|
| 670 |
+
point_payload = {
|
| 671 |
+
"lat": float(row[lat_plot_col]),
|
| 672 |
+
"lon": float(row[lon_plot_col]),
|
| 673 |
+
"indice": _formatar_indices_badge(indices_display) if is_grouped else idx_display,
|
| 674 |
+
"color": cor,
|
| 675 |
+
"base_radius": float(max(1.0, raio)),
|
| 676 |
+
"stroke_color": "#243746" if is_grouped else "#000000",
|
| 677 |
+
"stroke_weight": float(max(stroke_weight, 2.0) if is_grouped else stroke_weight),
|
| 678 |
+
"fill_opacity": float(0.74 if is_grouped else fill_opacity),
|
| 679 |
+
"tooltip_html": tooltip_html,
|
| 680 |
+
}
|
| 681 |
+
if is_grouped:
|
| 682 |
+
point_payload.update(
|
| 683 |
+
{
|
| 684 |
+
"grouped": True,
|
| 685 |
+
"count": len(rows_grupo),
|
| 686 |
+
"group_title": f"{len(rows_grupo)} dados neste local",
|
| 687 |
+
"group_items": registros_grupo,
|
| 688 |
+
}
|
| 689 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
else:
|
| 691 |
+
point_payload["popup_request"] = registros_grupo[0].get("popup_request") if registros_grupo else None
|
| 692 |
+
market_points.append(point_payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
if mostrar_indices:
|
| 695 |
indices_markers.append(
|
|
|
|
| 701 |
+ 'border: 1px solid rgba(28, 45, 66, 0.45);border-radius: 10px;padding: 1px 6px;font-size: 11px;'
|
| 702 |
+ 'font-weight: 700;line-height: 1.2;color: #1f2f44;white-space: nowrap;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);'
|
| 703 |
+ 'pointer-events: none;">'
|
| 704 |
+
+ escape(_formatar_indices_badge(indices_display))
|
| 705 |
+ "</div>"
|
| 706 |
),
|
| 707 |
+
"icon_size": [96 if is_grouped else 72, 24],
|
| 708 |
"icon_anchor": [0, 0],
|
| 709 |
"class_name": "mesa-indice-label",
|
| 710 |
"interactive": False,
|
|
|
|
| 835 |
show_indices = False
|
| 836 |
lat_plot_key = "__mesa_lat_plot__"
|
| 837 |
lon_plot_key = "__mesa_lon_plot__"
|
| 838 |
+
df_plot_pontos = df_mapa.copy()
|
| 839 |
+
df_plot_pontos[lat_plot_key] = df_plot_pontos[lat_key]
|
| 840 |
+
df_plot_pontos[lon_plot_key] = df_plot_pontos[lon_key]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 841 |
|
| 842 |
tooltip_col = None
|
| 843 |
tooltip_key = None
|
|
|
|
| 856 |
raio_padrao = 4.0 if total_pontos_plot <= 2500 else 3.0
|
| 857 |
|
| 858 |
market_points: list[dict[str, Any]] = []
|
| 859 |
+
grupos_coord = df_plot_pontos.groupby(
|
| 860 |
+
[df_plot_pontos[lat_key].round(7), df_plot_pontos[lon_key].round(7)],
|
| 861 |
+
sort=False,
|
| 862 |
+
).indices
|
| 863 |
+
for marker_ordem, posicoes_raw in enumerate(grupos_coord.values()):
|
| 864 |
+
posicoes = list(posicoes_raw)
|
| 865 |
+
rows_grupo = [df_plot_pontos.iloc[int(pos)] for pos in posicoes]
|
| 866 |
+
row = rows_grupo[0]
|
| 867 |
+
indices_display: list[Any] = []
|
| 868 |
+
registros_grupo: list[dict[str, Any]] = []
|
| 869 |
+
valores_tooltip: list[str] = []
|
| 870 |
+
cores_grupo: list[str] = []
|
| 871 |
+
raios_grupo: list[float] = []
|
| 872 |
+
|
| 873 |
+
for pos in posicoes:
|
| 874 |
+
row_item = df_plot_pontos.iloc[int(pos)]
|
| 875 |
+
idx_item = row_item.name
|
| 876 |
+
idx_display_item = int(row_item["index"]) if "index" in row_item.index else int(idx_item)
|
| 877 |
+
indices_display.append(idx_display_item)
|
| 878 |
+
|
| 879 |
+
valor_texto = (
|
| 880 |
+
_formatar_tooltip_valor(str(tooltip_col or ""), row_item[tooltip_key])
|
| 881 |
+
if tooltip_col and tooltip_key and tooltip_key in row_item.index
|
| 882 |
+
else ""
|
| 883 |
+
)
|
| 884 |
+
if valor_texto:
|
| 885 |
+
valores_tooltip.append(valor_texto)
|
| 886 |
|
| 887 |
+
cor_item = colormap(row_item[cor_key]) if colormap and cor_key and pd.notna(row_item[cor_key]) else COR_PRINCIPAL
|
| 888 |
+
cores_grupo.append(str(cor_item))
|
| 889 |
+
if tamanho_func and tamanho_key and pd.notna(row_item[tamanho_key]):
|
| 890 |
+
raio_item = float(tamanho_func(row_item[tamanho_key]))
|
| 891 |
+
else:
|
| 892 |
+
raio_item = raio_padrao
|
| 893 |
+
raios_grupo.append(float(max(1.0, raio_item)))
|
| 894 |
+
|
| 895 |
+
row_id_raw_item = row_item["__mesa_row_id__"] if "__mesa_row_id__" in row_item.index else None
|
| 896 |
+
row_id_item = int(row_id_raw_item) if row_id_raw_item is not None and pd.notna(row_id_raw_item) else None
|
| 897 |
+
registros_grupo.append(
|
| 898 |
+
{
|
| 899 |
+
"indice": idx_display_item,
|
| 900 |
+
"label": f"Índice {idx_display_item}",
|
| 901 |
+
"value_label": str(tooltip_col or ""),
|
| 902 |
+
"value": valor_texto,
|
| 903 |
+
"popup_request": (
|
| 904 |
+
{"kind": "visualizacao_row", "row_id": row_id_item}
|
| 905 |
+
if row_id_item is not None
|
| 906 |
+
else None
|
| 907 |
+
),
|
| 908 |
+
}
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
is_grouped = len(rows_grupo) > 1
|
| 912 |
+
idx_display = indices_display[0] if indices_display else marker_ordem
|
| 913 |
+
cor = cores_grupo[0] if cores_grupo else COR_PRINCIPAL
|
| 914 |
+
raio = max(raios_grupo) if raios_grupo else raio_padrao
|
| 915 |
tooltip_payload = {
|
| 916 |
"title": f"Índice {idx_display}",
|
| 917 |
"label": str(tooltip_col or ""),
|
| 918 |
+
"value": valores_tooltip[0] if valores_tooltip else "",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 919 |
}
|
| 920 |
row_id_raw = row["__mesa_row_id__"] if "__mesa_row_id__" in row.index else None
|
| 921 |
+
point_payload = {
|
| 922 |
+
"lat": float(row[lat_plot_key]),
|
| 923 |
+
"lon": float(row[lon_plot_key]),
|
| 924 |
+
"indice": _formatar_indices_badge(indices_display) if is_grouped else idx_display,
|
| 925 |
+
"row_id": int(row_id_raw) if (not is_grouped and row_id_raw is not None and pd.notna(row_id_raw)) else None,
|
| 926 |
+
"color": str(cor),
|
| 927 |
+
"base_radius": float(max(1.0, raio)),
|
| 928 |
+
}
|
| 929 |
+
if is_grouped:
|
| 930 |
+
point_payload.update(
|
| 931 |
+
{
|
| 932 |
+
"grouped": True,
|
| 933 |
+
"count": len(rows_grupo),
|
| 934 |
+
"stroke_color": "#243746",
|
| 935 |
+
"stroke_weight": 2.0,
|
| 936 |
+
"fill_opacity": 0.74,
|
| 937 |
+
"tooltip_html": _tooltip_html_grupo_mercado(
|
| 938 |
+
indices_display,
|
| 939 |
+
label=str(tooltip_col or "") if tooltip_col else None,
|
| 940 |
+
valores=valores_tooltip,
|
| 941 |
+
),
|
| 942 |
+
"group_title": f"{len(rows_grupo)} dados neste local",
|
| 943 |
+
"group_items": registros_grupo,
|
| 944 |
+
}
|
| 945 |
+
)
|
| 946 |
+
else:
|
| 947 |
+
point_payload["tooltip"] = tooltip_payload
|
| 948 |
+
market_points.append(point_payload)
|
| 949 |
|
| 950 |
return {
|
| 951 |
"type": "mesa_leaflet_payload",
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -54,6 +54,7 @@ from app.core.elaboracao.formatadores import (
|
|
| 54 |
formatar_micronumerosidade_html,
|
| 55 |
formatar_outliers_anteriores_html,
|
| 56 |
)
|
|
|
|
| 57 |
from app.core.visualizacao.map_payload import build_elaboracao_map_payload
|
| 58 |
from app.models.session import SessionState
|
| 59 |
from app.runtime_paths import resolve_core_path
|
|
@@ -2107,7 +2108,11 @@ def apply_outlier_filters(session: SessionState, filtros: list[dict[str, Any]])
|
|
| 2107 |
|
| 2108 |
indices = _coletar_indices_outliers(metricas, filtros)
|
| 2109 |
outliers_propostos = sorted(set(_clean_int_list(session.outliers_anteriores) + indices))
|
| 2110 |
-
_validar_outliers_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2111 |
texto = ", ".join(str(i) for i in indices)
|
| 2112 |
return {
|
| 2113 |
"indices": indices,
|
|
@@ -2213,7 +2218,10 @@ def _info_micronumerosidade_geral_outliers(session: SessionState, outliers_base:
|
|
| 2213 |
}
|
| 2214 |
|
| 2215 |
|
| 2216 |
-
def _formatar_erro_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
| 2217 |
if not limite_info:
|
| 2218 |
return "A exclusão ultrapassa a micronumerosidade mínima do modelo."
|
| 2219 |
n_final = int(limite_info.get("n_final_atual") or 0)
|
|
@@ -2221,15 +2229,20 @@ def _formatar_erro_micronumerosidade_geral(limite_info: dict[str, Any] | None) -
|
|
| 2221 |
k = int(limite_info.get("k") or 0)
|
| 2222 |
total = int(limite_info.get("n_total") or 0)
|
| 2223 |
excluidos = int(limite_info.get("excluidos_presentes") or 0)
|
|
|
|
| 2224 |
return (
|
| 2225 |
"A exclusão ultrapassa a micronumerosidade mínima: "
|
| 2226 |
f"restariam {n_final} observações de {total}, mas o modelo precisa manter pelo menos {minimo} "
|
| 2227 |
f"(n >= 3(k+1), k={k}). "
|
| 2228 |
-
f"A seleção atual excluiria {excluidos} observação(ões).
|
| 2229 |
)
|
| 2230 |
|
| 2231 |
|
| 2232 |
-
def _validar_outliers_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2233 |
info = _info_micronumerosidade_geral_outliers(session, outliers)
|
| 2234 |
n_final = int(info.get("n_final_atual") or 0)
|
| 2235 |
minimo = int(info.get("min_observacoes") or 0)
|
|
@@ -2241,7 +2254,7 @@ def _validar_outliers_micronumerosidade_geral(session: SessionState, outliers: l
|
|
| 2241 |
|
| 2242 |
raise HTTPException(
|
| 2243 |
status_code=400,
|
| 2244 |
-
detail=_formatar_erro_micronumerosidade_geral(info),
|
| 2245 |
)
|
| 2246 |
|
| 2247 |
|
|
@@ -2358,7 +2371,11 @@ def apply_outlier_filters_recursive(
|
|
| 2358 |
novos_iteracao = [idx for idx in indices_iteracao if idx not in outliers_atuais and idx not in novos_set]
|
| 2359 |
if novos_iteracao:
|
| 2360 |
outliers_propostos = sorted(set(outliers_atuais + novos_iteracao))
|
| 2361 |
-
_validar_outliers_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2362 |
iteracoes += 1
|
| 2363 |
novos_acumulados.extend(novos_iteracao)
|
| 2364 |
novos_set.update(novos_iteracao)
|
|
@@ -2392,7 +2409,11 @@ def apply_outlier_filters_recursive(
|
|
| 2392 |
break
|
| 2393 |
|
| 2394 |
outliers_propostos = sorted(set(outliers_atuais + novos_iteracao))
|
| 2395 |
-
_validar_outliers_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2396 |
iteracoes += 1
|
| 2397 |
novos_acumulados.extend(novos_iteracao)
|
| 2398 |
novos_set.update(novos_iteracao)
|
|
@@ -2453,7 +2474,11 @@ def reiniciar_iteracao(
|
|
| 2453 |
|
| 2454 |
anteriores_atualizados = [i for i in session.outliers_anteriores if i not in reincluir]
|
| 2455 |
outliers_combinados = sorted(set(anteriores_atualizados + novos))
|
| 2456 |
-
_validar_outliers_micronumerosidade_geral(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2457 |
|
| 2458 |
transformacao_y_atual = str(session.transformacao_y or "(x)")
|
| 2459 |
transformacoes_x_atuais = {
|
|
@@ -3124,6 +3149,40 @@ def atualizar_mapa(session: SessionState, var_mapa: str | None, modo_mapa: str |
|
|
| 3124 |
return {"mapa_html": mapa_html, "mapa_payload": mapa_payload}
|
| 3125 |
|
| 3126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3127 |
def _normalizar_extremo_abs_residuos(valor: float | None) -> float | None:
|
| 3128 |
if valor is None:
|
| 3129 |
return None
|
|
@@ -3209,6 +3268,7 @@ def atualizar_mapa_residuos(
|
|
| 3209 |
cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
|
| 3210 |
cor_tick_values=ticks_valores,
|
| 3211 |
cor_tick_labels=ticks_labels,
|
|
|
|
| 3212 |
)
|
| 3213 |
return {
|
| 3214 |
"mapa_html": mapa_html,
|
|
|
|
| 54 |
formatar_micronumerosidade_html,
|
| 55 |
formatar_outliers_anteriores_html,
|
| 56 |
)
|
| 57 |
+
from app.core.visualizacao import app as visualizacao_app
|
| 58 |
from app.core.visualizacao.map_payload import build_elaboracao_map_payload
|
| 59 |
from app.models.session import SessionState
|
| 60 |
from app.runtime_paths import resolve_core_path
|
|
|
|
| 2108 |
|
| 2109 |
indices = _coletar_indices_outliers(metricas, filtros)
|
| 2110 |
outliers_propostos = sorted(set(_clean_int_list(session.outliers_anteriores) + indices))
|
| 2111 |
+
_validar_outliers_micronumerosidade_geral(
|
| 2112 |
+
session,
|
| 2113 |
+
outliers_propostos,
|
| 2114 |
+
orientacao="Ajuste os filtros de outliers.",
|
| 2115 |
+
)
|
| 2116 |
texto = ", ".join(str(i) for i in indices)
|
| 2117 |
return {
|
| 2118 |
"indices": indices,
|
|
|
|
| 2218 |
}
|
| 2219 |
|
| 2220 |
|
| 2221 |
+
def _formatar_erro_micronumerosidade_geral(
|
| 2222 |
+
limite_info: dict[str, Any] | None,
|
| 2223 |
+
orientacao: str | None = None,
|
| 2224 |
+
) -> str:
|
| 2225 |
if not limite_info:
|
| 2226 |
return "A exclusão ultrapassa a micronumerosidade mínima do modelo."
|
| 2227 |
n_final = int(limite_info.get("n_final_atual") or 0)
|
|
|
|
| 2229 |
k = int(limite_info.get("k") or 0)
|
| 2230 |
total = int(limite_info.get("n_total") or 0)
|
| 2231 |
excluidos = int(limite_info.get("excluidos_presentes") or 0)
|
| 2232 |
+
orientacao_final = str(orientacao or "Ajuste os filtros de outliers.").strip()
|
| 2233 |
return (
|
| 2234 |
"A exclusão ultrapassa a micronumerosidade mínima: "
|
| 2235 |
f"restariam {n_final} observações de {total}, mas o modelo precisa manter pelo menos {minimo} "
|
| 2236 |
f"(n >= 3(k+1), k={k}). "
|
| 2237 |
+
f"A seleção atual excluiria {excluidos} observação(ões). {orientacao_final}"
|
| 2238 |
)
|
| 2239 |
|
| 2240 |
|
| 2241 |
+
def _validar_outliers_micronumerosidade_geral(
|
| 2242 |
+
session: SessionState,
|
| 2243 |
+
outliers: list[int],
|
| 2244 |
+
orientacao: str | None = None,
|
| 2245 |
+
) -> None:
|
| 2246 |
info = _info_micronumerosidade_geral_outliers(session, outliers)
|
| 2247 |
n_final = int(info.get("n_final_atual") or 0)
|
| 2248 |
minimo = int(info.get("min_observacoes") or 0)
|
|
|
|
| 2254 |
|
| 2255 |
raise HTTPException(
|
| 2256 |
status_code=400,
|
| 2257 |
+
detail=_formatar_erro_micronumerosidade_geral(info, orientacao=orientacao),
|
| 2258 |
)
|
| 2259 |
|
| 2260 |
|
|
|
|
| 2371 |
novos_iteracao = [idx for idx in indices_iteracao if idx not in outliers_atuais and idx not in novos_set]
|
| 2372 |
if novos_iteracao:
|
| 2373 |
outliers_propostos = sorted(set(outliers_atuais + novos_iteracao))
|
| 2374 |
+
_validar_outliers_micronumerosidade_geral(
|
| 2375 |
+
session,
|
| 2376 |
+
outliers_propostos,
|
| 2377 |
+
orientacao="Ajuste os filtros de outliers antes de aplicar com recursividade.",
|
| 2378 |
+
)
|
| 2379 |
iteracoes += 1
|
| 2380 |
novos_acumulados.extend(novos_iteracao)
|
| 2381 |
novos_set.update(novos_iteracao)
|
|
|
|
| 2409 |
break
|
| 2410 |
|
| 2411 |
outliers_propostos = sorted(set(outliers_atuais + novos_iteracao))
|
| 2412 |
+
_validar_outliers_micronumerosidade_geral(
|
| 2413 |
+
session,
|
| 2414 |
+
outliers_propostos,
|
| 2415 |
+
orientacao="Ajuste os filtros de outliers antes de aplicar com recursividade.",
|
| 2416 |
+
)
|
| 2417 |
iteracoes += 1
|
| 2418 |
novos_acumulados.extend(novos_iteracao)
|
| 2419 |
novos_set.update(novos_iteracao)
|
|
|
|
| 2474 |
|
| 2475 |
anteriores_atualizados = [i for i in session.outliers_anteriores if i not in reincluir]
|
| 2476 |
outliers_combinados = sorted(set(anteriores_atualizados + novos))
|
| 2477 |
+
_validar_outliers_micronumerosidade_geral(
|
| 2478 |
+
session,
|
| 2479 |
+
outliers_combinados,
|
| 2480 |
+
orientacao="Modifique a lista de exclusões ou ajuste os filtros de outliers.",
|
| 2481 |
+
)
|
| 2482 |
|
| 2483 |
transformacao_y_atual = str(session.transformacao_y or "(x)")
|
| 2484 |
transformacoes_x_atuais = {
|
|
|
|
| 3149 |
return {"mapa_html": mapa_html, "mapa_payload": mapa_payload}
|
| 3150 |
|
| 3151 |
|
| 3152 |
+
def carregar_popup_ponto_mapa(session: SessionState, row_id: int, source: str | None = None) -> dict[str, Any]:
|
| 3153 |
+
source_norm = str(source or "mercado").strip().lower()
|
| 3154 |
+
if source_norm in {"residuo", "residuos", "resíduos"}:
|
| 3155 |
+
df = session.tabela_metricas_estado
|
| 3156 |
+
mensagem_vazia = "Ajuste o modelo antes de abrir os detalhes do ponto"
|
| 3157 |
+
popup_source = "residuos"
|
| 3158 |
+
else:
|
| 3159 |
+
df = session.df_filtrado if session.df_filtrado is not None else session.df_original
|
| 3160 |
+
mensagem_vazia = "Carregue dados antes de abrir os detalhes do ponto"
|
| 3161 |
+
popup_source = "mercado"
|
| 3162 |
+
|
| 3163 |
+
if df is None or df.empty:
|
| 3164 |
+
raise HTTPException(status_code=400, detail=mensagem_vazia)
|
| 3165 |
+
|
| 3166 |
+
try:
|
| 3167 |
+
row_id_int = int(row_id)
|
| 3168 |
+
except (TypeError, ValueError) as exc:
|
| 3169 |
+
raise HTTPException(status_code=400, detail="Identificador de ponto invalido") from exc
|
| 3170 |
+
|
| 3171 |
+
if row_id_int < 0 or row_id_int >= len(df.index):
|
| 3172 |
+
raise HTTPException(status_code=404, detail="Ponto nao encontrado para esta sessao")
|
| 3173 |
+
|
| 3174 |
+
row = df.iloc[row_id_int]
|
| 3175 |
+
popup_html, popup_width = visualizacao_app.montar_popup_registro_html(
|
| 3176 |
+
row,
|
| 3177 |
+
popup_uid=f"mesa-popup-elab-{popup_source}-{row_id_int}",
|
| 3178 |
+
max_itens_pagina=8,
|
| 3179 |
+
)
|
| 3180 |
+
return {
|
| 3181 |
+
"popup_html": popup_html,
|
| 3182 |
+
"popup_width": int(popup_width),
|
| 3183 |
+
}
|
| 3184 |
+
|
| 3185 |
+
|
| 3186 |
def _normalizar_extremo_abs_residuos(valor: float | None) -> float | None:
|
| 3187 |
if valor is None:
|
| 3188 |
return None
|
|
|
|
| 3268 |
cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
|
| 3269 |
cor_tick_values=ticks_valores,
|
| 3270 |
cor_tick_labels=ticks_labels,
|
| 3271 |
+
popup_source="residuos",
|
| 3272 |
)
|
| 3273 |
return {
|
| 3274 |
"mapa_html": mapa_html,
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -25,7 +25,6 @@ from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, normalizar_obser
|
|
| 25 |
from app.core.map_layers import (
|
| 26 |
add_bairros_layer,
|
| 27 |
add_marker_payloads,
|
| 28 |
-
apply_marker_payload_jitter,
|
| 29 |
build_trabalhos_tecnicos_marker_payloads,
|
| 30 |
add_zoom_responsive_circle_markers,
|
| 31 |
)
|
|
@@ -922,6 +921,7 @@ def gerar_mapa_modelos(
|
|
| 922 |
avaliando_lon: float | None = None,
|
| 923 |
avaliandos: list[dict[str, Any]] | None = None,
|
| 924 |
modo_exibicao: str | None = "pontos",
|
|
|
|
| 925 |
criterio_espacial: str | None = CRITERIO_ESPACIAL_PADRAO,
|
| 926 |
trabalhos_tecnicos_modelos_modo: str | None = TRABALHOS_TECNICOS_MODELOS_SELECIONADOS,
|
| 927 |
trabalhos_tecnicos_proximidade_modo: str | None = TRABALHOS_TECNICOS_PROXIMIDADE_DESATIVADA,
|
|
@@ -949,6 +949,7 @@ def gerar_mapa_modelos(
|
|
| 949 |
raise HTTPException(status_code=404, detail="Nenhum modelo selecionado foi encontrado na pasta de pesquisa")
|
| 950 |
|
| 951 |
modo_exibicao_norm = _normalizar_modo_exibicao_mapa(modo_exibicao)
|
|
|
|
| 952 |
trabalhos_tecnicos_modelos_modo_norm = _normalizar_modo_trabalhos_tecnicos_modelos_mapa(trabalhos_tecnicos_modelos_modo)
|
| 953 |
trabalhos_tecnicos_proximidade_modo_norm = _normalizar_modo_trabalhos_tecnicos_proximidade_mapa(
|
| 954 |
trabalhos_tecnicos_proximidade_modo
|
|
@@ -1069,6 +1070,7 @@ def gerar_mapa_modelos(
|
|
| 1069 |
bounds,
|
| 1070 |
avaliandos_geo,
|
| 1071 |
modo_exibicao_norm,
|
|
|
|
| 1072 |
avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
|
| 1073 |
trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
|
| 1074 |
)
|
|
@@ -1135,6 +1137,7 @@ def gerar_mapa_modelos(
|
|
| 1135 |
"avaliandos": avaliandos_geo,
|
| 1136 |
"criterio_espacial": criterio_espacial_norm,
|
| 1137 |
"modo_exibicao": modo_exibicao_norm,
|
|
|
|
| 1138 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 1139 |
"trabalhos_tecnicos_proximidade_modo": trabalhos_tecnicos_proximidade_modo_norm,
|
| 1140 |
"trabalhos_tecnicos_raio_m": trabalhos_tecnicos_raio_m_norm,
|
|
@@ -1160,6 +1163,13 @@ def _normalizar_modo_exibicao_mapa(value: Any) -> str:
|
|
| 1160 |
return "pontos"
|
| 1161 |
|
| 1162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
def _normalizar_modo_trabalhos_tecnicos_modelos_mapa(value: Any) -> str:
|
| 1164 |
modo = str(value or "").strip().lower()
|
| 1165 |
if modo in {
|
|
@@ -1217,6 +1227,95 @@ def _tooltip_mapa_modelo_html(modelo: dict[str, Any]) -> str:
|
|
| 1217 |
)
|
| 1218 |
|
| 1219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1220 |
def _marker_payloads_avaliandos(avaliandos_geo: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 1221 |
payloads: list[dict[str, Any]] = []
|
| 1222 |
for idx, avaliando in enumerate(avaliandos_geo):
|
|
@@ -1335,6 +1434,7 @@ def _build_mapa_modelos_payload(
|
|
| 1335 |
bounds: list[list[float]],
|
| 1336 |
avaliandos_geo: list[dict[str, Any]],
|
| 1337 |
modo_exibicao: str,
|
|
|
|
| 1338 |
avaliandos_tecnicos_proximos: list[dict[str, Any]] | None = None,
|
| 1339 |
trabalhos_tecnicos_raio_m: int | None = None,
|
| 1340 |
) -> dict[str, Any] | None:
|
|
@@ -1362,8 +1462,11 @@ def _build_mapa_modelos_payload(
|
|
| 1362 |
if avaliandos_tecnicos_proximos is not None
|
| 1363 |
else []
|
| 1364 |
)
|
| 1365 |
-
|
| 1366 |
-
|
|
|
|
|
|
|
|
|
|
| 1367 |
|
| 1368 |
for modelo in modelos_plotados:
|
| 1369 |
layer: dict[str, Any] = {
|
|
@@ -1373,20 +1476,11 @@ def _build_mapa_modelos_payload(
|
|
| 1373 |
"hover_highlight_group": "pesquisa-modelos",
|
| 1374 |
}
|
| 1375 |
if modo_exibicao == "pontos":
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
"color": str(modelo.get("cor") or "#1f77b4"),
|
| 1382 |
-
"base_radius": 3.0,
|
| 1383 |
-
"stroke_color": str(modelo.get("cor") or "#1f77b4"),
|
| 1384 |
-
"stroke_weight": 1.0,
|
| 1385 |
-
"fill_opacity": 0.72,
|
| 1386 |
-
"tooltip_html": tooltip_html,
|
| 1387 |
-
}
|
| 1388 |
-
for ponto in (modelo.get("pontos") or [])
|
| 1389 |
-
]
|
| 1390 |
else:
|
| 1391 |
layer["shapes"] = _shapes_modelo_payload(modelo, aval_lat, aval_lon)
|
| 1392 |
overlay_layers.append(layer)
|
|
@@ -1478,8 +1572,8 @@ def _renderizar_mapa_modelos(
|
|
| 1478 |
control_scale=True,
|
| 1479 |
tiles=None,
|
| 1480 |
)
|
| 1481 |
-
folium.TileLayer(tiles="
|
| 1482 |
-
folium.TileLayer(tiles="
|
| 1483 |
add_bairros_layer(mapa, show=True)
|
| 1484 |
nome_camada_avaliando = "Avaliando" if len(avaliandos_geo) == 1 else "Avaliandos"
|
| 1485 |
camada_avaliando = folium.FeatureGroup(name=nome_camada_avaliando, show=True)
|
|
@@ -1503,8 +1597,6 @@ def _renderizar_mapa_modelos(
|
|
| 1503 |
if avaliandos_tecnicos_proximos is not None
|
| 1504 |
else []
|
| 1505 |
)
|
| 1506 |
-
if trabalhos_tecnicos_modelos_markers or trabalhos_tecnicos_proximos_markers:
|
| 1507 |
-
apply_marker_payload_jitter([*trabalhos_tecnicos_modelos_markers, *trabalhos_tecnicos_proximos_markers])
|
| 1508 |
camada_trabalhos_modelos = (
|
| 1509 |
folium.FeatureGroup(name="Trabalhos tecnicos dos modelos", show=True)
|
| 1510 |
if trabalhos_tecnicos_modelos
|
|
@@ -4415,13 +4507,15 @@ def _parse_datetime(texto: str) -> datetime | None:
|
|
| 4415 |
|
| 4416 |
|
| 4417 |
def _contains_any(candidatos: list[Any], consulta: str) -> bool:
|
| 4418 |
-
|
| 4419 |
-
|
|
|
|
| 4420 |
return True
|
| 4421 |
for item in candidatos:
|
| 4422 |
if item is None:
|
| 4423 |
continue
|
| 4424 |
-
|
|
|
|
| 4425 |
return True
|
| 4426 |
return False
|
| 4427 |
|
|
|
|
| 25 |
from app.core.map_layers import (
|
| 26 |
add_bairros_layer,
|
| 27 |
add_marker_payloads,
|
|
|
|
| 28 |
build_trabalhos_tecnicos_marker_payloads,
|
| 29 |
add_zoom_responsive_circle_markers,
|
| 30 |
)
|
|
|
|
| 921 |
avaliando_lon: float | None = None,
|
| 922 |
avaliandos: list[dict[str, Any]] | None = None,
|
| 923 |
modo_exibicao: str | None = "pontos",
|
| 924 |
+
agrupar_pontos_mercado: Any = False,
|
| 925 |
criterio_espacial: str | None = CRITERIO_ESPACIAL_PADRAO,
|
| 926 |
trabalhos_tecnicos_modelos_modo: str | None = TRABALHOS_TECNICOS_MODELOS_SELECIONADOS,
|
| 927 |
trabalhos_tecnicos_proximidade_modo: str | None = TRABALHOS_TECNICOS_PROXIMIDADE_DESATIVADA,
|
|
|
|
| 949 |
raise HTTPException(status_code=404, detail="Nenhum modelo selecionado foi encontrado na pasta de pesquisa")
|
| 950 |
|
| 951 |
modo_exibicao_norm = _normalizar_modo_exibicao_mapa(modo_exibicao)
|
| 952 |
+
agrupar_pontos_mercado_norm = _normalizar_agrupar_pontos_mercado_mapa(agrupar_pontos_mercado)
|
| 953 |
trabalhos_tecnicos_modelos_modo_norm = _normalizar_modo_trabalhos_tecnicos_modelos_mapa(trabalhos_tecnicos_modelos_modo)
|
| 954 |
trabalhos_tecnicos_proximidade_modo_norm = _normalizar_modo_trabalhos_tecnicos_proximidade_mapa(
|
| 955 |
trabalhos_tecnicos_proximidade_modo
|
|
|
|
| 1070 |
bounds,
|
| 1071 |
avaliandos_geo,
|
| 1072 |
modo_exibicao_norm,
|
| 1073 |
+
agrupar_pontos_mercado=agrupar_pontos_mercado_norm,
|
| 1074 |
avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
|
| 1075 |
trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
|
| 1076 |
)
|
|
|
|
| 1137 |
"avaliandos": avaliandos_geo,
|
| 1138 |
"criterio_espacial": criterio_espacial_norm,
|
| 1139 |
"modo_exibicao": modo_exibicao_norm,
|
| 1140 |
+
"agrupar_pontos_mercado": agrupar_pontos_mercado_norm,
|
| 1141 |
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 1142 |
"trabalhos_tecnicos_proximidade_modo": trabalhos_tecnicos_proximidade_modo_norm,
|
| 1143 |
"trabalhos_tecnicos_raio_m": trabalhos_tecnicos_raio_m_norm,
|
|
|
|
| 1163 |
return "pontos"
|
| 1164 |
|
| 1165 |
|
| 1166 |
+
def _normalizar_agrupar_pontos_mercado_mapa(value: Any) -> bool:
|
| 1167 |
+
if isinstance(value, bool):
|
| 1168 |
+
return value
|
| 1169 |
+
texto = _normalize(str(value or "")).strip()
|
| 1170 |
+
return texto in {"1", "true", "sim", "s", "yes", "y", "agrupar", "agrupado", "agrupados"}
|
| 1171 |
+
|
| 1172 |
+
|
| 1173 |
def _normalizar_modo_trabalhos_tecnicos_modelos_mapa(value: Any) -> str:
|
| 1174 |
modo = str(value or "").strip().lower()
|
| 1175 |
if modo in {
|
|
|
|
| 1227 |
)
|
| 1228 |
|
| 1229 |
|
| 1230 |
+
def _tooltip_mapa_modelo_ponto_html(modelo: dict[str, Any], total_no_local: int) -> str:
|
| 1231 |
+
tooltip = _tooltip_mapa_modelo_html(modelo)
|
| 1232 |
+
if total_no_local <= 1:
|
| 1233 |
+
return tooltip
|
| 1234 |
+
detalhe = f"<br><span style='color:#555;'>Dados neste local:</span> <b>{total_no_local}</b>"
|
| 1235 |
+
if tooltip.endswith("</div>"):
|
| 1236 |
+
return f"{tooltip[:-6]}{detalhe}</div>"
|
| 1237 |
+
return f"{tooltip}{detalhe}"
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
+
def _chave_coordenada_ponto(ponto: dict[str, Any], precisao: int = 7) -> tuple[float, float] | None:
|
| 1241 |
+
try:
|
| 1242 |
+
lat = float(ponto["lat"])
|
| 1243 |
+
lon = float(ponto["lon"])
|
| 1244 |
+
except (KeyError, TypeError, ValueError):
|
| 1245 |
+
return None
|
| 1246 |
+
if not math.isfinite(lat) or not math.isfinite(lon):
|
| 1247 |
+
return None
|
| 1248 |
+
return (round(lat, precisao), round(lon, precisao))
|
| 1249 |
+
|
| 1250 |
+
|
| 1251 |
+
def _contagens_coordenadas_modelos(modelos_plotados: list[dict[str, Any]]) -> dict[tuple[float, float], int]:
|
| 1252 |
+
contagens: dict[tuple[float, float], int] = {}
|
| 1253 |
+
for modelo in modelos_plotados:
|
| 1254 |
+
for ponto in modelo.get("pontos") or []:
|
| 1255 |
+
chave = _chave_coordenada_ponto(ponto)
|
| 1256 |
+
if chave is None:
|
| 1257 |
+
continue
|
| 1258 |
+
contagens[chave] = contagens.get(chave, 0) + 1
|
| 1259 |
+
return contagens
|
| 1260 |
+
|
| 1261 |
+
|
| 1262 |
+
def _ponto_mapa_modelo_payload_item(
|
| 1263 |
+
modelo: dict[str, Any],
|
| 1264 |
+
ponto: dict[str, Any],
|
| 1265 |
+
total_no_local: int = 1,
|
| 1266 |
+
) -> dict[str, Any]:
|
| 1267 |
+
cor = str(modelo.get("cor") or "#1f77b4")
|
| 1268 |
+
item: dict[str, Any] = {
|
| 1269 |
+
"lat": float(ponto["lat"]),
|
| 1270 |
+
"lon": float(ponto["lon"]),
|
| 1271 |
+
"color": cor,
|
| 1272 |
+
"base_radius": 3.0,
|
| 1273 |
+
"stroke_color": cor,
|
| 1274 |
+
"stroke_weight": 1.0,
|
| 1275 |
+
"fill_opacity": 0.72,
|
| 1276 |
+
"tooltip_html": _tooltip_mapa_modelo_ponto_html(modelo, total_no_local),
|
| 1277 |
+
}
|
| 1278 |
+
if total_no_local > 1:
|
| 1279 |
+
item.update(
|
| 1280 |
+
{
|
| 1281 |
+
"grouped": True,
|
| 1282 |
+
"count": total_no_local,
|
| 1283 |
+
"group_title": f"{total_no_local} dados neste local",
|
| 1284 |
+
"pane": "mesa-market-group-pane",
|
| 1285 |
+
"tooltip_sticky": False,
|
| 1286 |
+
}
|
| 1287 |
+
)
|
| 1288 |
+
return item
|
| 1289 |
+
|
| 1290 |
+
|
| 1291 |
+
def _pontos_mapa_modelo_payload(
|
| 1292 |
+
modelo: dict[str, Any],
|
| 1293 |
+
contagens_coordenadas: dict[tuple[float, float], int] | None = None,
|
| 1294 |
+
agrupar_pontos_mercado: bool = False,
|
| 1295 |
+
) -> list[dict[str, Any]]:
|
| 1296 |
+
if not agrupar_pontos_mercado:
|
| 1297 |
+
pontos_payload: list[dict[str, Any]] = []
|
| 1298 |
+
for ponto in modelo.get("pontos") or []:
|
| 1299 |
+
if _chave_coordenada_ponto(ponto) is None:
|
| 1300 |
+
continue
|
| 1301 |
+
pontos_payload.append(_ponto_mapa_modelo_payload_item(modelo, ponto))
|
| 1302 |
+
return pontos_payload
|
| 1303 |
+
|
| 1304 |
+
grupos: dict[tuple[float, float], list[dict[str, Any]]] = {}
|
| 1305 |
+
for ponto in modelo.get("pontos") or []:
|
| 1306 |
+
chave = _chave_coordenada_ponto(ponto)
|
| 1307 |
+
if chave is None:
|
| 1308 |
+
continue
|
| 1309 |
+
grupos.setdefault(chave, []).append(ponto)
|
| 1310 |
+
|
| 1311 |
+
pontos_payload: list[dict[str, Any]] = []
|
| 1312 |
+
for chave, pontos_no_local in grupos.items():
|
| 1313 |
+
ponto = pontos_no_local[0]
|
| 1314 |
+
total_no_local = max(len(pontos_no_local), int((contagens_coordenadas or {}).get(chave) or 0))
|
| 1315 |
+
pontos_payload.append(_ponto_mapa_modelo_payload_item(modelo, ponto, total_no_local))
|
| 1316 |
+
return pontos_payload
|
| 1317 |
+
|
| 1318 |
+
|
| 1319 |
def _marker_payloads_avaliandos(avaliandos_geo: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 1320 |
payloads: list[dict[str, Any]] = []
|
| 1321 |
for idx, avaliando in enumerate(avaliandos_geo):
|
|
|
|
| 1434 |
bounds: list[list[float]],
|
| 1435 |
avaliandos_geo: list[dict[str, Any]],
|
| 1436 |
modo_exibicao: str,
|
| 1437 |
+
agrupar_pontos_mercado: bool = False,
|
| 1438 |
avaliandos_tecnicos_proximos: list[dict[str, Any]] | None = None,
|
| 1439 |
trabalhos_tecnicos_raio_m: int | None = None,
|
| 1440 |
) -> dict[str, Any] | None:
|
|
|
|
| 1462 |
if avaliandos_tecnicos_proximos is not None
|
| 1463 |
else []
|
| 1464 |
)
|
| 1465 |
+
contagens_pontos = (
|
| 1466 |
+
_contagens_coordenadas_modelos(modelos_plotados)
|
| 1467 |
+
if modo_exibicao == "pontos" and agrupar_pontos_mercado
|
| 1468 |
+
else {}
|
| 1469 |
+
)
|
| 1470 |
|
| 1471 |
for modelo in modelos_plotados:
|
| 1472 |
layer: dict[str, Any] = {
|
|
|
|
| 1476 |
"hover_highlight_group": "pesquisa-modelos",
|
| 1477 |
}
|
| 1478 |
if modo_exibicao == "pontos":
|
| 1479 |
+
layer["points"] = _pontos_mapa_modelo_payload(
|
| 1480 |
+
modelo,
|
| 1481 |
+
contagens_pontos,
|
| 1482 |
+
agrupar_pontos_mercado=agrupar_pontos_mercado,
|
| 1483 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1484 |
else:
|
| 1485 |
layer["shapes"] = _shapes_modelo_payload(modelo, aval_lat, aval_lon)
|
| 1486 |
overlay_layers.append(layer)
|
|
|
|
| 1572 |
control_scale=True,
|
| 1573 |
tiles=None,
|
| 1574 |
)
|
| 1575 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(mapa)
|
| 1576 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(mapa)
|
| 1577 |
add_bairros_layer(mapa, show=True)
|
| 1578 |
nome_camada_avaliando = "Avaliando" if len(avaliandos_geo) == 1 else "Avaliandos"
|
| 1579 |
camada_avaliando = folium.FeatureGroup(name=nome_camada_avaliando, show=True)
|
|
|
|
| 1597 |
if avaliandos_tecnicos_proximos is not None
|
| 1598 |
else []
|
| 1599 |
)
|
|
|
|
|
|
|
| 1600 |
camada_trabalhos_modelos = (
|
| 1601 |
folium.FeatureGroup(name="Trabalhos tecnicos dos modelos", show=True)
|
| 1602 |
if trabalhos_tecnicos_modelos
|
|
|
|
| 4507 |
|
| 4508 |
|
| 4509 |
def _contains_any(candidatos: list[Any], consulta: str) -> bool:
|
| 4510 |
+
alvos = [_normalize(termo) for termo in re.split(r"\s*\|\|\s*|[;|]", str(consulta or ""))]
|
| 4511 |
+
alvos = [alvo for alvo in alvos if alvo]
|
| 4512 |
+
if not alvos:
|
| 4513 |
return True
|
| 4514 |
for item in candidatos:
|
| 4515 |
if item is None:
|
| 4516 |
continue
|
| 4517 |
+
item_norm = _normalize(str(item))
|
| 4518 |
+
if any(alvo in item_norm for alvo in alvos):
|
| 4519 |
return True
|
| 4520 |
return False
|
| 4521 |
|
backend/app/services/trabalhos_tecnicos_service.py
CHANGED
|
@@ -1539,8 +1539,8 @@ def gerar_mapa_trabalhos(
|
|
| 1539 |
prefer_canvas=True,
|
| 1540 |
control_scale=True,
|
| 1541 |
)
|
| 1542 |
-
folium.TileLayer(tiles="
|
| 1543 |
-
folium.TileLayer(tiles="
|
| 1544 |
add_bairros_layer(mapa, show=True)
|
| 1545 |
|
| 1546 |
camada = folium.FeatureGroup(name="Trabalhos tecnicos", show=True)
|
|
@@ -1631,8 +1631,8 @@ def _criar_mapa_trabalho(nome_trabalho: str, imoveis: list[dict[str, Any]]) -> s
|
|
| 1631 |
prefer_canvas=True,
|
| 1632 |
control_scale=True,
|
| 1633 |
)
|
| 1634 |
-
folium.TileLayer(tiles="
|
| 1635 |
-
folium.TileLayer(tiles="
|
| 1636 |
add_bairros_layer(mapa, show=True)
|
| 1637 |
|
| 1638 |
camada = folium.FeatureGroup(name="Imoveis do trabalho", show=True)
|
|
|
|
| 1539 |
prefer_canvas=True,
|
| 1540 |
control_scale=True,
|
| 1541 |
)
|
| 1542 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(mapa)
|
| 1543 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(mapa)
|
| 1544 |
add_bairros_layer(mapa, show=True)
|
| 1545 |
|
| 1546 |
camada = folium.FeatureGroup(name="Trabalhos tecnicos", show=True)
|
|
|
|
| 1631 |
prefer_canvas=True,
|
| 1632 |
control_scale=True,
|
| 1633 |
)
|
| 1634 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(mapa)
|
| 1635 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(mapa)
|
| 1636 |
add_bairros_layer(mapa, show=True)
|
| 1637 |
|
| 1638 |
camada = folium.FeatureGroup(name="Imoveis do trabalho", show=True)
|
backend/app/services/visualizacao_service.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from html import escape
|
| 4 |
from pathlib import Path
|
| 5 |
from time import sleep
|
|
@@ -14,6 +16,7 @@ from folium import plugins
|
|
| 14 |
from joblib import load
|
| 15 |
|
| 16 |
from app.core.visualizacao import app as viz_app
|
|
|
|
| 17 |
from app.core.map_layers import (
|
| 18 |
add_bairros_layer,
|
| 19 |
add_popup_pagination_handlers,
|
|
@@ -87,6 +90,98 @@ def _to_dataframe(value: Any) -> pd.DataFrame | None:
|
|
| 87 |
return None
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
def _is_rh_col(coluna: str) -> bool:
|
| 91 |
return str(coluna or "").strip().upper() == "RH"
|
| 92 |
|
|
@@ -247,8 +342,8 @@ def _criar_mapa_knn_destaque(
|
|
| 247 |
prefer_canvas=True,
|
| 248 |
control_scale=True,
|
| 249 |
)
|
| 250 |
-
folium.TileLayer(tiles="
|
| 251 |
-
folium.TileLayer(tiles="
|
| 252 |
add_bairros_layer(mapa, show=True)
|
| 253 |
|
| 254 |
posicoes_set = {int(v) for v in (posicoes_knn or [])}
|
|
@@ -621,7 +716,7 @@ def _aplicar_jitter_pontos_knn(dados: pd.DataFrame) -> pd.DataFrame:
|
|
| 621 |
if dados is None or dados.empty:
|
| 622 |
return dados
|
| 623 |
try:
|
| 624 |
-
return
|
| 625 |
dados,
|
| 626 |
lat_col="__lat__",
|
| 627 |
lon_col="__lon__",
|
|
@@ -711,7 +806,7 @@ def _criar_payload_mapa_avaliacao_localizacao(
|
|
| 711 |
return None
|
| 712 |
|
| 713 |
try:
|
| 714 |
-
dados_plot =
|
| 715 |
dados,
|
| 716 |
lat_col="__lat__",
|
| 717 |
lon_col="__lon__",
|
|
@@ -1118,12 +1213,21 @@ def _payload_modelo_graficos(session: SessionState) -> dict[str, Any]:
|
|
| 1118 |
return cached
|
| 1119 |
|
| 1120 |
figs = viz_app.gerar_todos_graficos(session.pacote_visualizacao)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
payload = {
|
| 1122 |
-
"grafico_obs_calc":
|
| 1123 |
-
"
|
| 1124 |
-
"
|
| 1125 |
-
"
|
| 1126 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
}
|
| 1128 |
tabs_cache["graficos"] = payload
|
| 1129 |
return payload
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import base64
|
| 4 |
+
import json
|
| 5 |
from html import escape
|
| 6 |
from pathlib import Path
|
| 7 |
from time import sleep
|
|
|
|
| 16 |
from joblib import load
|
| 17 |
|
| 18 |
from app.core.visualizacao import app as viz_app
|
| 19 |
+
from app.core.map_jitter import aplicar_jitter_dados_mercado
|
| 20 |
from app.core.map_layers import (
|
| 21 |
add_bairros_layer,
|
| 22 |
add_popup_pagination_handlers,
|
|
|
|
| 90 |
return None
|
| 91 |
|
| 92 |
|
| 93 |
+
def _trace_mode_includes_markers(mode: Any) -> bool:
|
| 94 |
+
return "markers" in str(mode or "").strip().lower()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _extrair_sequencia_payload(valores: Any) -> list[Any]:
|
| 98 |
+
if isinstance(valores, list):
|
| 99 |
+
return valores
|
| 100 |
+
if isinstance(valores, tuple):
|
| 101 |
+
return list(valores)
|
| 102 |
+
if isinstance(valores, dict):
|
| 103 |
+
dtype_text = str(valores.get("dtype") or "").strip()
|
| 104 |
+
bdata_text = str(valores.get("bdata") or "").strip()
|
| 105 |
+
if dtype_text and bdata_text:
|
| 106 |
+
try:
|
| 107 |
+
buffer = base64.b64decode(bdata_text)
|
| 108 |
+
array = np.frombuffer(buffer, dtype=np.dtype(dtype_text))
|
| 109 |
+
return array.tolist()
|
| 110 |
+
except Exception:
|
| 111 |
+
return []
|
| 112 |
+
return []
|
| 113 |
+
try:
|
| 114 |
+
return list(valores)
|
| 115 |
+
except Exception:
|
| 116 |
+
return []
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _coletar_rotulos_indices_trace_payload(trace: dict[str, Any] | None) -> list[str]:
|
| 120 |
+
if not isinstance(trace, dict):
|
| 121 |
+
return []
|
| 122 |
+
if not _trace_mode_includes_markers(trace.get("mode")):
|
| 123 |
+
return []
|
| 124 |
+
|
| 125 |
+
for source_key in ("customdata", "ids", "text"):
|
| 126 |
+
valores = _extrair_sequencia_payload(trace.get(source_key))
|
| 127 |
+
if not valores:
|
| 128 |
+
continue
|
| 129 |
+
rotulos: list[str] = []
|
| 130 |
+
for item in valores:
|
| 131 |
+
if isinstance(item, (list, tuple)):
|
| 132 |
+
texto = next((str(sub).strip() for sub in item if str(sub).strip()), "")
|
| 133 |
+
elif isinstance(item, dict):
|
| 134 |
+
texto = str(
|
| 135 |
+
item.get("indice")
|
| 136 |
+
or item.get("Indice")
|
| 137 |
+
or item.get("Índice")
|
| 138 |
+
or "",
|
| 139 |
+
).strip()
|
| 140 |
+
else:
|
| 141 |
+
texto = str(item or "").strip()
|
| 142 |
+
rotulos.append(texto)
|
| 143 |
+
if any(rotulos):
|
| 144 |
+
return rotulos
|
| 145 |
+
return []
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _payload_grafico_com_indices(payload: Any) -> dict[str, Any] | None:
|
| 149 |
+
payload_sanitizado = sanitize_value(payload)
|
| 150 |
+
if not isinstance(payload_sanitizado, dict):
|
| 151 |
+
return None
|
| 152 |
+
|
| 153 |
+
clone = json.loads(json.dumps(payload_sanitizado))
|
| 154 |
+
data = clone.get("data")
|
| 155 |
+
if not isinstance(data, list):
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
alterado = False
|
| 159 |
+
for trace in data:
|
| 160 |
+
if not isinstance(trace, dict):
|
| 161 |
+
continue
|
| 162 |
+
rotulos = _coletar_rotulos_indices_trace_payload(trace)
|
| 163 |
+
if not rotulos:
|
| 164 |
+
continue
|
| 165 |
+
mode_parts = [part.strip() for part in str(trace.get("mode") or "markers").split("+") if part.strip()]
|
| 166 |
+
if "text" not in mode_parts:
|
| 167 |
+
mode_parts.append("text")
|
| 168 |
+
if "markers" not in mode_parts:
|
| 169 |
+
mode_parts.append("markers")
|
| 170 |
+
trace["mode"] = "+".join(mode_parts)
|
| 171 |
+
trace["text"] = rotulos
|
| 172 |
+
trace["textposition"] = trace.get("textposition") or "top center"
|
| 173 |
+
base_textfont = trace.get("textfont") if isinstance(trace.get("textfont"), dict) else {}
|
| 174 |
+
trace["textfont"] = {
|
| 175 |
+
"size": 10,
|
| 176 |
+
"color": "#243746",
|
| 177 |
+
**base_textfont,
|
| 178 |
+
}
|
| 179 |
+
trace["cliponaxis"] = False
|
| 180 |
+
alterado = True
|
| 181 |
+
|
| 182 |
+
return clone if alterado else None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
def _is_rh_col(coluna: str) -> bool:
|
| 186 |
return str(coluna or "").strip().upper() == "RH"
|
| 187 |
|
|
|
|
| 342 |
prefer_canvas=True,
|
| 343 |
control_scale=True,
|
| 344 |
)
|
| 345 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=True).add_to(mapa)
|
| 346 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=False).add_to(mapa)
|
| 347 |
add_bairros_layer(mapa, show=True)
|
| 348 |
|
| 349 |
posicoes_set = {int(v) for v in (posicoes_knn or [])}
|
|
|
|
| 716 |
if dados is None or dados.empty:
|
| 717 |
return dados
|
| 718 |
try:
|
| 719 |
+
return aplicar_jitter_dados_mercado(
|
| 720 |
dados,
|
| 721 |
lat_col="__lat__",
|
| 722 |
lon_col="__lon__",
|
|
|
|
| 806 |
return None
|
| 807 |
|
| 808 |
try:
|
| 809 |
+
dados_plot = aplicar_jitter_dados_mercado(
|
| 810 |
dados,
|
| 811 |
lat_col="__lat__",
|
| 812 |
lon_col="__lon__",
|
|
|
|
| 1213 |
return cached
|
| 1214 |
|
| 1215 |
figs = viz_app.gerar_todos_graficos(session.pacote_visualizacao)
|
| 1216 |
+
grafico_obs_calc = figure_to_payload(figs.get("obs_calc"))
|
| 1217 |
+
grafico_residuos = figure_to_payload(figs.get("residuos"))
|
| 1218 |
+
grafico_histograma = figure_to_payload(figs.get("hist"))
|
| 1219 |
+
grafico_cook = figure_to_payload(figs.get("cook"))
|
| 1220 |
+
grafico_correlacao = figure_to_payload(figs.get("corr"))
|
| 1221 |
payload = {
|
| 1222 |
+
"grafico_obs_calc": grafico_obs_calc,
|
| 1223 |
+
"grafico_obs_calc_com_indices": _payload_grafico_com_indices(grafico_obs_calc),
|
| 1224 |
+
"grafico_residuos": grafico_residuos,
|
| 1225 |
+
"grafico_residuos_com_indices": _payload_grafico_com_indices(grafico_residuos),
|
| 1226 |
+
"grafico_histograma": grafico_histograma,
|
| 1227 |
+
"grafico_histograma_com_indices": _payload_grafico_com_indices(grafico_histograma),
|
| 1228 |
+
"grafico_cook": grafico_cook,
|
| 1229 |
+
"grafico_cook_com_indices": _payload_grafico_com_indices(grafico_cook),
|
| 1230 |
+
"grafico_correlacao": grafico_correlacao,
|
| 1231 |
}
|
| 1232 |
tabs_cache["graficos"] = payload
|
| 1233 |
return payload
|
frontend/src/api.js
CHANGED
|
@@ -210,6 +210,7 @@ export const api = {
|
|
| 210 |
trabalhosTecnicosModelosModo = 'selecionados',
|
| 211 |
trabalhosTecnicosProximidadeModo = 'sem_proximidade',
|
| 212 |
trabalhosTecnicosRaioM = 1000,
|
|
|
|
| 213 |
) {
|
| 214 |
const avaliandos = Array.isArray(avaliando) ? avaliando : []
|
| 215 |
const avaliandoUnico = !avaliandos.length && avaliando && typeof avaliando === 'object' ? avaliando : null
|
|
@@ -223,6 +224,7 @@ export const api = {
|
|
| 223 |
trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
|
| 224 |
trabalhos_tecnicos_proximidade_modo: trabalhosTecnicosProximidadeModo,
|
| 225 |
trabalhos_tecnicos_raio_m: trabalhosTecnicosRaioM,
|
|
|
|
| 226 |
})
|
| 227 |
},
|
| 228 |
|
|
|
|
| 210 |
trabalhosTecnicosModelosModo = 'selecionados',
|
| 211 |
trabalhosTecnicosProximidadeModo = 'sem_proximidade',
|
| 212 |
trabalhosTecnicosRaioM = 1000,
|
| 213 |
+
agruparPontosMercado = false,
|
| 214 |
) {
|
| 215 |
const avaliandos = Array.isArray(avaliando) ? avaliando : []
|
| 216 |
const avaliandoUnico = !avaliandos.length && avaliando && typeof avaliando === 'object' ? avaliando : null
|
|
|
|
| 224 |
trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
|
| 225 |
trabalhos_tecnicos_proximidade_modo: trabalhosTecnicosProximidadeModo,
|
| 226 |
trabalhos_tecnicos_raio_m: trabalhosTecnicosRaioM,
|
| 227 |
+
agrupar_pontos_mercado: Boolean(agruparPontosMercado),
|
| 228 |
})
|
| 229 |
},
|
| 230 |
|
frontend/src/components/AvaliacaoTab.jsx
CHANGED
|
@@ -596,6 +596,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 596 |
const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
|
| 597 |
const [confirmarExclusaoCardId, setConfirmarExclusaoCardId] = useState('')
|
| 598 |
const [avaliacaoPopup, setAvaliacaoPopup] = useState(null)
|
|
|
|
| 599 |
const [knnDetalheAberto, setKnnDetalheAberto] = useState(false)
|
| 600 |
const [knnDetalheLoading, setKnnDetalheLoading] = useState(false)
|
| 601 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
|
@@ -1230,6 +1231,20 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1230 |
setAvaliacaoPopup(null)
|
| 1231 |
}
|
| 1232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
async function onAbrirDetalheKnn(card, indice) {
|
| 1234 |
if (!sessionId || !card?.avaliacao?.valores_x) return
|
| 1235 |
setKnnDetalheAberto(true)
|
|
@@ -1955,6 +1970,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1955 |
onMouseLeave={onPopupLeave}
|
| 1956 |
onFocus={(event) => onPopupEnter(event, popupPrecisaoHtml(aval))}
|
| 1957 |
onBlur={onPopupLeave}
|
|
|
|
| 1958 |
>
|
| 1959 |
ⓘ
|
| 1960 |
</button>
|
|
@@ -1972,6 +1988,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 1972 |
onMouseLeave={onPopupLeave}
|
| 1973 |
onFocus={(event) => onPopupEnter(event, popupFundamentacaoHtml(aval))}
|
| 1974 |
onBlur={onPopupLeave}
|
|
|
|
| 1975 |
>
|
| 1976 |
ⓘ
|
| 1977 |
</button>
|
|
@@ -2001,6 +2018,27 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRou
|
|
| 2001 |
dangerouslySetInnerHTML={{ __html: avaliacaoPopup.html }}
|
| 2002 |
/>
|
| 2003 |
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2004 |
{avaliandoMapaAberto ? (
|
| 2005 |
<div className="pesquisa-modal-backdrop" onClick={(event) => {
|
| 2006 |
if (event.target === event.currentTarget) onFecharMapaAvaliando()
|
|
|
|
| 596 |
const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
|
| 597 |
const [confirmarExclusaoCardId, setConfirmarExclusaoCardId] = useState('')
|
| 598 |
const [avaliacaoPopup, setAvaliacaoPopup] = useState(null)
|
| 599 |
+
const [avaliacaoInfoModal, setAvaliacaoInfoModal] = useState(null)
|
| 600 |
const [knnDetalheAberto, setKnnDetalheAberto] = useState(false)
|
| 601 |
const [knnDetalheLoading, setKnnDetalheLoading] = useState(false)
|
| 602 |
const [knnDetalheErro, setKnnDetalheErro] = useState('')
|
|
|
|
| 1231 |
setAvaliacaoPopup(null)
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
+
function onAbrirInfoAvaliacao(titulo, html) {
|
| 1235 |
+
const conteudo = String(html || '').trim()
|
| 1236 |
+
if (!conteudo) return
|
| 1237 |
+
setAvaliacaoPopup(null)
|
| 1238 |
+
setAvaliacaoInfoModal({
|
| 1239 |
+
titulo: String(titulo || 'Detalhes do enquadramento'),
|
| 1240 |
+
html: conteudo,
|
| 1241 |
+
})
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
function onFecharInfoAvaliacao() {
|
| 1245 |
+
setAvaliacaoInfoModal(null)
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
async function onAbrirDetalheKnn(card, indice) {
|
| 1249 |
if (!sessionId || !card?.avaliacao?.valores_x) return
|
| 1250 |
setKnnDetalheAberto(true)
|
|
|
|
| 1970 |
onMouseLeave={onPopupLeave}
|
| 1971 |
onFocus={(event) => onPopupEnter(event, popupPrecisaoHtml(aval))}
|
| 1972 |
onBlur={onPopupLeave}
|
| 1973 |
+
onClick={() => onAbrirInfoAvaliacao('Detalhes da precisão', popupPrecisaoHtml(aval))}
|
| 1974 |
>
|
| 1975 |
ⓘ
|
| 1976 |
</button>
|
|
|
|
| 1988 |
onMouseLeave={onPopupLeave}
|
| 1989 |
onFocus={(event) => onPopupEnter(event, popupFundamentacaoHtml(aval))}
|
| 1990 |
onBlur={onPopupLeave}
|
| 1991 |
+
onClick={() => onAbrirInfoAvaliacao('Detalhes da fundamentação', popupFundamentacaoHtml(aval))}
|
| 1992 |
>
|
| 1993 |
ⓘ
|
| 1994 |
</button>
|
|
|
|
| 2018 |
dangerouslySetInnerHTML={{ __html: avaliacaoPopup.html }}
|
| 2019 |
/>
|
| 2020 |
) : null}
|
| 2021 |
+
{avaliacaoInfoModal?.html ? (
|
| 2022 |
+
<div className="pesquisa-modal-backdrop" onClick={(event) => {
|
| 2023 |
+
if (event.target === event.currentTarget) onFecharInfoAvaliacao()
|
| 2024 |
+
}}
|
| 2025 |
+
>
|
| 2026 |
+
<div className="pesquisa-modal avaliacao-info-modal">
|
| 2027 |
+
<div className="pesquisa-modal-head">
|
| 2028 |
+
<div>
|
| 2029 |
+
<h4>{avaliacaoInfoModal.titulo || 'Detalhes do enquadramento'}</h4>
|
| 2030 |
+
</div>
|
| 2031 |
+
<button type="button" className="pesquisa-modal-close" onClick={onFecharInfoAvaliacao}>
|
| 2032 |
+
Fechar
|
| 2033 |
+
</button>
|
| 2034 |
+
</div>
|
| 2035 |
+
<div
|
| 2036 |
+
className="pesquisa-modal-body avaliacao-info-modal-body"
|
| 2037 |
+
dangerouslySetInnerHTML={{ __html: avaliacaoInfoModal.html }}
|
| 2038 |
+
/>
|
| 2039 |
+
</div>
|
| 2040 |
+
</div>
|
| 2041 |
+
) : null}
|
| 2042 |
{avaliandoMapaAberto ? (
|
| 2043 |
<div className="pesquisa-modal-backdrop" onClick={(event) => {
|
| 2044 |
if (event.target === event.currentTarget) onFecharMapaAvaliando()
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -240,13 +240,56 @@ function formatCurrencyBr(value) {
|
|
| 240 |
return num.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
function Section14MetricRows({ rows }) {
|
| 244 |
return (
|
| 245 |
<div className="section14-field-grid">
|
| 246 |
{(rows || []).map((row) => (
|
| 247 |
<div key={row.label} className="section14-field-row">
|
| 248 |
<span className="section14-field-label">{row.label}</span>
|
| 249 |
-
<span className="section14-field-value">{row.value}</span>
|
| 250 |
</div>
|
| 251 |
))}
|
| 252 |
</div>
|
|
@@ -1656,6 +1699,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 1656 |
const [tabelaOutliersExcluidos, setTabelaOutliersExcluidos] = useState(null)
|
| 1657 |
const [outlierLimitWarning, setOutlierLimitWarning] = useState('')
|
| 1658 |
const [outlierUpdateWarning, setOutlierUpdateWarning] = useState('')
|
|
|
|
| 1659 |
|
| 1660 |
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 1661 |
const valoresAvaliacaoRef = useRef({})
|
|
@@ -2019,7 +2063,11 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 2019 |
id: 'curva',
|
| 2020 |
label: 'Curva normal',
|
| 2021 |
rows: [
|
| 2022 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2023 |
{ label: 'Ideal 68%', value: 'aceitável entre 64% e 75%' },
|
| 2024 |
{ label: 'Ideal 90%', value: 'aceitável entre 88% e 95%' },
|
| 2025 |
{ label: 'Ideal 95%', value: 'aceitável entre 95% e 100%' },
|
|
@@ -3100,6 +3148,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3100 |
setTabelaOutliersExcluidos(null)
|
| 3101 |
setOutlierLimitWarning('')
|
| 3102 |
setOutlierUpdateWarning('')
|
|
|
|
| 3103 |
setOutliersAnteriores([])
|
| 3104 |
setIteracao(1)
|
| 3105 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
|
@@ -3231,6 +3280,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3231 |
setTabelaOutliersExcluidos(null)
|
| 3232 |
setOutlierLimitWarning('')
|
| 3233 |
setOutlierUpdateWarning('')
|
|
|
|
| 3234 |
setCamposAvaliacao([])
|
| 3235 |
valoresAvaliacaoRef.current = {}
|
| 3236 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
@@ -3328,6 +3378,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3328 |
setFit(resp)
|
| 3329 |
setOutlierLimitWarning('')
|
| 3330 |
setOutlierUpdateWarning('')
|
|
|
|
| 3331 |
setSecao13InterativoFigura(null)
|
| 3332 |
setSecao13InterativoFiguraComIndices(null)
|
| 3333 |
setSecao13InterativoSelecionado('none')
|
|
@@ -3497,6 +3548,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3497 |
setTabelaOutliersExcluidos(null)
|
| 3498 |
setOutlierLimitWarning('')
|
| 3499 |
setOutlierUpdateWarning('')
|
|
|
|
| 3500 |
setOutliersAnteriores([])
|
| 3501 |
setIteracao(1)
|
| 3502 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
|
@@ -3881,6 +3933,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3881 |
setTabelaOutliersExcluidos(null)
|
| 3882 |
setOutlierLimitWarning('')
|
| 3883 |
setOutlierUpdateWarning('')
|
|
|
|
| 3884 |
setOutliersTexto('')
|
| 3885 |
setReincluirTexto('')
|
| 3886 |
setBaseChoices([])
|
|
@@ -4034,6 +4087,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4034 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 4035 |
setOutlierLimitWarning('')
|
| 4036 |
setOutlierUpdateWarning('')
|
|
|
|
| 4037 |
setCamposAvaliacao([])
|
| 4038 |
valoresAvaliacaoRef.current = {}
|
| 4039 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
@@ -4210,6 +4264,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4210 |
if (!sessionId) return
|
| 4211 |
setOutlierLimitWarning('')
|
| 4212 |
setOutlierUpdateWarning('')
|
|
|
|
| 4213 |
await withBusy(
|
| 4214 |
async () => {
|
| 4215 |
const filtrosValidos = (filtros || [])
|
|
@@ -4241,6 +4296,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4241 |
if (!sessionId) return
|
| 4242 |
setOutlierLimitWarning('')
|
| 4243 |
setOutlierUpdateWarning('')
|
|
|
|
| 4244 |
await withBusy(
|
| 4245 |
async () => {
|
| 4246 |
const filtrosValidos = (filtros || [])
|
|
@@ -4328,6 +4384,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4328 |
if (!sessionId) return
|
| 4329 |
setOutlierLimitWarning('')
|
| 4330 |
setOutlierUpdateWarning('')
|
|
|
|
| 4331 |
await withBusy(
|
| 4332 |
async () => {
|
| 4333 |
const resp = await api.restartOutlierIteration(sessionId, outliersInput, reincluirInput, grauCoef, grauF)
|
|
@@ -4350,6 +4407,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4350 |
mensagemRegressao ||
|
| 4351 |
'Outliers atualizados, mas a regressão não fechou. Reinclua dados ou ajuste os filtros antes de continuar.',
|
| 4352 |
)
|
|
|
|
| 4353 |
setSection14Tab('diagnosticos')
|
| 4354 |
if (typeof window !== 'undefined') {
|
| 4355 |
window.setTimeout(() => {
|
|
@@ -4375,6 +4433,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4375 |
mensagemRegressao ||
|
| 4376 |
'Outliers atualizados. Volte ao passo 12, Aplicação das Transformações, para ajustar novamente o modelo.',
|
| 4377 |
)
|
|
|
|
| 4378 |
await sleep(0)
|
| 4379 |
if (typeof window !== 'undefined') {
|
| 4380 |
window.setTimeout(() => {
|
|
@@ -4386,7 +4445,8 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4386 |
suppressError: isOutlierLimitError,
|
| 4387 |
onError: (err) => {
|
| 4388 |
if (isOutlierLimitError(err)) {
|
| 4389 |
-
|
|
|
|
| 4390 |
}
|
| 4391 |
},
|
| 4392 |
},
|
|
@@ -4445,6 +4505,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 4445 |
setTabelaOutliersExcluidos(null)
|
| 4446 |
setOutlierLimitWarning('')
|
| 4447 |
setOutlierUpdateWarning('')
|
|
|
|
| 4448 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 4449 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 4450 |
syncPeriodoDataMercadoFromContext(resp.contexto)
|
|
@@ -6072,7 +6133,13 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 6072 |
Fazer download
|
| 6073 |
</button>
|
| 6074 |
</div>
|
| 6075 |
-
<DataTable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6076 |
</div>
|
| 6077 |
</SectionBlock>
|
| 6078 |
|
|
@@ -6674,7 +6741,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 6674 |
{section10ManualOpen ? 'Ocultar ajustes manuais de transformação' : 'Proceder com as transformações manualmente'}
|
| 6675 |
</button>
|
| 6676 |
</div>
|
| 6677 |
-
{!fit && outlierUpdateWarning ? (
|
| 6678 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 6679 |
) : null}
|
| 6680 |
{!fit && outliersAnteriores.length > 0 && tabelaOutliersExcluidosAtual ? (
|
|
@@ -7063,7 +7130,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 7063 |
</SectionBlock>
|
| 7064 |
|
| 7065 |
<SectionBlock step="14" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 7066 |
-
{outlierUpdateWarning && fit ? (
|
| 7067 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 7068 |
) : null}
|
| 7069 |
<div className="section14-tabs" role="tablist" aria-label="Conteúdos do diagnóstico do modelo">
|
|
@@ -7545,7 +7612,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 7545 |
Reiniciar Modelo (Reincluir Todos)
|
| 7546 |
</button>
|
| 7547 |
</div>
|
| 7548 |
-
{outlierUpdateWarning && fit ? (
|
| 7549 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 7550 |
) : null}
|
| 7551 |
</div>
|
|
|
|
| 240 |
return num.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
| 241 |
}
|
| 242 |
|
| 243 |
+
const CURVA_NORMAL_PERCENTUAL_FAIXAS = [
|
| 244 |
+
{ min: 64, max: 75 },
|
| 245 |
+
{ min: 88, max: 95 },
|
| 246 |
+
{ min: 95, max: 100 },
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
function parseCurvaNormalPercentual(value) {
|
| 250 |
+
const match = String(value || '').replace(',', '.').match(/-?\d+(?:\.\d+)?/)
|
| 251 |
+
if (!match) return null
|
| 252 |
+
const numero = Number(match[0])
|
| 253 |
+
return Number.isFinite(numero) ? numero : null
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function formatCurvaNormalPercentuais(value) {
|
| 257 |
+
const raw = String(value || '').trim()
|
| 258 |
+
if (!raw || raw === '-') return raw || '-'
|
| 259 |
+
|
| 260 |
+
const partes = raw.split(',').map((item) => item.trim()).filter(Boolean)
|
| 261 |
+
if (!partes.length) return raw
|
| 262 |
+
|
| 263 |
+
return (
|
| 264 |
+
<span className="section14-curva-normal-percentuais">
|
| 265 |
+
{partes.map((parte, index) => {
|
| 266 |
+
const faixa = CURVA_NORMAL_PERCENTUAL_FAIXAS[index]
|
| 267 |
+
const percentual = parseCurvaNormalPercentual(parte)
|
| 268 |
+
const foraDaFaixa = Boolean(
|
| 269 |
+
faixa
|
| 270 |
+
&& percentual !== null
|
| 271 |
+
&& (percentual < faixa.min || percentual > faixa.max),
|
| 272 |
+
)
|
| 273 |
+
return (
|
| 274 |
+
<React.Fragment key={`curva-normal-percentual-${index}-${parte}`}>
|
| 275 |
+
{index > 0 ? <span>, </span> : null}
|
| 276 |
+
<span className={foraDaFaixa ? 'section14-percentual-fora-faixa' : undefined}>
|
| 277 |
+
{parte}
|
| 278 |
+
</span>
|
| 279 |
+
</React.Fragment>
|
| 280 |
+
)
|
| 281 |
+
})}
|
| 282 |
+
</span>
|
| 283 |
+
)
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
function Section14MetricRows({ rows }) {
|
| 287 |
return (
|
| 288 |
<div className="section14-field-grid">
|
| 289 |
{(rows || []).map((row) => (
|
| 290 |
<div key={row.label} className="section14-field-row">
|
| 291 |
<span className="section14-field-label">{row.label}</span>
|
| 292 |
+
<span className="section14-field-value">{row.valueNode ?? row.value}</span>
|
| 293 |
</div>
|
| 294 |
))}
|
| 295 |
</div>
|
|
|
|
| 1699 |
const [tabelaOutliersExcluidos, setTabelaOutliersExcluidos] = useState(null)
|
| 1700 |
const [outlierLimitWarning, setOutlierLimitWarning] = useState('')
|
| 1701 |
const [outlierUpdateWarning, setOutlierUpdateWarning] = useState('')
|
| 1702 |
+
const [outlierUpdateWarningKind, setOutlierUpdateWarningKind] = useState('')
|
| 1703 |
|
| 1704 |
const [camposAvaliacao, setCamposAvaliacao] = useState([])
|
| 1705 |
const valoresAvaliacaoRef = useRef({})
|
|
|
|
| 2063 |
id: 'curva',
|
| 2064 |
label: 'Curva normal',
|
| 2065 |
rows: [
|
| 2066 |
+
{
|
| 2067 |
+
label: 'Percentuais atingidos',
|
| 2068 |
+
value: diagnosticos.perc_resid || '-',
|
| 2069 |
+
valueNode: formatCurvaNormalPercentuais(diagnosticos.perc_resid),
|
| 2070 |
+
},
|
| 2071 |
{ label: 'Ideal 68%', value: 'aceitável entre 64% e 75%' },
|
| 2072 |
{ label: 'Ideal 90%', value: 'aceitável entre 88% e 95%' },
|
| 2073 |
{ label: 'Ideal 95%', value: 'aceitável entre 95% e 100%' },
|
|
|
|
| 3148 |
setTabelaOutliersExcluidos(null)
|
| 3149 |
setOutlierLimitWarning('')
|
| 3150 |
setOutlierUpdateWarning('')
|
| 3151 |
+
setOutlierUpdateWarningKind('')
|
| 3152 |
setOutliersAnteriores([])
|
| 3153 |
setIteracao(1)
|
| 3154 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
|
|
|
| 3280 |
setTabelaOutliersExcluidos(null)
|
| 3281 |
setOutlierLimitWarning('')
|
| 3282 |
setOutlierUpdateWarning('')
|
| 3283 |
+
setOutlierUpdateWarningKind('')
|
| 3284 |
setCamposAvaliacao([])
|
| 3285 |
valoresAvaliacaoRef.current = {}
|
| 3286 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 3378 |
setFit(resp)
|
| 3379 |
setOutlierLimitWarning('')
|
| 3380 |
setOutlierUpdateWarning('')
|
| 3381 |
+
setOutlierUpdateWarningKind('')
|
| 3382 |
setSecao13InterativoFigura(null)
|
| 3383 |
setSecao13InterativoFiguraComIndices(null)
|
| 3384 |
setSecao13InterativoSelecionado('none')
|
|
|
|
| 3548 |
setTabelaOutliersExcluidos(null)
|
| 3549 |
setOutlierLimitWarning('')
|
| 3550 |
setOutlierUpdateWarning('')
|
| 3551 |
+
setOutlierUpdateWarningKind('')
|
| 3552 |
setOutliersAnteriores([])
|
| 3553 |
setIteracao(1)
|
| 3554 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
|
|
|
| 3933 |
setTabelaOutliersExcluidos(null)
|
| 3934 |
setOutlierLimitWarning('')
|
| 3935 |
setOutlierUpdateWarning('')
|
| 3936 |
+
setOutlierUpdateWarningKind('')
|
| 3937 |
setOutliersTexto('')
|
| 3938 |
setReincluirTexto('')
|
| 3939 |
setBaseChoices([])
|
|
|
|
| 4087 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 4088 |
setOutlierLimitWarning('')
|
| 4089 |
setOutlierUpdateWarning('')
|
| 4090 |
+
setOutlierUpdateWarningKind('')
|
| 4091 |
setCamposAvaliacao([])
|
| 4092 |
valoresAvaliacaoRef.current = {}
|
| 4093 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 4264 |
if (!sessionId) return
|
| 4265 |
setOutlierLimitWarning('')
|
| 4266 |
setOutlierUpdateWarning('')
|
| 4267 |
+
setOutlierUpdateWarningKind('')
|
| 4268 |
await withBusy(
|
| 4269 |
async () => {
|
| 4270 |
const filtrosValidos = (filtros || [])
|
|
|
|
| 4296 |
if (!sessionId) return
|
| 4297 |
setOutlierLimitWarning('')
|
| 4298 |
setOutlierUpdateWarning('')
|
| 4299 |
+
setOutlierUpdateWarningKind('')
|
| 4300 |
await withBusy(
|
| 4301 |
async () => {
|
| 4302 |
const filtrosValidos = (filtros || [])
|
|
|
|
| 4384 |
if (!sessionId) return
|
| 4385 |
setOutlierLimitWarning('')
|
| 4386 |
setOutlierUpdateWarning('')
|
| 4387 |
+
setOutlierUpdateWarningKind('')
|
| 4388 |
await withBusy(
|
| 4389 |
async () => {
|
| 4390 |
const resp = await api.restartOutlierIteration(sessionId, outliersInput, reincluirInput, grauCoef, grauF)
|
|
|
|
| 4407 |
mensagemRegressao ||
|
| 4408 |
'Outliers atualizados, mas a regressão não fechou. Reinclua dados ou ajuste os filtros antes de continuar.',
|
| 4409 |
)
|
| 4410 |
+
setOutlierUpdateWarningKind('regressao')
|
| 4411 |
setSection14Tab('diagnosticos')
|
| 4412 |
if (typeof window !== 'undefined') {
|
| 4413 |
window.setTimeout(() => {
|
|
|
|
| 4433 |
mensagemRegressao ||
|
| 4434 |
'Outliers atualizados. Volte ao passo 12, Aplicação das Transformações, para ajustar novamente o modelo.',
|
| 4435 |
)
|
| 4436 |
+
setOutlierUpdateWarningKind('success')
|
| 4437 |
await sleep(0)
|
| 4438 |
if (typeof window !== 'undefined') {
|
| 4439 |
window.setTimeout(() => {
|
|
|
|
| 4445 |
suppressError: isOutlierLimitError,
|
| 4446 |
onError: (err) => {
|
| 4447 |
if (isOutlierLimitError(err)) {
|
| 4448 |
+
setOutlierUpdateWarning(getErrorMessage(err))
|
| 4449 |
+
setOutlierUpdateWarningKind('micronumerosidade')
|
| 4450 |
}
|
| 4451 |
},
|
| 4452 |
},
|
|
|
|
| 4505 |
setTabelaOutliersExcluidos(null)
|
| 4506 |
setOutlierLimitWarning('')
|
| 4507 |
setOutlierUpdateWarning('')
|
| 4508 |
+
setOutlierUpdateWarningKind('')
|
| 4509 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 4510 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 4511 |
syncPeriodoDataMercadoFromContext(resp.contexto)
|
|
|
|
| 6133 |
Fazer download
|
| 6134 |
</button>
|
| 6135 |
</div>
|
| 6136 |
+
<DataTable
|
| 6137 |
+
table={dados}
|
| 6138 |
+
maxHeight={320}
|
| 6139 |
+
highlightedRowIndices={outliersAnteriores}
|
| 6140 |
+
highlightIndexColumn="_index"
|
| 6141 |
+
highlightClassName="table-row-outlier-excluded"
|
| 6142 |
+
/>
|
| 6143 |
</div>
|
| 6144 |
</SectionBlock>
|
| 6145 |
|
|
|
|
| 6741 |
{section10ManualOpen ? 'Ocultar ajustes manuais de transformação' : 'Proceder com as transformações manualmente'}
|
| 6742 |
</button>
|
| 6743 |
</div>
|
| 6744 |
+
{!fit && outlierUpdateWarning && outlierUpdateWarningKind === 'success' ? (
|
| 6745 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 6746 |
) : null}
|
| 6747 |
{!fit && outliersAnteriores.length > 0 && tabelaOutliersExcluidosAtual ? (
|
|
|
|
| 7130 |
</SectionBlock>
|
| 7131 |
|
| 7132 |
<SectionBlock step="14" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 7133 |
+
{outlierUpdateWarning && fit && outlierUpdateWarningKind === 'regressao' ? (
|
| 7134 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 7135 |
) : null}
|
| 7136 |
<div className="section14-tabs" role="tablist" aria-label="Conteúdos do diagnóstico do modelo">
|
|
|
|
| 7612 |
Reiniciar Modelo (Reincluir Todos)
|
| 7613 |
</button>
|
| 7614 |
</div>
|
| 7615 |
+
{outlierUpdateWarning && fit && ['regressao', 'micronumerosidade'].includes(outlierUpdateWarningKind) ? (
|
| 7616 |
<div className="outlier-update-warning">{outlierUpdateWarning}</div>
|
| 7617 |
) : null}
|
| 7618 |
</div>
|
frontend/src/components/LeafletMapFrame.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
| 2 |
import L from 'leaflet'
|
| 3 |
import 'leaflet.fullscreen'
|
| 4 |
import 'leaflet.heat'
|
|
@@ -37,6 +37,42 @@ function buildPopupErrorHtml(message) {
|
|
| 37 |
)
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
function parseFiniteCoordinate(value, min, max) {
|
| 41 |
if (value === null || value === undefined) return null
|
| 42 |
const text = String(value).trim().replace(',', '.')
|
|
@@ -336,7 +372,7 @@ function buildLegacyOverlayLayers(payload) {
|
|
| 336 |
show: true,
|
| 337 |
points: payload.market_points.map((item) => ({
|
| 338 |
...item,
|
| 339 |
-
popup_request:
|
| 340 |
? { kind: 'visualizacao_row', row_id: Number(item.row_id) }
|
| 341 |
: null,
|
| 342 |
})),
|
|
@@ -685,10 +721,12 @@ async function buildHighResolutionSelectionBlob({
|
|
| 685 |
|
| 686 |
const bairrosPane = exportMap.createPane('mesa-bairros-pane')
|
| 687 |
const marketPane = exportMap.createPane('mesa-market-pane')
|
|
|
|
| 688 |
const trabalhosPane = exportMap.createPane('mesa-trabalhos-pane')
|
| 689 |
const indicesPane = exportMap.createPane('mesa-indices-pane')
|
| 690 |
bairrosPane.style.zIndex = '410'
|
| 691 |
marketPane.style.zIndex = '420'
|
|
|
|
| 692 |
trabalhosPane.style.zIndex = '430'
|
| 693 |
indicesPane.style.zIndex = '440'
|
| 694 |
|
|
@@ -745,6 +783,20 @@ async function buildHighResolutionSelectionBlob({
|
|
| 745 |
items.forEach((item) => {
|
| 746 |
const latlng = parseLatLonPair(item?.lat, item?.lon)
|
| 747 |
if (!latlng) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
const marker = L.circleMarker(latlng, {
|
| 749 |
renderer: canvasRenderer,
|
| 750 |
pane: String(item?.pane || 'mesa-market-pane'),
|
|
@@ -971,7 +1023,10 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 971 |
const mapRef = useRef(null)
|
| 972 |
const payloadRef = useRef(payload)
|
| 973 |
const popupCacheRef = useRef(new Map())
|
|
|
|
|
|
|
| 974 |
const [runtimeError, setRuntimeError] = useState('')
|
|
|
|
| 975 |
|
| 976 |
payloadRef.current = payload
|
| 977 |
|
|
@@ -1016,10 +1071,26 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1016 |
},
|
| 1017 |
}), [])
|
| 1018 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
useEffect(() => {
|
| 1020 |
if (!hostRef.current || !payload) return undefined
|
| 1021 |
let disposed = false
|
| 1022 |
setRuntimeError('')
|
|
|
|
|
|
|
| 1023 |
hostRef.current.innerHTML = ''
|
| 1024 |
|
| 1025 |
const map = L.map(hostRef.current, {
|
|
@@ -1030,10 +1101,12 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1030 |
let restoreMapInteractions = null
|
| 1031 |
const bairrosPane = map.createPane('mesa-bairros-pane')
|
| 1032 |
const marketPane = map.createPane('mesa-market-pane')
|
|
|
|
| 1033 |
const trabalhosPane = map.createPane('mesa-trabalhos-pane')
|
| 1034 |
const indicesPane = map.createPane('mesa-indices-pane')
|
| 1035 |
bairrosPane.style.zIndex = '410'
|
| 1036 |
marketPane.style.zIndex = '420'
|
|
|
|
| 1037 |
trabalhosPane.style.zIndex = '430'
|
| 1038 |
indicesPane.style.zIndex = '440'
|
| 1039 |
|
|
@@ -1046,9 +1119,7 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1046 |
|
| 1047 |
;(payload.tile_layers || []).forEach((layerDef, index) => {
|
| 1048 |
const tileLayer = L.tileLayer(String(layerDef?.url || ''), {
|
| 1049 |
-
attribution:
|
| 1050 |
-
? '© OpenStreetMap contributors'
|
| 1051 |
-
: '© OpenStreetMap contributors © CARTO',
|
| 1052 |
crossOrigin: 'anonymous',
|
| 1053 |
detectRetina: true,
|
| 1054 |
})
|
|
@@ -1101,17 +1172,29 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1101 |
responsivePointContainers.forEach((container) => {
|
| 1102 |
if (!container || typeof container.eachLayer !== 'function') return
|
| 1103 |
container.eachLayer((layer) => {
|
| 1104 |
-
if (typeof layer.setRadius !== 'function') return
|
| 1105 |
const base = Number(layer.options?.mesaBaseRadius || layer.options?.radius || 4)
|
| 1106 |
const dynamicMin = Math.max(minRadius, base * floorScale)
|
| 1107 |
const dynamicMax = Math.max(dynamicMin + 0.1, Math.min(maxRadius, base * 8.0))
|
| 1108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
})
|
| 1110 |
})
|
| 1111 |
}
|
| 1112 |
|
| 1113 |
-
async function
|
| 1114 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
const cached = popupCacheRef.current.get(cacheKey)
|
| 1116 |
if (cached) {
|
| 1117 |
const cachedHtml = typeof cached === 'string' ? cached : String(cached?.html || '')
|
|
@@ -1131,16 +1214,20 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1131 |
).openPopup()
|
| 1132 |
|
| 1133 |
try {
|
| 1134 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
method: 'POST',
|
| 1136 |
headers: {
|
| 1137 |
'Content-Type': 'application/json',
|
| 1138 |
...(getAuthToken() ? { 'X-Auth-Token': getAuthToken() } : {}),
|
| 1139 |
},
|
| 1140 |
-
body: JSON.stringify(
|
| 1141 |
-
session_id: sessionId,
|
| 1142 |
-
row_id: rowId,
|
| 1143 |
-
}),
|
| 1144 |
})
|
| 1145 |
const payloadResp = await readJsonSafely(response)
|
| 1146 |
if (!response.ok) {
|
|
@@ -1167,6 +1254,8 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1167 |
}
|
| 1168 |
}
|
| 1169 |
|
|
|
|
|
|
|
| 1170 |
function bindPointPopup(marker, item) {
|
| 1171 |
const popupHtml = String(item?.popup_html || '').trim()
|
| 1172 |
if (popupHtml) {
|
|
@@ -1175,9 +1264,14 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1175 |
}
|
| 1176 |
|
| 1177 |
const popupRequest = item?.popup_request
|
| 1178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1179 |
marker.on('click', () => {
|
| 1180 |
-
void
|
| 1181 |
})
|
| 1182 |
}
|
| 1183 |
}
|
|
@@ -1378,6 +1472,36 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1378 |
items.forEach((item) => {
|
| 1379 |
const latlng = parseLatLonPair(item?.lat, item?.lon)
|
| 1380 |
if (!latlng) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1381 |
const marker = L.circleMarker(latlng, {
|
| 1382 |
renderer: canvasRenderer,
|
| 1383 |
pane: String(item?.pane || 'mesa-market-pane'),
|
|
@@ -1937,6 +2061,10 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1937 |
}
|
| 1938 |
disposed = true
|
| 1939 |
restoreMapInteractions?.()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1940 |
if (mapRef.current === map) {
|
| 1941 |
mapRef.current = null
|
| 1942 |
}
|
|
@@ -1948,6 +2076,47 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
|
|
| 1948 |
<div className="map-frame leaflet-map-host">
|
| 1949 |
<div ref={hostRef} className="leaflet-map-canvas" />
|
| 1950 |
{runtimeError ? <div className="leaflet-map-runtime-error">{runtimeError}</div> : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1951 |
</div>
|
| 1952 |
)
|
| 1953 |
})
|
|
|
|
| 1 |
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
| 2 |
import L from 'leaflet'
|
| 3 |
import 'leaflet.fullscreen'
|
| 4 |
import 'leaflet.heat'
|
|
|
|
| 37 |
)
|
| 38 |
}
|
| 39 |
|
| 40 |
+
function normalizeCssColor(value, fallback = '#607d8b') {
|
| 41 |
+
const text = String(value || '').trim()
|
| 42 |
+
if (/^#[0-9a-fA-F]{3,8}$/.test(text)) return text
|
| 43 |
+
if (/^rgba?\(\s*[\d.\s%,]+\)$/.test(text)) return text
|
| 44 |
+
return fallback
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function buildGroupedMarketMarkerHtml(item, size = 22) {
|
| 48 |
+
const count = Math.max(2, Number(item?.count) || 2)
|
| 49 |
+
const color = normalizeCssColor(item?.color || item?.fill_color || '#607d8b')
|
| 50 |
+
const markerSize = Math.max(14, Math.min(38, Number(size) || 22))
|
| 51 |
+
const fontSize = Math.max(9, Math.min(12, markerSize * 0.38))
|
| 52 |
+
const borderSize = markerSize >= 24 ? 3 : 2
|
| 53 |
+
const haloSize = markerSize >= 24 ? 4 : 2
|
| 54 |
+
return (
|
| 55 |
+
`<div class="mesa-market-group-marker" style="--mesa-group-color:${color};--mesa-group-size:${markerSize}px;--mesa-group-font:${fontSize}px;--mesa-group-border:${borderSize}px;--mesa-group-halo:${haloSize}px;">`
|
| 56 |
+
+ `<span>${escapeHtml(count > 99 ? '99+' : count)}</span>`
|
| 57 |
+
+ '</div>'
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function buildGroupedMarketIcon(item, size = 22) {
|
| 62 |
+
const markerSize = Math.max(14, Math.min(38, Number(size) || 22))
|
| 63 |
+
return L.divIcon({
|
| 64 |
+
html: buildGroupedMarketMarkerHtml(item, markerSize),
|
| 65 |
+
iconSize: [markerSize, markerSize],
|
| 66 |
+
iconAnchor: [markerSize / 2, markerSize / 2],
|
| 67 |
+
className: 'mesa-market-group-icon',
|
| 68 |
+
})
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function hasValidRowId(value) {
|
| 72 |
+
if (value === null || value === undefined || value === '') return false
|
| 73 |
+
return Number.isInteger(Number(value)) && Number(value) >= 0
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
function parseFiniteCoordinate(value, min, max) {
|
| 77 |
if (value === null || value === undefined) return null
|
| 78 |
const text = String(value).trim().replace(',', '.')
|
|
|
|
| 372 |
show: true,
|
| 373 |
points: payload.market_points.map((item) => ({
|
| 374 |
...item,
|
| 375 |
+
popup_request: !item?.grouped && hasValidRowId(item?.row_id)
|
| 376 |
? { kind: 'visualizacao_row', row_id: Number(item.row_id) }
|
| 377 |
: null,
|
| 378 |
})),
|
|
|
|
| 721 |
|
| 722 |
const bairrosPane = exportMap.createPane('mesa-bairros-pane')
|
| 723 |
const marketPane = exportMap.createPane('mesa-market-pane')
|
| 724 |
+
const marketGroupPane = exportMap.createPane('mesa-market-group-pane')
|
| 725 |
const trabalhosPane = exportMap.createPane('mesa-trabalhos-pane')
|
| 726 |
const indicesPane = exportMap.createPane('mesa-indices-pane')
|
| 727 |
bairrosPane.style.zIndex = '410'
|
| 728 |
marketPane.style.zIndex = '420'
|
| 729 |
+
marketGroupPane.style.zIndex = '425'
|
| 730 |
trabalhosPane.style.zIndex = '430'
|
| 731 |
indicesPane.style.zIndex = '440'
|
| 732 |
|
|
|
|
| 783 |
items.forEach((item) => {
|
| 784 |
const latlng = parseLatLonPair(item?.lat, item?.lon)
|
| 785 |
if (!latlng) return
|
| 786 |
+
if (item?.grouped) {
|
| 787 |
+
const marker = L.marker(latlng, {
|
| 788 |
+
icon: buildGroupedMarketIcon(item, 16),
|
| 789 |
+
pane: String(item?.pane || 'mesa-market-group-pane'),
|
| 790 |
+
interactive: false,
|
| 791 |
+
keyboard: false,
|
| 792 |
+
bubblingMouseEvents: false,
|
| 793 |
+
})
|
| 794 |
+
marker.options.mesaBaseRadius = Number(item?.base_radius) || 4
|
| 795 |
+
marker.options.mesaGroupedMarketMarker = true
|
| 796 |
+
marker.options.mesaGroupedMarketItem = item
|
| 797 |
+
layerGroup.addLayer(marker)
|
| 798 |
+
return
|
| 799 |
+
}
|
| 800 |
const marker = L.circleMarker(latlng, {
|
| 801 |
renderer: canvasRenderer,
|
| 802 |
pane: String(item?.pane || 'mesa-market-pane'),
|
|
|
|
| 1023 |
const mapRef = useRef(null)
|
| 1024 |
const payloadRef = useRef(payload)
|
| 1025 |
const popupCacheRef = useRef(new Map())
|
| 1026 |
+
const popupLoaderRef = useRef(null)
|
| 1027 |
+
const groupSelectionMarkerRef = useRef(null)
|
| 1028 |
const [runtimeError, setRuntimeError] = useState('')
|
| 1029 |
+
const [groupSelection, setGroupSelection] = useState(null)
|
| 1030 |
|
| 1031 |
payloadRef.current = payload
|
| 1032 |
|
|
|
|
| 1071 |
},
|
| 1072 |
}), [])
|
| 1073 |
|
| 1074 |
+
const closeGroupSelection = useCallback(() => {
|
| 1075 |
+
setGroupSelection(null)
|
| 1076 |
+
groupSelectionMarkerRef.current = null
|
| 1077 |
+
}, [])
|
| 1078 |
+
|
| 1079 |
+
const selectGroupedMarketItem = useCallback((item) => {
|
| 1080 |
+
const popupRequest = item?.popup_request
|
| 1081 |
+
const marker = groupSelectionMarkerRef.current
|
| 1082 |
+
const loader = popupLoaderRef.current
|
| 1083 |
+
if (!popupRequest || !marker || !loader) return
|
| 1084 |
+
setGroupSelection(null)
|
| 1085 |
+
void loader(popupRequest, marker)
|
| 1086 |
+
}, [])
|
| 1087 |
+
|
| 1088 |
useEffect(() => {
|
| 1089 |
if (!hostRef.current || !payload) return undefined
|
| 1090 |
let disposed = false
|
| 1091 |
setRuntimeError('')
|
| 1092 |
+
setGroupSelection(null)
|
| 1093 |
+
groupSelectionMarkerRef.current = null
|
| 1094 |
hostRef.current.innerHTML = ''
|
| 1095 |
|
| 1096 |
const map = L.map(hostRef.current, {
|
|
|
|
| 1101 |
let restoreMapInteractions = null
|
| 1102 |
const bairrosPane = map.createPane('mesa-bairros-pane')
|
| 1103 |
const marketPane = map.createPane('mesa-market-pane')
|
| 1104 |
+
const marketGroupPane = map.createPane('mesa-market-group-pane')
|
| 1105 |
const trabalhosPane = map.createPane('mesa-trabalhos-pane')
|
| 1106 |
const indicesPane = map.createPane('mesa-indices-pane')
|
| 1107 |
bairrosPane.style.zIndex = '410'
|
| 1108 |
marketPane.style.zIndex = '420'
|
| 1109 |
+
marketGroupPane.style.zIndex = '425'
|
| 1110 |
trabalhosPane.style.zIndex = '430'
|
| 1111 |
indicesPane.style.zIndex = '440'
|
| 1112 |
|
|
|
|
| 1119 |
|
| 1120 |
;(payload.tile_layers || []).forEach((layerDef, index) => {
|
| 1121 |
const tileLayer = L.tileLayer(String(layerDef?.url || ''), {
|
| 1122 |
+
attribution: String(layerDef?.attribution || '© OpenStreetMap contributors'),
|
|
|
|
|
|
|
| 1123 |
crossOrigin: 'anonymous',
|
| 1124 |
detectRetina: true,
|
| 1125 |
})
|
|
|
|
| 1172 |
responsivePointContainers.forEach((container) => {
|
| 1173 |
if (!container || typeof container.eachLayer !== 'function') return
|
| 1174 |
container.eachLayer((layer) => {
|
|
|
|
| 1175 |
const base = Number(layer.options?.mesaBaseRadius || layer.options?.radius || 4)
|
| 1176 |
const dynamicMin = Math.max(minRadius, base * floorScale)
|
| 1177 |
const dynamicMax = Math.max(dynamicMin + 0.1, Math.min(maxRadius, base * 8.0))
|
| 1178 |
+
const radius = clamp(base * expFactor, dynamicMin, dynamicMax)
|
| 1179 |
+
if (layer.options?.mesaGroupedMarketMarker && typeof layer.setIcon === 'function') {
|
| 1180 |
+
const iconSize = Math.max(16, Math.min(36, Math.round(radius * 2.35)))
|
| 1181 |
+
layer.setIcon(buildGroupedMarketIcon(layer.options.mesaGroupedMarketItem || {}, iconSize))
|
| 1182 |
+
return
|
| 1183 |
+
}
|
| 1184 |
+
if (typeof layer.setRadius !== 'function') return
|
| 1185 |
+
layer.setRadius(radius)
|
| 1186 |
})
|
| 1187 |
})
|
| 1188 |
}
|
| 1189 |
|
| 1190 |
+
async function carregarPopupRegistro(popupRequest, layer) {
|
| 1191 |
+
const kind = String(popupRequest?.kind || '').trim()
|
| 1192 |
+
const rowId = Number(popupRequest?.row_id)
|
| 1193 |
+
const source = String(popupRequest?.source || '').trim()
|
| 1194 |
+
const endpoint = kind === 'elaboracao_row'
|
| 1195 |
+
? '/api/elaboracao/map/popup'
|
| 1196 |
+
: '/api/visualizacao/map/popup'
|
| 1197 |
+
const cacheKey = `${kind || 'visualizacao_row'}:${source || 'default'}:${rowId}`
|
| 1198 |
const cached = popupCacheRef.current.get(cacheKey)
|
| 1199 |
if (cached) {
|
| 1200 |
const cachedHtml = typeof cached === 'string' ? cached : String(cached?.html || '')
|
|
|
|
| 1214 |
).openPopup()
|
| 1215 |
|
| 1216 |
try {
|
| 1217 |
+
const body = {
|
| 1218 |
+
session_id: sessionId,
|
| 1219 |
+
row_id: rowId,
|
| 1220 |
+
}
|
| 1221 |
+
if (source) {
|
| 1222 |
+
body.source = source
|
| 1223 |
+
}
|
| 1224 |
+
const response = await fetch(apiUrl(endpoint), {
|
| 1225 |
method: 'POST',
|
| 1226 |
headers: {
|
| 1227 |
'Content-Type': 'application/json',
|
| 1228 |
...(getAuthToken() ? { 'X-Auth-Token': getAuthToken() } : {}),
|
| 1229 |
},
|
| 1230 |
+
body: JSON.stringify(body),
|
|
|
|
|
|
|
|
|
|
| 1231 |
})
|
| 1232 |
const payloadResp = await readJsonSafely(response)
|
| 1233 |
if (!response.ok) {
|
|
|
|
| 1254 |
}
|
| 1255 |
}
|
| 1256 |
|
| 1257 |
+
popupLoaderRef.current = carregarPopupRegistro
|
| 1258 |
+
|
| 1259 |
function bindPointPopup(marker, item) {
|
| 1260 |
const popupHtml = String(item?.popup_html || '').trim()
|
| 1261 |
if (popupHtml) {
|
|
|
|
| 1264 |
}
|
| 1265 |
|
| 1266 |
const popupRequest = item?.popup_request
|
| 1267 |
+
const popupKind = String(popupRequest?.kind || '').trim()
|
| 1268 |
+
if (
|
| 1269 |
+
['visualizacao_row', 'elaboracao_row'].includes(popupKind)
|
| 1270 |
+
&& Number.isFinite(Number(popupRequest?.row_id))
|
| 1271 |
+
&& sessionId
|
| 1272 |
+
) {
|
| 1273 |
marker.on('click', () => {
|
| 1274 |
+
void carregarPopupRegistro(popupRequest, marker)
|
| 1275 |
})
|
| 1276 |
}
|
| 1277 |
}
|
|
|
|
| 1472 |
items.forEach((item) => {
|
| 1473 |
const latlng = parseLatLonPair(item?.lat, item?.lon)
|
| 1474 |
if (!latlng) return
|
| 1475 |
+
if (item?.grouped) {
|
| 1476 |
+
const marker = L.marker(latlng, {
|
| 1477 |
+
icon: buildGroupedMarketIcon(item, 16),
|
| 1478 |
+
pane: String(item?.pane || 'mesa-market-group-pane'),
|
| 1479 |
+
interactive: item?.interactive !== false,
|
| 1480 |
+
keyboard: item?.keyboard !== false,
|
| 1481 |
+
bubblingMouseEvents: false,
|
| 1482 |
+
})
|
| 1483 |
+
marker.options.mesaBaseRadius = Number(item?.base_radius) || 4
|
| 1484 |
+
marker.options.mesaGroupedMarketMarker = true
|
| 1485 |
+
marker.options.mesaGroupedMarketItem = item
|
| 1486 |
+
const tooltipHtml = String(item?.tooltip_html || '').trim()
|
| 1487 |
+
if (tooltipHtml) {
|
| 1488 |
+
marker.bindTooltip(tooltipHtml, { sticky: item?.tooltip_sticky !== false })
|
| 1489 |
+
}
|
| 1490 |
+
marker.on('click', () => {
|
| 1491 |
+
map.closePopup()
|
| 1492 |
+
marker.unbindPopup()
|
| 1493 |
+
const groupItems = Array.isArray(item?.group_items) ? item.group_items : []
|
| 1494 |
+
if (!groupItems.length) return
|
| 1495 |
+
groupSelectionMarkerRef.current = marker
|
| 1496 |
+
setGroupSelection({
|
| 1497 |
+
title: String(item?.group_title || `${groupItems.length} dados neste local`),
|
| 1498 |
+
count: Number(item?.count) || groupItems.length,
|
| 1499 |
+
items: groupItems,
|
| 1500 |
+
})
|
| 1501 |
+
})
|
| 1502 |
+
layerGroup.addLayer(marker)
|
| 1503 |
+
return
|
| 1504 |
+
}
|
| 1505 |
const marker = L.circleMarker(latlng, {
|
| 1506 |
renderer: canvasRenderer,
|
| 1507 |
pane: String(item?.pane || 'mesa-market-pane'),
|
|
|
|
| 2061 |
}
|
| 2062 |
disposed = true
|
| 2063 |
restoreMapInteractions?.()
|
| 2064 |
+
if (popupLoaderRef.current === carregarPopupRegistro) {
|
| 2065 |
+
popupLoaderRef.current = null
|
| 2066 |
+
}
|
| 2067 |
+
groupSelectionMarkerRef.current = null
|
| 2068 |
if (mapRef.current === map) {
|
| 2069 |
mapRef.current = null
|
| 2070 |
}
|
|
|
|
| 2076 |
<div className="map-frame leaflet-map-host">
|
| 2077 |
<div ref={hostRef} className="leaflet-map-canvas" />
|
| 2078 |
{runtimeError ? <div className="leaflet-map-runtime-error">{runtimeError}</div> : null}
|
| 2079 |
+
{groupSelection ? (
|
| 2080 |
+
<div className="leaflet-market-group-modal-backdrop" onClick={closeGroupSelection}>
|
| 2081 |
+
<div
|
| 2082 |
+
className="leaflet-market-group-modal"
|
| 2083 |
+
role="dialog"
|
| 2084 |
+
aria-modal="true"
|
| 2085 |
+
aria-label="Selecionar dado de mercado"
|
| 2086 |
+
onClick={(event) => event.stopPropagation()}
|
| 2087 |
+
>
|
| 2088 |
+
<div className="leaflet-market-group-modal-head">
|
| 2089 |
+
<div>
|
| 2090 |
+
<strong>{groupSelection.title}</strong>
|
| 2091 |
+
<span>{Number(groupSelection.count) || groupSelection.items?.length || 0} registros sobrepostos</span>
|
| 2092 |
+
</div>
|
| 2093 |
+
<button type="button" onClick={closeGroupSelection} aria-label="Fechar">×</button>
|
| 2094 |
+
</div>
|
| 2095 |
+
<div className="leaflet-market-group-list">
|
| 2096 |
+
{(groupSelection.items || []).map((item, index) => {
|
| 2097 |
+
const disabled = !item?.popup_request
|
| 2098 |
+
return (
|
| 2099 |
+
<button
|
| 2100 |
+
key={`${item?.indice ?? index}-${index}`}
|
| 2101 |
+
type="button"
|
| 2102 |
+
className="leaflet-market-group-option"
|
| 2103 |
+
onClick={() => selectGroupedMarketItem(item)}
|
| 2104 |
+
disabled={disabled}
|
| 2105 |
+
>
|
| 2106 |
+
<span>{item?.label || `Índice ${item?.indice ?? index + 1}`}</span>
|
| 2107 |
+
{item?.value ? (
|
| 2108 |
+
<small>
|
| 2109 |
+
{item?.value_label ? `${item.value_label}: ` : ''}
|
| 2110 |
+
{item.value}
|
| 2111 |
+
</small>
|
| 2112 |
+
) : null}
|
| 2113 |
+
</button>
|
| 2114 |
+
)
|
| 2115 |
+
})}
|
| 2116 |
+
</div>
|
| 2117 |
+
</div>
|
| 2118 |
+
</div>
|
| 2119 |
+
) : null}
|
| 2120 |
</div>
|
| 2121 |
)
|
| 2122 |
})
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -1015,6 +1015,7 @@ export default function PesquisaTab({
|
|
| 1015 |
const [mapaHtmls, setMapaHtmls] = useState({ pontos: '', cobertura: '' })
|
| 1016 |
const [mapaPayloads, setMapaPayloads] = useState({ pontos: null, cobertura: null })
|
| 1017 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('pontos')
|
|
|
|
| 1018 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState('selecionados_e_outras_versoes')
|
| 1019 |
const [mapaTrabalhosTecnicosProximidadeModo, setMapaTrabalhosTecnicosProximidadeModo] = useState('sem_proximidade')
|
| 1020 |
const [mapaTrabalhosTecnicosRaio, setMapaTrabalhosTecnicosRaio] = useState(1000)
|
|
@@ -1039,9 +1040,13 @@ export default function PesquisaTab({
|
|
| 1039 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1040 |
const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
|
| 1041 |
const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
|
|
|
|
| 1042 |
const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null)
|
|
|
|
| 1043 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
|
|
|
| 1044 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
|
|
|
| 1045 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 1046 |
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 1047 |
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
|
@@ -1136,6 +1141,7 @@ export default function PesquisaTab({
|
|
| 1136 |
setMapaStatus('')
|
| 1137 |
setMapaError('')
|
| 1138 |
setMapaModoExibicao('pontos')
|
|
|
|
| 1139 |
mapaTrabalhosTecnicosConfigRef.current = ''
|
| 1140 |
}
|
| 1141 |
|
|
@@ -1166,6 +1172,7 @@ export default function PesquisaTab({
|
|
| 1166 |
async function carregarMapaPesquisa(ids, overrides = {}) {
|
| 1167 |
const idsValidos = (ids || []).map((item) => String(item || '').trim()).filter(Boolean)
|
| 1168 |
const modoExibicaoSolicitado = String(overrides.modoExibicao || mapaModoExibicao || 'pontos')
|
|
|
|
| 1169 |
const trabalhosTecnicosConfig = getMapaTrabalhosTecnicosRequestConfig(overrides)
|
| 1170 |
|
| 1171 |
if (!idsValidos.length) {
|
|
@@ -1188,6 +1195,7 @@ export default function PesquisaTab({
|
|
| 1188 |
trabalhosTecnicosConfig.modelosModo,
|
| 1189 |
trabalhosTecnicosConfig.proximidadeModo,
|
| 1190 |
trabalhosTecnicosConfig.raio,
|
|
|
|
| 1191 |
)
|
| 1192 |
const mapaHtmlSolicitado = String(
|
| 1193 |
response.mapa_html
|
|
@@ -1491,9 +1499,13 @@ export default function PesquisaTab({
|
|
| 1491 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1492 |
setModeloAbertoTrabalhosTecnicos([])
|
| 1493 |
setModeloAbertoPlotObsCalc(null)
|
|
|
|
| 1494 |
setModeloAbertoPlotResiduos(null)
|
|
|
|
| 1495 |
setModeloAbertoPlotHistograma(null)
|
|
|
|
| 1496 |
setModeloAbertoPlotCook(null)
|
|
|
|
| 1497 |
setModeloAbertoPlotCorr(null)
|
| 1498 |
setModeloAbertoLoadedTabs({})
|
| 1499 |
setModeloAbertoLoadingTabs({})
|
|
@@ -1529,9 +1541,13 @@ export default function PesquisaTab({
|
|
| 1529 |
}
|
| 1530 |
if (key === 'graficos') {
|
| 1531 |
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
|
|
|
| 1532 |
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
|
|
|
| 1533 |
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
|
|
|
| 1534 |
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
|
|
|
| 1535 |
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 1536 |
return
|
| 1537 |
}
|
|
@@ -1723,6 +1739,18 @@ export default function PesquisaTab({
|
|
| 1723 |
void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
|
| 1724 |
}
|
| 1725 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1726 |
async function onAdminConfigSalva() {
|
| 1727 |
if (pesquisaInicializada) {
|
| 1728 |
await buscarModelos(filters, avaliandosGeolocalizados)
|
|
@@ -1942,10 +1970,31 @@ export default function PesquisaTab({
|
|
| 1942 |
modeloAbertoLoadedTabs.graficos ? (
|
| 1943 |
<>
|
| 1944 |
<div className="plot-grid-2-fixed">
|
| 1945 |
-
<PlotFigure
|
| 1946 |
-
|
| 1947 |
-
|
| 1948 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1949 |
</div>
|
| 1950 |
<div className="plot-full-width">
|
| 1951 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
|
@@ -2454,6 +2503,21 @@ export default function PesquisaTab({
|
|
| 2454 |
</select>
|
| 2455 |
</label>
|
| 2456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2457 |
<label className="pesquisa-field pesquisa-mapa-trabalhos-field">
|
| 2458 |
Exibição dos trabalhos técnicos
|
| 2459 |
<select
|
|
|
|
| 1015 |
const [mapaHtmls, setMapaHtmls] = useState({ pontos: '', cobertura: '' })
|
| 1016 |
const [mapaPayloads, setMapaPayloads] = useState({ pontos: null, cobertura: null })
|
| 1017 |
const [mapaModoExibicao, setMapaModoExibicao] = useState('pontos')
|
| 1018 |
+
const [mapaAgruparPontosMercado, setMapaAgruparPontosMercado] = useState(false)
|
| 1019 |
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState('selecionados_e_outras_versoes')
|
| 1020 |
const [mapaTrabalhosTecnicosProximidadeModo, setMapaTrabalhosTecnicosProximidadeModo] = useState('sem_proximidade')
|
| 1021 |
const [mapaTrabalhosTecnicosRaio, setMapaTrabalhosTecnicosRaio] = useState(1000)
|
|
|
|
| 1040 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1041 |
const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
|
| 1042 |
const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
|
| 1043 |
+
const [modeloAbertoPlotObsCalcIndices, setModeloAbertoPlotObsCalcIndices] = useState(null)
|
| 1044 |
const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null)
|
| 1045 |
+
const [modeloAbertoPlotResiduosIndices, setModeloAbertoPlotResiduosIndices] = useState(null)
|
| 1046 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 1047 |
+
const [modeloAbertoPlotHistogramaIndices, setModeloAbertoPlotHistogramaIndices] = useState(null)
|
| 1048 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 1049 |
+
const [modeloAbertoPlotCookIndices, setModeloAbertoPlotCookIndices] = useState(null)
|
| 1050 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 1051 |
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 1052 |
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
|
|
|
| 1141 |
setMapaStatus('')
|
| 1142 |
setMapaError('')
|
| 1143 |
setMapaModoExibicao('pontos')
|
| 1144 |
+
setMapaAgruparPontosMercado(false)
|
| 1145 |
mapaTrabalhosTecnicosConfigRef.current = ''
|
| 1146 |
}
|
| 1147 |
|
|
|
|
| 1172 |
async function carregarMapaPesquisa(ids, overrides = {}) {
|
| 1173 |
const idsValidos = (ids || []).map((item) => String(item || '').trim()).filter(Boolean)
|
| 1174 |
const modoExibicaoSolicitado = String(overrides.modoExibicao || mapaModoExibicao || 'pontos')
|
| 1175 |
+
const agruparPontosMercadoSolicitado = Boolean(overrides.agruparPontosMercado ?? mapaAgruparPontosMercado)
|
| 1176 |
const trabalhosTecnicosConfig = getMapaTrabalhosTecnicosRequestConfig(overrides)
|
| 1177 |
|
| 1178 |
if (!idsValidos.length) {
|
|
|
|
| 1195 |
trabalhosTecnicosConfig.modelosModo,
|
| 1196 |
trabalhosTecnicosConfig.proximidadeModo,
|
| 1197 |
trabalhosTecnicosConfig.raio,
|
| 1198 |
+
agruparPontosMercadoSolicitado,
|
| 1199 |
)
|
| 1200 |
const mapaHtmlSolicitado = String(
|
| 1201 |
response.mapa_html
|
|
|
|
| 1499 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 1500 |
setModeloAbertoTrabalhosTecnicos([])
|
| 1501 |
setModeloAbertoPlotObsCalc(null)
|
| 1502 |
+
setModeloAbertoPlotObsCalcIndices(null)
|
| 1503 |
setModeloAbertoPlotResiduos(null)
|
| 1504 |
+
setModeloAbertoPlotResiduosIndices(null)
|
| 1505 |
setModeloAbertoPlotHistograma(null)
|
| 1506 |
+
setModeloAbertoPlotHistogramaIndices(null)
|
| 1507 |
setModeloAbertoPlotCook(null)
|
| 1508 |
+
setModeloAbertoPlotCookIndices(null)
|
| 1509 |
setModeloAbertoPlotCorr(null)
|
| 1510 |
setModeloAbertoLoadedTabs({})
|
| 1511 |
setModeloAbertoLoadingTabs({})
|
|
|
|
| 1541 |
}
|
| 1542 |
if (key === 'graficos') {
|
| 1543 |
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
| 1544 |
+
setModeloAbertoPlotObsCalcIndices(resp?.grafico_obs_calc_com_indices || null)
|
| 1545 |
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
| 1546 |
+
setModeloAbertoPlotResiduosIndices(resp?.grafico_residuos_com_indices || null)
|
| 1547 |
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
| 1548 |
+
setModeloAbertoPlotHistogramaIndices(resp?.grafico_histograma_com_indices || null)
|
| 1549 |
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
| 1550 |
+
setModeloAbertoPlotCookIndices(resp?.grafico_cook_com_indices || null)
|
| 1551 |
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 1552 |
return
|
| 1553 |
}
|
|
|
|
| 1739 |
void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
|
| 1740 |
}
|
| 1741 |
|
| 1742 |
+
function onMapaAgrupamentoPontosChange(event) {
|
| 1743 |
+
const nextAgrupar = String(event?.target?.value || 'individual') === 'agrupado'
|
| 1744 |
+
setMapaAgruparPontosMercado(nextAgrupar)
|
| 1745 |
+
setMapaHtmls((prev) => ({ ...prev, pontos: '' }))
|
| 1746 |
+
setMapaPayloads((prev) => ({ ...prev, pontos: null }))
|
| 1747 |
+
if (!mapaFoiGerado || mapaLoading || !selectedIds.length || mapaModoExibicao !== 'pontos') return
|
| 1748 |
+
void carregarMapaPesquisa(selectedIds, {
|
| 1749 |
+
modoExibicao: 'pontos',
|
| 1750 |
+
agruparPontosMercado: nextAgrupar,
|
| 1751 |
+
})
|
| 1752 |
+
}
|
| 1753 |
+
|
| 1754 |
async function onAdminConfigSalva() {
|
| 1755 |
if (pesquisaInicializada) {
|
| 1756 |
await buscarModelos(filters, avaliandosGeolocalizados)
|
|
|
|
| 1970 |
modeloAbertoLoadedTabs.graficos ? (
|
| 1971 |
<>
|
| 1972 |
<div className="plot-grid-2-fixed">
|
| 1973 |
+
<PlotFigure
|
| 1974 |
+
figure={modeloAbertoPlotObsCalc}
|
| 1975 |
+
indexedFigure={modeloAbertoPlotObsCalcIndices}
|
| 1976 |
+
title="Obs x Calc"
|
| 1977 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotObsCalcIndices)}
|
| 1978 |
+
/>
|
| 1979 |
+
<PlotFigure
|
| 1980 |
+
figure={modeloAbertoPlotResiduos}
|
| 1981 |
+
indexedFigure={modeloAbertoPlotResiduosIndices}
|
| 1982 |
+
title="Resíduos"
|
| 1983 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotResiduosIndices)}
|
| 1984 |
+
/>
|
| 1985 |
+
<PlotFigure
|
| 1986 |
+
figure={modeloAbertoPlotHistograma}
|
| 1987 |
+
indexedFigure={modeloAbertoPlotHistogramaIndices}
|
| 1988 |
+
title="Histograma"
|
| 1989 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotHistogramaIndices)}
|
| 1990 |
+
/>
|
| 1991 |
+
<PlotFigure
|
| 1992 |
+
figure={modeloAbertoPlotCook}
|
| 1993 |
+
indexedFigure={modeloAbertoPlotCookIndices}
|
| 1994 |
+
title="Cook"
|
| 1995 |
+
forceHideLegend
|
| 1996 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotCookIndices)}
|
| 1997 |
+
/>
|
| 1998 |
</div>
|
| 1999 |
<div className="plot-full-width">
|
| 2000 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
|
|
|
| 2503 |
</select>
|
| 2504 |
</label>
|
| 2505 |
|
| 2506 |
+
{mapaModoExibicao === 'pontos' ? (
|
| 2507 |
+
<label className="pesquisa-field pesquisa-mapa-agrupamento-field">
|
| 2508 |
+
Pontos sobrepostos
|
| 2509 |
+
<select
|
| 2510 |
+
{...buildSelectAutofillProps('mapaAgrupamentoPontos')}
|
| 2511 |
+
value={mapaAgruparPontosMercado ? 'agrupado' : 'individual'}
|
| 2512 |
+
onChange={onMapaAgrupamentoPontosChange}
|
| 2513 |
+
disabled={mapaLoading || !selectedIds.length}
|
| 2514 |
+
>
|
| 2515 |
+
<option value="individual">Mostrar individualmente</option>
|
| 2516 |
+
<option value="agrupado">Agrupar por coordenada</option>
|
| 2517 |
+
</select>
|
| 2518 |
+
</label>
|
| 2519 |
+
) : null}
|
| 2520 |
+
|
| 2521 |
<label className="pesquisa-field pesquisa-mapa-trabalhos-field">
|
| 2522 |
Exibição dos trabalhos técnicos
|
| 2523 |
<select
|
frontend/src/components/RepositorioTab.jsx
CHANGED
|
@@ -180,9 +180,13 @@ export default function RepositorioTab({
|
|
| 180 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 181 |
const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
|
| 182 |
const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
|
|
|
|
| 183 |
const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null)
|
|
|
|
| 184 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
|
|
|
| 185 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
|
|
|
| 186 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 187 |
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 188 |
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
|
@@ -356,9 +360,13 @@ export default function RepositorioTab({
|
|
| 356 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 357 |
setModeloAbertoTrabalhosTecnicos([])
|
| 358 |
setModeloAbertoPlotObsCalc(null)
|
|
|
|
| 359 |
setModeloAbertoPlotResiduos(null)
|
|
|
|
| 360 |
setModeloAbertoPlotHistograma(null)
|
|
|
|
| 361 |
setModeloAbertoPlotCook(null)
|
|
|
|
| 362 |
setModeloAbertoPlotCorr(null)
|
| 363 |
setModeloAbertoLoadedTabs({})
|
| 364 |
setModeloAbertoLoadingTabs({})
|
|
@@ -394,9 +402,13 @@ export default function RepositorioTab({
|
|
| 394 |
}
|
| 395 |
if (secaoNormalizada === 'graficos') {
|
| 396 |
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
|
|
|
| 397 |
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
|
|
|
| 398 |
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
|
|
|
| 399 |
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
|
|
|
| 400 |
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 401 |
return
|
| 402 |
}
|
|
@@ -755,10 +767,31 @@ export default function RepositorioTab({
|
|
| 755 |
activeTabLoaded ? (
|
| 756 |
<>
|
| 757 |
<div className="plot-grid-2-fixed">
|
| 758 |
-
<PlotFigure
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
</div>
|
| 763 |
<div className="plot-full-width">
|
| 764 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
|
|
|
| 180 |
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 181 |
const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
|
| 182 |
const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
|
| 183 |
+
const [modeloAbertoPlotObsCalcIndices, setModeloAbertoPlotObsCalcIndices] = useState(null)
|
| 184 |
const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null)
|
| 185 |
+
const [modeloAbertoPlotResiduosIndices, setModeloAbertoPlotResiduosIndices] = useState(null)
|
| 186 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 187 |
+
const [modeloAbertoPlotHistogramaIndices, setModeloAbertoPlotHistogramaIndices] = useState(null)
|
| 188 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 189 |
+
const [modeloAbertoPlotCookIndices, setModeloAbertoPlotCookIndices] = useState(null)
|
| 190 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 191 |
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 192 |
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
|
|
|
| 360 |
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 361 |
setModeloAbertoTrabalhosTecnicos([])
|
| 362 |
setModeloAbertoPlotObsCalc(null)
|
| 363 |
+
setModeloAbertoPlotObsCalcIndices(null)
|
| 364 |
setModeloAbertoPlotResiduos(null)
|
| 365 |
+
setModeloAbertoPlotResiduosIndices(null)
|
| 366 |
setModeloAbertoPlotHistograma(null)
|
| 367 |
+
setModeloAbertoPlotHistogramaIndices(null)
|
| 368 |
setModeloAbertoPlotCook(null)
|
| 369 |
+
setModeloAbertoPlotCookIndices(null)
|
| 370 |
setModeloAbertoPlotCorr(null)
|
| 371 |
setModeloAbertoLoadedTabs({})
|
| 372 |
setModeloAbertoLoadingTabs({})
|
|
|
|
| 402 |
}
|
| 403 |
if (secaoNormalizada === 'graficos') {
|
| 404 |
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
| 405 |
+
setModeloAbertoPlotObsCalcIndices(resp?.grafico_obs_calc_com_indices || null)
|
| 406 |
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
| 407 |
+
setModeloAbertoPlotResiduosIndices(resp?.grafico_residuos_com_indices || null)
|
| 408 |
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
| 409 |
+
setModeloAbertoPlotHistogramaIndices(resp?.grafico_histograma_com_indices || null)
|
| 410 |
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
| 411 |
+
setModeloAbertoPlotCookIndices(resp?.grafico_cook_com_indices || null)
|
| 412 |
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 413 |
return
|
| 414 |
}
|
|
|
|
| 767 |
activeTabLoaded ? (
|
| 768 |
<>
|
| 769 |
<div className="plot-grid-2-fixed">
|
| 770 |
+
<PlotFigure
|
| 771 |
+
figure={modeloAbertoPlotObsCalc}
|
| 772 |
+
indexedFigure={modeloAbertoPlotObsCalcIndices}
|
| 773 |
+
title="Obs x Calc"
|
| 774 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotObsCalcIndices)}
|
| 775 |
+
/>
|
| 776 |
+
<PlotFigure
|
| 777 |
+
figure={modeloAbertoPlotResiduos}
|
| 778 |
+
indexedFigure={modeloAbertoPlotResiduosIndices}
|
| 779 |
+
title="Resíduos"
|
| 780 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotResiduosIndices)}
|
| 781 |
+
/>
|
| 782 |
+
<PlotFigure
|
| 783 |
+
figure={modeloAbertoPlotHistograma}
|
| 784 |
+
indexedFigure={modeloAbertoPlotHistogramaIndices}
|
| 785 |
+
title="Histograma"
|
| 786 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotHistogramaIndices)}
|
| 787 |
+
/>
|
| 788 |
+
<PlotFigure
|
| 789 |
+
figure={modeloAbertoPlotCook}
|
| 790 |
+
indexedFigure={modeloAbertoPlotCookIndices}
|
| 791 |
+
title="Cook"
|
| 792 |
+
forceHideLegend
|
| 793 |
+
showPointIndexToggle={Boolean(modeloAbertoPlotCookIndices)}
|
| 794 |
+
/>
|
| 795 |
</div>
|
| 796 |
<div className="plot-full-width">
|
| 797 |
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
frontend/src/styles.css
CHANGED
|
@@ -191,7 +191,7 @@ textarea {
|
|
| 191 |
.tab-pill {
|
| 192 |
text-align: center;
|
| 193 |
border: 1px solid #d2deea;
|
| 194 |
-
border-radius:
|
| 195 |
background: linear-gradient(180deg, #f7fafd 0%, #edf3f8 100%);
|
| 196 |
padding: 8px 12px;
|
| 197 |
color: #32475d;
|
|
@@ -3125,6 +3125,12 @@ button.pesquisa-coluna-remove:hover {
|
|
| 3125 |
margin: 0;
|
| 3126 |
}
|
| 3127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3128 |
.pesquisa-mapa-trabalhos-field {
|
| 3129 |
min-width: 0;
|
| 3130 |
max-width: none;
|
|
@@ -4261,12 +4267,17 @@ button.pesquisa-coluna-remove:hover {
|
|
| 4261 |
display: inline-flex;
|
| 4262 |
align-items: center;
|
| 4263 |
justify-content: center;
|
| 4264 |
-
cursor:
|
| 4265 |
font-size: 0.85em;
|
| 4266 |
line-height: 1;
|
| 4267 |
opacity: 0.7;
|
| 4268 |
}
|
| 4269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4270 |
.avaliacao-popup-overlay {
|
| 4271 |
position: fixed;
|
| 4272 |
z-index: 3600;
|
|
@@ -4287,6 +4298,18 @@ button.pesquisa-coluna-remove:hover {
|
|
| 4287 |
pointer-events: none;
|
| 4288 |
}
|
| 4289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4290 |
.avaliacao-knn-modal {
|
| 4291 |
width: min(1180px, 100%);
|
| 4292 |
}
|
|
@@ -5595,6 +5618,11 @@ button.import-preview-clear-btn {
|
|
| 5595 |
text-align: right;
|
| 5596 |
}
|
| 5597 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5598 |
.section14-table-head {
|
| 5599 |
display: flex;
|
| 5600 |
align-items: center;
|
|
@@ -5645,6 +5673,136 @@ button.import-preview-clear-btn {
|
|
| 5645 |
z-index: 600;
|
| 5646 |
}
|
| 5647 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5648 |
.mesa-leaflet-legend {
|
| 5649 |
min-width: 168px;
|
| 5650 |
border-radius: 12px;
|
|
@@ -6052,7 +6210,7 @@ button.import-preview-clear-btn {
|
|
| 6052 |
}
|
| 6053 |
|
| 6054 |
.table-wrapper tr:hover td {
|
| 6055 |
-
background: #
|
| 6056 |
}
|
| 6057 |
|
| 6058 |
.table-wrapper tr.table-row-highlight td {
|
|
@@ -6063,6 +6221,16 @@ button.import-preview-clear-btn {
|
|
| 6063 |
background: #ffe882;
|
| 6064 |
}
|
| 6065 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6066 |
.table-cell-clamp,
|
| 6067 |
.avaliacao-knn-table-cell {
|
| 6068 |
display: -webkit-box;
|
|
|
|
| 191 |
.tab-pill {
|
| 192 |
text-align: center;
|
| 193 |
border: 1px solid #d2deea;
|
| 194 |
+
border-radius: 8px;
|
| 195 |
background: linear-gradient(180deg, #f7fafd 0%, #edf3f8 100%);
|
| 196 |
padding: 8px 12px;
|
| 197 |
color: #32475d;
|
|
|
|
| 3125 |
margin: 0;
|
| 3126 |
}
|
| 3127 |
|
| 3128 |
+
.pesquisa-mapa-agrupamento-field {
|
| 3129 |
+
min-width: 0;
|
| 3130 |
+
max-width: none;
|
| 3131 |
+
margin: 0;
|
| 3132 |
+
}
|
| 3133 |
+
|
| 3134 |
.pesquisa-mapa-trabalhos-field {
|
| 3135 |
min-width: 0;
|
| 3136 |
max-width: none;
|
|
|
|
| 4267 |
display: inline-flex;
|
| 4268 |
align-items: center;
|
| 4269 |
justify-content: center;
|
| 4270 |
+
cursor: pointer;
|
| 4271 |
font-size: 0.85em;
|
| 4272 |
line-height: 1;
|
| 4273 |
opacity: 0.7;
|
| 4274 |
}
|
| 4275 |
|
| 4276 |
+
.avaliacao-popup-trigger:hover,
|
| 4277 |
+
.avaliacao-popup-trigger:focus-visible {
|
| 4278 |
+
opacity: 1;
|
| 4279 |
+
}
|
| 4280 |
+
|
| 4281 |
.avaliacao-popup-overlay {
|
| 4282 |
position: fixed;
|
| 4283 |
z-index: 3600;
|
|
|
|
| 4298 |
pointer-events: none;
|
| 4299 |
}
|
| 4300 |
|
| 4301 |
+
.avaliacao-info-modal {
|
| 4302 |
+
width: min(680px, 100%);
|
| 4303 |
+
}
|
| 4304 |
+
|
| 4305 |
+
.avaliacao-info-modal-body {
|
| 4306 |
+
color: #333;
|
| 4307 |
+
font-size: 12px;
|
| 4308 |
+
font-weight: 400;
|
| 4309 |
+
line-height: 1.4;
|
| 4310 |
+
text-align: left;
|
| 4311 |
+
}
|
| 4312 |
+
|
| 4313 |
.avaliacao-knn-modal {
|
| 4314 |
width: min(1180px, 100%);
|
| 4315 |
}
|
|
|
|
| 5618 |
text-align: right;
|
| 5619 |
}
|
| 5620 |
|
| 5621 |
+
.section14-percentual-fora-faixa {
|
| 5622 |
+
color: var(--danger);
|
| 5623 |
+
font-weight: 800;
|
| 5624 |
+
}
|
| 5625 |
+
|
| 5626 |
.section14-table-head {
|
| 5627 |
display: flex;
|
| 5628 |
align-items: center;
|
|
|
|
| 5673 |
z-index: 600;
|
| 5674 |
}
|
| 5675 |
|
| 5676 |
+
.mesa-market-group-icon {
|
| 5677 |
+
background: transparent;
|
| 5678 |
+
border: 0;
|
| 5679 |
+
}
|
| 5680 |
+
|
| 5681 |
+
.mesa-market-group-marker {
|
| 5682 |
+
width: var(--mesa-group-size, 22px);
|
| 5683 |
+
height: var(--mesa-group-size, 22px);
|
| 5684 |
+
display: flex;
|
| 5685 |
+
align-items: center;
|
| 5686 |
+
justify-content: center;
|
| 5687 |
+
border: var(--mesa-group-border, 2px) solid #243746;
|
| 5688 |
+
border-radius: 50%;
|
| 5689 |
+
background: var(--mesa-group-color, #607d8b);
|
| 5690 |
+
color: #fff;
|
| 5691 |
+
box-shadow: 0 0 0 var(--mesa-group-halo, 2px) rgba(36, 55, 70, 0.18), 0 6px 16px rgba(23, 39, 55, 0.26);
|
| 5692 |
+
font-size: var(--mesa-group-font, 10px);
|
| 5693 |
+
font-weight: 800;
|
| 5694 |
+
line-height: 1;
|
| 5695 |
+
}
|
| 5696 |
+
|
| 5697 |
+
.mesa-market-group-marker span {
|
| 5698 |
+
min-width: calc(var(--mesa-group-size, 22px) * 0.48);
|
| 5699 |
+
padding: 1px 2px;
|
| 5700 |
+
border-radius: 999px;
|
| 5701 |
+
background: rgba(20, 33, 45, 0.42);
|
| 5702 |
+
pointer-events: none;
|
| 5703 |
+
text-align: center;
|
| 5704 |
+
}
|
| 5705 |
+
|
| 5706 |
+
.leaflet-market-group-modal-backdrop {
|
| 5707 |
+
position: absolute;
|
| 5708 |
+
inset: 0;
|
| 5709 |
+
z-index: 900;
|
| 5710 |
+
display: flex;
|
| 5711 |
+
align-items: center;
|
| 5712 |
+
justify-content: center;
|
| 5713 |
+
padding: 24px;
|
| 5714 |
+
background: rgba(18, 30, 42, 0.24);
|
| 5715 |
+
}
|
| 5716 |
+
|
| 5717 |
+
.leaflet-market-group-modal {
|
| 5718 |
+
width: min(420px, 100%);
|
| 5719 |
+
max-height: min(460px, calc(100% - 32px));
|
| 5720 |
+
display: flex;
|
| 5721 |
+
flex-direction: column;
|
| 5722 |
+
overflow: hidden;
|
| 5723 |
+
border: 1px solid rgba(186, 201, 214, 0.95);
|
| 5724 |
+
border-radius: 8px;
|
| 5725 |
+
background: #fff;
|
| 5726 |
+
box-shadow: 0 18px 46px rgba(22, 39, 58, 0.26);
|
| 5727 |
+
}
|
| 5728 |
+
|
| 5729 |
+
.leaflet-market-group-modal-head {
|
| 5730 |
+
display: flex;
|
| 5731 |
+
align-items: flex-start;
|
| 5732 |
+
justify-content: space-between;
|
| 5733 |
+
gap: 16px;
|
| 5734 |
+
padding: 14px 16px;
|
| 5735 |
+
border-bottom: 1px solid #e3ebf2;
|
| 5736 |
+
background: #f6f9fc;
|
| 5737 |
+
color: #243746;
|
| 5738 |
+
}
|
| 5739 |
+
|
| 5740 |
+
.leaflet-market-group-modal-head strong,
|
| 5741 |
+
.leaflet-market-group-modal-head span {
|
| 5742 |
+
display: block;
|
| 5743 |
+
}
|
| 5744 |
+
|
| 5745 |
+
.leaflet-market-group-modal-head strong {
|
| 5746 |
+
font-size: 0.92rem;
|
| 5747 |
+
}
|
| 5748 |
+
|
| 5749 |
+
.leaflet-market-group-modal-head span {
|
| 5750 |
+
margin-top: 3px;
|
| 5751 |
+
color: #627487;
|
| 5752 |
+
font-size: 0.78rem;
|
| 5753 |
+
}
|
| 5754 |
+
|
| 5755 |
+
.leaflet-market-group-modal-head button {
|
| 5756 |
+
width: 28px;
|
| 5757 |
+
height: 28px;
|
| 5758 |
+
border: 1px solid #cbd8e4;
|
| 5759 |
+
border-radius: 50%;
|
| 5760 |
+
background: #fff;
|
| 5761 |
+
color: #40576d;
|
| 5762 |
+
cursor: pointer;
|
| 5763 |
+
font-size: 20px;
|
| 5764 |
+
line-height: 1;
|
| 5765 |
+
}
|
| 5766 |
+
|
| 5767 |
+
.leaflet-market-group-list {
|
| 5768 |
+
overflow-y: auto;
|
| 5769 |
+
padding: 8px;
|
| 5770 |
+
}
|
| 5771 |
+
|
| 5772 |
+
.leaflet-market-group-option {
|
| 5773 |
+
width: 100%;
|
| 5774 |
+
display: flex;
|
| 5775 |
+
align-items: center;
|
| 5776 |
+
justify-content: space-between;
|
| 5777 |
+
gap: 12px;
|
| 5778 |
+
padding: 9px 10px;
|
| 5779 |
+
border: 0;
|
| 5780 |
+
border-bottom: 1px solid #edf2f6;
|
| 5781 |
+
background: #fff;
|
| 5782 |
+
color: #243746;
|
| 5783 |
+
cursor: pointer;
|
| 5784 |
+
text-align: left;
|
| 5785 |
+
}
|
| 5786 |
+
|
| 5787 |
+
.leaflet-market-group-option:hover {
|
| 5788 |
+
background: #eef5fb;
|
| 5789 |
+
}
|
| 5790 |
+
|
| 5791 |
+
.leaflet-market-group-option span {
|
| 5792 |
+
font-weight: 700;
|
| 5793 |
+
}
|
| 5794 |
+
|
| 5795 |
+
.leaflet-market-group-option small {
|
| 5796 |
+
color: #627487;
|
| 5797 |
+
font-size: 0.74rem;
|
| 5798 |
+
text-align: right;
|
| 5799 |
+
}
|
| 5800 |
+
|
| 5801 |
+
.leaflet-market-group-option:disabled {
|
| 5802 |
+
cursor: not-allowed;
|
| 5803 |
+
opacity: 0.5;
|
| 5804 |
+
}
|
| 5805 |
+
|
| 5806 |
.mesa-leaflet-legend {
|
| 5807 |
min-width: 168px;
|
| 5808 |
border-radius: 12px;
|
|
|
|
| 6210 |
}
|
| 6211 |
|
| 6212 |
.table-wrapper tr:hover td {
|
| 6213 |
+
background: #eef4fa;
|
| 6214 |
}
|
| 6215 |
|
| 6216 |
.table-wrapper tr.table-row-highlight td {
|
|
|
|
| 6221 |
background: #ffe882;
|
| 6222 |
}
|
| 6223 |
|
| 6224 |
+
.table-wrapper tr.table-row-outlier-excluded td {
|
| 6225 |
+
background: #fde8e6;
|
| 6226 |
+
color: #8a1f17;
|
| 6227 |
+
font-weight: 600;
|
| 6228 |
+
}
|
| 6229 |
+
|
| 6230 |
+
.table-wrapper tr.table-row-outlier-excluded:hover td {
|
| 6231 |
+
background: #fbd3cf;
|
| 6232 |
+
}
|
| 6233 |
+
|
| 6234 |
.table-cell-clamp,
|
| 6235 |
.avaliacao-knn-table-cell {
|
| 6236 |
display: -webkit-box;
|