Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
03a7ca2
1
Parent(s): 4e2aace
correcao de mapas, linkagem da pesquisa e avaliacao
Browse files- backend/app/core/elaboracao/charts.py +76 -48
- backend/app/core/map_layers.py +251 -0
- backend/app/core/visualizacao/app.py +77 -49
- frontend/src/App.jsx +15 -2
- frontend/src/components/AvaliacaoBetaTab.jsx +19 -5
- frontend/src/components/PesquisaTab.jsx +64 -22
- frontend/src/styles.css +39 -60
backend/app/core/elaboracao/charts.py
CHANGED
|
@@ -17,7 +17,12 @@ 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 |
|
| 22 |
# ============================================================
|
| 23 |
# CONSTANTES DE ESTILO
|
|
@@ -634,12 +639,15 @@ def _aplicar_jitter_sobrepostos(
|
|
| 634 |
max_raio_metros = 22.0
|
| 635 |
metros_por_grau_lat = 111_320.0
|
| 636 |
|
|
|
|
|
|
|
|
|
|
| 637 |
for _, idx_labels in grupos.indices.items():
|
| 638 |
-
|
|
|
|
| 639 |
continue
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
base_lon = float(df_plot.at[idx_list[0], lon_plot_col])
|
| 643 |
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 644 |
continue
|
| 645 |
|
|
@@ -648,7 +656,7 @@ def _aplicar_jitter_sobrepostos(
|
|
| 648 |
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 649 |
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 650 |
|
| 651 |
-
for pos,
|
| 652 |
if pos == 0:
|
| 653 |
continue
|
| 654 |
|
|
@@ -665,8 +673,8 @@ def _aplicar_jitter_sobrepostos(
|
|
| 665 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 666 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 667 |
|
| 668 |
-
df_plot.
|
| 669 |
-
df_plot.
|
| 670 |
|
| 671 |
return df_plot
|
| 672 |
|
|
@@ -678,6 +686,14 @@ def _montar_popup_registro_em_colunas(
|
|
| 678 |
max_itens_coluna: int = 8,
|
| 679 |
popup_uid: str | None = None,
|
| 680 |
) -> tuple[str, int]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
itens: list[tuple[str, str]] = []
|
| 682 |
for col in allowed_cols:
|
| 683 |
if col not in row.index:
|
|
@@ -698,67 +714,78 @@ def _montar_popup_registro_em_colunas(
|
|
| 698 |
if not itens:
|
| 699 |
return f"<b>Índice: {escape(str(idx))}</b>", 320
|
| 700 |
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
popup_uid = popup_uid or f"mesa-pop-{abs(hash(str(idx))) % 10_000_000}"
|
|
|
|
|
|
|
|
|
|
| 703 |
|
| 704 |
pages_html = []
|
| 705 |
-
botoes_html = []
|
| 706 |
for pagina_idx, pagina_itens in enumerate(paginas):
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
display = "block" if pagina_idx == 0 else "none"
|
| 715 |
pages_html.append(
|
| 716 |
-
f"<div class='mesa-popup-page' data-page='{pagina_idx}' style='display:{display};'>"
|
| 717 |
-
f"<
|
| 718 |
"</div>"
|
| 719 |
)
|
| 720 |
-
botao_style = (
|
| 721 |
-
"border:1px solid #9fb4c8; background:#eaf1f7; border-radius:6px; "
|
| 722 |
-
"padding:2px 7px; font-size:11px; cursor:pointer; color:#2f4b66;"
|
| 723 |
-
if pagina_idx == 0
|
| 724 |
-
else
|
| 725 |
-
"border:1px solid #ced8e2; background:#fff; border-radius:6px; "
|
| 726 |
-
"padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;"
|
| 727 |
-
)
|
| 728 |
-
onclick = (
|
| 729 |
-
f"var root=document.getElementById('{popup_uid}');"
|
| 730 |
-
"if(!root){return false;}"
|
| 731 |
-
"var pages=root.querySelectorAll('.mesa-popup-page');"
|
| 732 |
-
"for(var i=0;i<pages.length;i++){pages[i].style.display=(i==="
|
| 733 |
-
f"{pagina_idx}"
|
| 734 |
-
")?'block':'none';}"
|
| 735 |
-
"var btns=root.querySelectorAll('[data-page-btn]');"
|
| 736 |
-
"for(var j=0;j<btns.length;j++){btns[j].style.background='#fff';btns[j].style.borderColor='#ced8e2';btns[j].style.color='#4e6479';}"
|
| 737 |
-
"this.style.background='#eaf1f7';this.style.borderColor='#9fb4c8';this.style.color='#2f4b66';"
|
| 738 |
-
"return false;"
|
| 739 |
-
)
|
| 740 |
-
botoes_html.append(
|
| 741 |
-
f"<button type='button' data-page-btn='1' style=\"{botao_style}\" onclick=\"{onclick}\">"
|
| 742 |
-
f"{pagina_idx + 1}</button>"
|
| 743 |
-
)
|
| 744 |
|
| 745 |
controls_html = ""
|
| 746 |
if len(paginas) > 1:
|
| 747 |
controls_html = (
|
| 748 |
-
"<div class='mesa-popup-controls' style='display:flex; gap:
|
| 749 |
-
f"<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
"</div>"
|
| 751 |
)
|
| 752 |
|
| 753 |
popup_html = (
|
| 754 |
-
f"<div id='{popup_uid}'
|
|
|
|
| 755 |
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 756 |
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 757 |
-
f"<div class='mesa-popup-pages'>{''.join(pages_html)}</div>"
|
| 758 |
f"{controls_html}"
|
| 759 |
"</div></div>"
|
| 760 |
)
|
| 761 |
-
return popup_html,
|
| 762 |
|
| 763 |
|
| 764 |
def _normalizar_stops_cor(
|
|
@@ -1344,6 +1371,7 @@ def criar_mapa(
|
|
| 1344 |
secondary_area_unit='hectares'
|
| 1345 |
).add_to(m)
|
| 1346 |
add_zoom_responsive_circle_markers(m)
|
|
|
|
| 1347 |
|
| 1348 |
# Ajusta bounds robustos para evitar "salto" para longe por pontos geocodificados distantes.
|
| 1349 |
df_bounds = df_mapa
|
|
|
|
| 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,
|
| 23 |
+
add_popup_pagination_handlers,
|
| 24 |
+
add_zoom_responsive_circle_markers,
|
| 25 |
+
)
|
| 26 |
|
| 27 |
# ============================================================
|
| 28 |
# CONSTANTES DE ESTILO
|
|
|
|
| 639 |
max_raio_metros = 22.0
|
| 640 |
metros_por_grau_lat = 111_320.0
|
| 641 |
|
| 642 |
+
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 643 |
+
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 644 |
+
|
| 645 |
for _, idx_labels in grupos.indices.items():
|
| 646 |
+
posicoes = np.asarray(idx_labels, dtype=int)
|
| 647 |
+
if posicoes.size <= 1:
|
| 648 |
continue
|
| 649 |
+
base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos])
|
| 650 |
+
base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos])
|
|
|
|
| 651 |
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 652 |
continue
|
| 653 |
|
|
|
|
| 656 |
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 657 |
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 658 |
|
| 659 |
+
for pos, pos_idx in enumerate(posicoes):
|
| 660 |
if pos == 0:
|
| 661 |
continue
|
| 662 |
|
|
|
|
| 673 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 674 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 675 |
|
| 676 |
+
df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat
|
| 677 |
+
df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon
|
| 678 |
|
| 679 |
return df_plot
|
| 680 |
|
|
|
|
| 686 |
max_itens_coluna: int = 8,
|
| 687 |
popup_uid: str | None = None,
|
| 688 |
) -> tuple[str, int]:
|
| 689 |
+
def _limitar_texto(valor: str, limite: int) -> str:
|
| 690 |
+
txt = str(valor)
|
| 691 |
+
if limite <= 0 or len(txt) <= limite:
|
| 692 |
+
return txt
|
| 693 |
+
if limite <= 3:
|
| 694 |
+
return txt[:limite]
|
| 695 |
+
return txt[: limite - 3] + "..."
|
| 696 |
+
|
| 697 |
itens: list[tuple[str, str]] = []
|
| 698 |
for col in allowed_cols:
|
| 699 |
if col not in row.index:
|
|
|
|
| 714 |
if not itens:
|
| 715 |
return f"<b>Índice: {escape(str(idx))}</b>", 320
|
| 716 |
|
| 717 |
+
max_colunas_por_pagina = 2
|
| 718 |
+
max_chars_chave = 20
|
| 719 |
+
max_chars_valor = 20
|
| 720 |
+
char_px = 7.2
|
| 721 |
+
gap_cols_px = 12
|
| 722 |
+
popup_padding_horizontal_px = 30
|
| 723 |
+
coluna_largura_px = int(round((max_chars_chave + max_chars_valor) * char_px + 28))
|
| 724 |
+
itens_por_pagina = max_itens_coluna * max_colunas_por_pagina
|
| 725 |
+
paginas = [itens[i:i + itens_por_pagina] for i in range(0, len(itens), itens_por_pagina)]
|
| 726 |
popup_uid = popup_uid or f"mesa-pop-{abs(hash(str(idx))) % 10_000_000}"
|
| 727 |
+
itens_primeira_pagina = len(paginas[0]) if paginas else 0
|
| 728 |
+
colunas_visiveis = max(1, min(max_colunas_por_pagina, int(math.ceil(itens_primeira_pagina / max_itens_coluna)) if itens_primeira_pagina else 1))
|
| 729 |
+
popup_largura_px = popup_padding_horizontal_px + (coluna_largura_px * colunas_visiveis) + (gap_cols_px * (colunas_visiveis - 1))
|
| 730 |
|
| 731 |
pages_html = []
|
|
|
|
| 732 |
for pagina_idx, pagina_itens in enumerate(paginas):
|
| 733 |
+
colunas = [pagina_itens[i:i + max_itens_coluna] for i in range(0, len(pagina_itens), max_itens_coluna)]
|
| 734 |
+
colunas_html = []
|
| 735 |
+
for col_itens in colunas:
|
| 736 |
+
trs_parts: list[str] = []
|
| 737 |
+
for c, v in col_itens:
|
| 738 |
+
c_full = str(c)
|
| 739 |
+
v_full = str(v)
|
| 740 |
+
c_txt = escape(_limitar_texto(c_full, max_chars_chave))
|
| 741 |
+
v_txt = escape(_limitar_texto(v_full, max_chars_valor))
|
| 742 |
+
c_title = escape(c_full)
|
| 743 |
+
v_title = escape(v_full)
|
| 744 |
+
trs_parts.append(
|
| 745 |
+
"<tr style='border-bottom:1px solid #e9ecef;'>"
|
| 746 |
+
"<td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>"
|
| 747 |
+
f"<span title='{c_title}'>{c_txt}</span>"
|
| 748 |
+
"</td>"
|
| 749 |
+
"<td style='padding:4px 0; text-align:right; color:#495057; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>"
|
| 750 |
+
f"<span title='{v_title}'>{v_txt}</span>"
|
| 751 |
+
"</td>"
|
| 752 |
+
"</tr>"
|
| 753 |
+
)
|
| 754 |
+
trs = "".join(trs_parts)
|
| 755 |
+
colunas_html.append(
|
| 756 |
+
f"<div style='flex:0 0 {coluna_largura_px}px; width:{coluna_largura_px}px; min-width:{coluna_largura_px}px; overflow:hidden;'>"
|
| 757 |
+
f"<table style='border-collapse:collapse; table-layout:fixed; font-size:12px; width:{coluna_largura_px}px;'>{trs}</table>"
|
| 758 |
+
"</div>"
|
| 759 |
+
)
|
| 760 |
display = "block" if pagina_idx == 0 else "none"
|
| 761 |
pages_html.append(
|
| 762 |
+
f"<div class='mesa-popup-page' data-page='{pagina_idx + 1}' style='display:{display};'>"
|
| 763 |
+
f"<div style='display:flex; gap:12px; align-items:flex-start; flex-wrap:nowrap;'>{''.join(colunas_html)}</div>"
|
| 764 |
"</div>"
|
| 765 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
|
| 767 |
controls_html = ""
|
| 768 |
if len(paginas) > 1:
|
| 769 |
controls_html = (
|
| 770 |
+
"<div class='mesa-popup-controls' style='display:flex; gap:5px; flex-wrap:nowrap; margin-top:8px; align-items:center; justify-content:center; white-space:nowrap; width:100%;'>"
|
| 771 |
+
f"<button type='button' data-page-nav='first' data-a='first' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">«</button>"
|
| 772 |
+
f"<button type='button' data-page-nav='prev' data-a='prev' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">‹</button>"
|
| 773 |
+
"<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>"
|
| 774 |
+
f"<button type='button' data-page-nav='next' data-a='next' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">›</button>"
|
| 775 |
+
f"<button type='button' data-page-nav='last' data-a='last' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">»</button>"
|
| 776 |
"</div>"
|
| 777 |
)
|
| 778 |
|
| 779 |
popup_html = (
|
| 780 |
+
f"<div id='{popup_uid}' data-pager='1' data-current-page='1' data-page-start='1' data-page-window='1' "
|
| 781 |
+
f"style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden; width:{popup_largura_px}px; max-width:{popup_largura_px}px;\">"
|
| 782 |
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 783 |
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 784 |
+
f"<div class='mesa-popup-pages' style='width:100%;'>{''.join(pages_html)}</div>"
|
| 785 |
f"{controls_html}"
|
| 786 |
"</div></div>"
|
| 787 |
)
|
| 788 |
+
return popup_html, popup_largura_px
|
| 789 |
|
| 790 |
|
| 791 |
def _normalizar_stops_cor(
|
|
|
|
| 1371 |
secondary_area_unit='hectares'
|
| 1372 |
).add_to(m)
|
| 1373 |
add_zoom_responsive_circle_markers(m)
|
| 1374 |
+
add_popup_pagination_handlers(m)
|
| 1375 |
|
| 1376 |
# Ajusta bounds robustos para evitar "salto" para longe por pontos geocodificados distantes.
|
| 1377 |
df_bounds = df_mapa
|
backend/app/core/map_layers.py
CHANGED
|
@@ -237,3 +237,254 @@ def add_zoom_responsive_circle_markers(
|
|
| 237 |
"""
|
| 238 |
mapa.get_root().html.add_child(Element(script))
|
| 239 |
setattr(mapa, "_mesa_zoom_radius_script", True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
"""
|
| 238 |
mapa.get_root().html.add_child(Element(script))
|
| 239 |
setattr(mapa, "_mesa_zoom_radius_script", True)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def add_popup_pagination_handlers(mapa: folium.Map) -> None:
|
| 243 |
+
if getattr(mapa, "_mesa_popup_pager_script", False):
|
| 244 |
+
return
|
| 245 |
+
|
| 246 |
+
map_name = mapa.get_name()
|
| 247 |
+
script = f"""
|
| 248 |
+
<script>
|
| 249 |
+
(function() {{
|
| 250 |
+
const MAP_NAME = "{map_name}";
|
| 251 |
+
|
| 252 |
+
function getCssGapPx(el, fallback) {{
|
| 253 |
+
if (!el || !window.getComputedStyle) return fallback;
|
| 254 |
+
const style = window.getComputedStyle(el);
|
| 255 |
+
const raw = style.columnGap || style.gap || '';
|
| 256 |
+
const value = parseFloat(raw);
|
| 257 |
+
return Number.isFinite(value) ? value : fallback;
|
| 258 |
+
}}
|
| 259 |
+
|
| 260 |
+
function buildNumberButton() {{
|
| 261 |
+
const btn = document.createElement('button');
|
| 262 |
+
btn.type = 'button';
|
| 263 |
+
btn.dataset.pageNumber = '1';
|
| 264 |
+
btn.style.border = '1px solid #ced8e2';
|
| 265 |
+
btn.style.background = '#fff';
|
| 266 |
+
btn.style.borderRadius = '6px';
|
| 267 |
+
btn.style.padding = '2px 7px';
|
| 268 |
+
btn.style.fontSize = '11px';
|
| 269 |
+
btn.style.cursor = 'pointer';
|
| 270 |
+
btn.style.color = '#4e6479';
|
| 271 |
+
btn.style.display = 'inline-flex';
|
| 272 |
+
btn.style.alignItems = 'center';
|
| 273 |
+
btn.style.justifyContent = 'center';
|
| 274 |
+
btn.textContent = '1';
|
| 275 |
+
return btn;
|
| 276 |
+
}}
|
| 277 |
+
|
| 278 |
+
function ensureNumberSlots(numberWrap, count) {{
|
| 279 |
+
if (!numberWrap) return;
|
| 280 |
+
let target = Number.isFinite(count) ? count : 1;
|
| 281 |
+
if (target < 1) target = 1;
|
| 282 |
+
while (numberWrap.children.length < target) {{
|
| 283 |
+
numberWrap.appendChild(buildNumberButton());
|
| 284 |
+
}}
|
| 285 |
+
while (numberWrap.children.length > target) {{
|
| 286 |
+
numberWrap.removeChild(numberWrap.lastElementChild);
|
| 287 |
+
}}
|
| 288 |
+
}}
|
| 289 |
+
|
| 290 |
+
function measureNumberButtonWidth(numberWrap) {{
|
| 291 |
+
if (!numberWrap) return 30;
|
| 292 |
+
const probe = buildNumberButton();
|
| 293 |
+
probe.style.visibility = 'hidden';
|
| 294 |
+
probe.style.position = 'absolute';
|
| 295 |
+
probe.style.pointerEvents = 'none';
|
| 296 |
+
numberWrap.appendChild(probe);
|
| 297 |
+
const width = probe.getBoundingClientRect().width || 30;
|
| 298 |
+
numberWrap.removeChild(probe);
|
| 299 |
+
return Math.max(24, width);
|
| 300 |
+
}}
|
| 301 |
+
|
| 302 |
+
function resolveWindowSize(root, total) {{
|
| 303 |
+
if (!(total > 0)) return 1;
|
| 304 |
+
const controls = root.querySelector('.mesa-popup-controls');
|
| 305 |
+
const numberWrap = root.querySelector('[data-page-number-wrap]');
|
| 306 |
+
if (!controls || !numberWrap) return Math.max(1, total);
|
| 307 |
+
|
| 308 |
+
const controlsWidth = controls.getBoundingClientRect().width || root.getBoundingClientRect().width || 0;
|
| 309 |
+
if (!(controlsWidth > 0)) return Math.max(1, total);
|
| 310 |
+
|
| 311 |
+
const controlsGap = getCssGapPx(controls, 5);
|
| 312 |
+
const numberGap = getCssGapPx(numberWrap, 5);
|
| 313 |
+
let navWidth = 0;
|
| 314 |
+
controls.querySelectorAll('[data-page-nav]').forEach(function(btn) {{
|
| 315 |
+
navWidth += btn.getBoundingClientRect().width || 28;
|
| 316 |
+
}});
|
| 317 |
+
|
| 318 |
+
const numberButtonWidth = measureNumberButtonWidth(numberWrap);
|
| 319 |
+
const navCount = controls.querySelectorAll('[data-page-nav]').length;
|
| 320 |
+
const estimatedGaps = Math.max(0, navCount + 2) * controlsGap;
|
| 321 |
+
let available = controlsWidth - navWidth - estimatedGaps;
|
| 322 |
+
if (!(available > 0)) {{
|
| 323 |
+
available = controlsWidth - navWidth;
|
| 324 |
+
}}
|
| 325 |
+
|
| 326 |
+
let fit = Math.floor((available + numberGap) / (numberButtonWidth + numberGap));
|
| 327 |
+
if (!Number.isFinite(fit) || fit < 1) fit = 1;
|
| 328 |
+
if (fit > total) fit = total;
|
| 329 |
+
ensureNumberSlots(numberWrap, fit);
|
| 330 |
+
return fit;
|
| 331 |
+
}}
|
| 332 |
+
|
| 333 |
+
function updatePager(root, targetPage) {{
|
| 334 |
+
if (!root) return;
|
| 335 |
+
const pages = root.querySelectorAll('.mesa-popup-page');
|
| 336 |
+
const total = pages.length;
|
| 337 |
+
if (!total) return;
|
| 338 |
+
|
| 339 |
+
let windowSize = resolveWindowSize(root, total);
|
| 340 |
+
if (!Number.isFinite(windowSize) || windowSize < 1) windowSize = 1;
|
| 341 |
+
if (windowSize > total) windowSize = total;
|
| 342 |
+
root.dataset.pageWindow = String(windowSize);
|
| 343 |
+
|
| 344 |
+
const currentRaw = parseInt(root.dataset.currentPage || '1', 10);
|
| 345 |
+
let current = Number.isFinite(currentRaw) && currentRaw > 0 ? currentRaw : 1;
|
| 346 |
+
let next = Number.isFinite(targetPage) ? targetPage : current;
|
| 347 |
+
if (next < 1) next = 1;
|
| 348 |
+
if (next > total) next = total;
|
| 349 |
+
current = next;
|
| 350 |
+
|
| 351 |
+
let start = parseInt(root.dataset.pageStart || '1', 10);
|
| 352 |
+
if (!Number.isFinite(start) || start < 1) start = 1;
|
| 353 |
+
if (total <= windowSize) {{
|
| 354 |
+
start = 1;
|
| 355 |
+
}} else {{
|
| 356 |
+
if (current < start) start = current;
|
| 357 |
+
if (current > start + windowSize - 1) start = current - windowSize + 1;
|
| 358 |
+
const maxStart = total - windowSize + 1;
|
| 359 |
+
if (start > maxStart) start = maxStart;
|
| 360 |
+
if (start < 1) start = 1;
|
| 361 |
+
}}
|
| 362 |
+
|
| 363 |
+
root.dataset.currentPage = String(current);
|
| 364 |
+
root.dataset.pageStart = String(start);
|
| 365 |
+
|
| 366 |
+
pages.forEach(function(pageEl, idx) {{
|
| 367 |
+
pageEl.style.display = idx + 1 === current ? 'block' : 'none';
|
| 368 |
+
}});
|
| 369 |
+
|
| 370 |
+
const numberWrap = root.querySelector('[data-page-number-wrap]');
|
| 371 |
+
const numberButtons = numberWrap ? numberWrap.querySelectorAll('[data-page-number]') : [];
|
| 372 |
+
numberButtons.forEach(function(buttonEl, idx) {{
|
| 373 |
+
const value = start + idx;
|
| 374 |
+
if (value <= total) {{
|
| 375 |
+
buttonEl.style.display = 'inline-flex';
|
| 376 |
+
buttonEl.dataset.pageValue = String(value);
|
| 377 |
+
buttonEl.textContent = String(value);
|
| 378 |
+
buttonEl.style.background = value === current ? '#eaf1f7' : '#fff';
|
| 379 |
+
buttonEl.style.borderColor = value === current ? '#9fb4c8' : '#ced8e2';
|
| 380 |
+
buttonEl.style.color = value === current ? '#2f4b66' : '#4e6479';
|
| 381 |
+
}} else {{
|
| 382 |
+
buttonEl.style.display = 'none';
|
| 383 |
+
}}
|
| 384 |
+
}});
|
| 385 |
+
|
| 386 |
+
const onFirst = current <= 1;
|
| 387 |
+
const onLast = current >= total;
|
| 388 |
+
const firstBtn = root.querySelector('[data-a="first"]');
|
| 389 |
+
const prevBtn = root.querySelector('[data-a="prev"]');
|
| 390 |
+
const nextBtn = root.querySelector('[data-a="next"]');
|
| 391 |
+
const lastBtn = root.querySelector('[data-a="last"]');
|
| 392 |
+
|
| 393 |
+
if (firstBtn) {{
|
| 394 |
+
firstBtn.disabled = onFirst;
|
| 395 |
+
firstBtn.style.opacity = onFirst ? '0.45' : '1';
|
| 396 |
+
firstBtn.style.cursor = onFirst ? 'not-allowed' : 'pointer';
|
| 397 |
+
}}
|
| 398 |
+
if (prevBtn) {{
|
| 399 |
+
prevBtn.disabled = onFirst;
|
| 400 |
+
prevBtn.style.opacity = onFirst ? '0.45' : '1';
|
| 401 |
+
prevBtn.style.cursor = onFirst ? 'not-allowed' : 'pointer';
|
| 402 |
+
}}
|
| 403 |
+
if (nextBtn) {{
|
| 404 |
+
nextBtn.disabled = onLast;
|
| 405 |
+
nextBtn.style.opacity = onLast ? '0.45' : '1';
|
| 406 |
+
nextBtn.style.cursor = onLast ? 'not-allowed' : 'pointer';
|
| 407 |
+
}}
|
| 408 |
+
if (lastBtn) {{
|
| 409 |
+
lastBtn.disabled = onLast;
|
| 410 |
+
lastBtn.style.opacity = onLast ? '0.45' : '1';
|
| 411 |
+
lastBtn.style.cursor = onLast ? 'not-allowed' : 'pointer';
|
| 412 |
+
}}
|
| 413 |
+
}}
|
| 414 |
+
|
| 415 |
+
function handleClick(event) {{
|
| 416 |
+
const target = event && event.target ? event.target : null;
|
| 417 |
+
if (!target || !target.closest) return;
|
| 418 |
+
const button = target.closest('[data-page-number], [data-page-nav]');
|
| 419 |
+
if (!button) return;
|
| 420 |
+
|
| 421 |
+
const root = button.closest('[data-pager]');
|
| 422 |
+
if (!root) return;
|
| 423 |
+
|
| 424 |
+
event.preventDefault();
|
| 425 |
+
event.stopPropagation();
|
| 426 |
+
if (button.disabled) return;
|
| 427 |
+
|
| 428 |
+
const action = button.dataset.a || button.dataset.pageValue || '';
|
| 429 |
+
const currentRaw = parseInt(root.dataset.currentPage || '1', 10);
|
| 430 |
+
const current = Number.isFinite(currentRaw) && currentRaw > 0 ? currentRaw : 1;
|
| 431 |
+
const total = root.querySelectorAll('.mesa-popup-page').length;
|
| 432 |
+
if (!total) return;
|
| 433 |
+
|
| 434 |
+
let nextPage = current;
|
| 435 |
+
if (action === 'first') nextPage = 1;
|
| 436 |
+
else if (action === 'prev') nextPage = current - 1;
|
| 437 |
+
else if (action === 'next') nextPage = current + 1;
|
| 438 |
+
else if (action === 'last') nextPage = total;
|
| 439 |
+
else {{
|
| 440 |
+
const parsed = parseInt(action, 10);
|
| 441 |
+
if (Number.isFinite(parsed)) nextPage = parsed;
|
| 442 |
+
}}
|
| 443 |
+
|
| 444 |
+
updatePager(root, nextPage);
|
| 445 |
+
}}
|
| 446 |
+
|
| 447 |
+
function bindWhenMapReady() {{
|
| 448 |
+
const map = window[MAP_NAME];
|
| 449 |
+
if (!map) {{
|
| 450 |
+
setTimeout(bindWhenMapReady, 50);
|
| 451 |
+
return;
|
| 452 |
+
}}
|
| 453 |
+
if (map.__mesaPopupPagerBound) return;
|
| 454 |
+
map.__mesaPopupPagerBound = true;
|
| 455 |
+
|
| 456 |
+
const mapContainer = typeof map.getContainer === 'function' ? map.getContainer() : null;
|
| 457 |
+
const doc = mapContainer && mapContainer.ownerDocument ? mapContainer.ownerDocument : document;
|
| 458 |
+
doc.addEventListener('click', handleClick, true);
|
| 459 |
+
|
| 460 |
+
map.on('popupopen', function(evt) {{
|
| 461 |
+
const popup = evt && evt.popup ? evt.popup : null;
|
| 462 |
+
const contentNode = popup && popup._contentNode ? popup._contentNode : null;
|
| 463 |
+
if (!contentNode || !contentNode.querySelector) return;
|
| 464 |
+
const root = contentNode.querySelector('[data-pager]');
|
| 465 |
+
if (!root) return;
|
| 466 |
+
const initialRaw = parseInt(root.dataset.currentPage || '1', 10);
|
| 467 |
+
const initialPage = Number.isFinite(initialRaw) && initialRaw > 0 ? initialRaw : 1;
|
| 468 |
+
updatePager(root, initialPage);
|
| 469 |
+
}});
|
| 470 |
+
|
| 471 |
+
let resizeTimer = null;
|
| 472 |
+
window.addEventListener('resize', function() {{
|
| 473 |
+
if (resizeTimer) window.clearTimeout(resizeTimer);
|
| 474 |
+
resizeTimer = window.setTimeout(function() {{
|
| 475 |
+
const pagers = doc.querySelectorAll('[data-pager]');
|
| 476 |
+
pagers.forEach(function(root) {{
|
| 477 |
+
const currentRaw = parseInt(root.dataset.currentPage || '1', 10);
|
| 478 |
+
const current = Number.isFinite(currentRaw) && currentRaw > 0 ? currentRaw : 1;
|
| 479 |
+
updatePager(root, current);
|
| 480 |
+
}});
|
| 481 |
+
}}, 70);
|
| 482 |
+
}});
|
| 483 |
+
}}
|
| 484 |
+
|
| 485 |
+
bindWhenMapReady();
|
| 486 |
+
}})();
|
| 487 |
+
</script>
|
| 488 |
+
"""
|
| 489 |
+
mapa.get_root().html.add_child(Element(script))
|
| 490 |
+
setattr(mapa, "_mesa_popup_pager_script", True)
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -23,7 +23,12 @@ import math
|
|
| 23 |
|
| 24 |
from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
|
| 25 |
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 26 |
-
from app.core.map_layers import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# ============================================================
|
| 29 |
# CONSTANTES
|
|
@@ -692,12 +697,15 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 692 |
max_raio_metros = 22.0
|
| 693 |
metros_por_grau_lat = 111_320.0
|
| 694 |
|
|
|
|
|
|
|
|
|
|
| 695 |
for _, idx_labels in grupos.indices.items():
|
| 696 |
-
|
|
|
|
| 697 |
continue
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
base_lon = float(df_plot.at[idx_list[0], lon_plot_col])
|
| 701 |
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 702 |
continue
|
| 703 |
|
|
@@ -706,7 +714,7 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 706 |
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 707 |
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 708 |
|
| 709 |
-
for pos,
|
| 710 |
if pos == 0:
|
| 711 |
continue
|
| 712 |
|
|
@@ -723,13 +731,21 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 723 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 724 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 725 |
|
| 726 |
-
df_plot.
|
| 727 |
-
df_plot.
|
| 728 |
|
| 729 |
return df_plot
|
| 730 |
|
| 731 |
|
| 732 |
def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
if not itens:
|
| 734 |
html = (
|
| 735 |
"<div style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">"
|
|
@@ -739,66 +755,77 @@ def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
|
| 739 |
)
|
| 740 |
return html, 360
|
| 741 |
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
|
|
|
|
| 746 |
for page_idx, page_items in enumerate(paginas):
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
display = "block" if page_idx == 0 else "none"
|
| 755 |
pages_html.append(
|
| 756 |
-
f"<div class='mesa-popup-page' data-page='{page_idx}' style='display:{display};'>"
|
| 757 |
-
f"<
|
| 758 |
"</div>"
|
| 759 |
)
|
| 760 |
-
botao_style = (
|
| 761 |
-
"border:1px solid #9fb4c8; background:#eaf1f7; border-radius:6px; "
|
| 762 |
-
"padding:2px 7px; font-size:11px; cursor:pointer; color:#2f4b66;"
|
| 763 |
-
if page_idx == 0
|
| 764 |
-
else
|
| 765 |
-
"border:1px solid #ced8e2; background:#fff; border-radius:6px; "
|
| 766 |
-
"padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;"
|
| 767 |
-
)
|
| 768 |
-
onclick = (
|
| 769 |
-
f"var root=document.getElementById('{popup_uid}');"
|
| 770 |
-
"if(!root){return false;}"
|
| 771 |
-
"var pages=root.querySelectorAll('.mesa-popup-page');"
|
| 772 |
-
"for(var i=0;i<pages.length;i++){pages[i].style.display=(i==="
|
| 773 |
-
f"{page_idx}"
|
| 774 |
-
")?'block':'none';}"
|
| 775 |
-
"var btns=root.querySelectorAll('[data-page-btn]');"
|
| 776 |
-
"for(var j=0;j<btns.length;j++){btns[j].style.background='#fff';btns[j].style.borderColor='#ced8e2';btns[j].style.color='#4e6479';}"
|
| 777 |
-
"this.style.background='#eaf1f7';this.style.borderColor='#9fb4c8';this.style.color='#2f4b66';"
|
| 778 |
-
"return false;"
|
| 779 |
-
)
|
| 780 |
-
botoes_html.append(
|
| 781 |
-
f"<button type='button' data-page-btn='1' style=\"{botao_style}\" onclick=\"{onclick}\">"
|
| 782 |
-
f"{page_idx + 1}</button>"
|
| 783 |
-
)
|
| 784 |
|
| 785 |
controls_html = ""
|
| 786 |
if len(paginas) > 1:
|
| 787 |
controls_html = (
|
| 788 |
-
"<div class='mesa-popup-controls' style='display:flex; gap:
|
| 789 |
-
f"<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
"</div>"
|
| 791 |
)
|
| 792 |
|
| 793 |
html = (
|
| 794 |
-
f"<div id='{popup_uid}'
|
|
|
|
| 795 |
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 796 |
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 797 |
-
f"<div class='mesa-popup-pages'>{''.join(pages_html)}</div>"
|
| 798 |
f"{controls_html}"
|
| 799 |
"</div></div>"
|
| 800 |
)
|
| 801 |
-
return html,
|
| 802 |
|
| 803 |
# ============================================================
|
| 804 |
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
|
@@ -1029,6 +1056,7 @@ def criar_mapa(df, lat_col="lat", lon_col="lon", cor_col=None, tamanho_col=None,
|
|
| 1029 |
secondary_area_unit='hectares'
|
| 1030 |
).add_to(m)
|
| 1031 |
add_zoom_responsive_circle_markers(m)
|
|
|
|
| 1032 |
|
| 1033 |
# Ajusta bounds robustos para evitar "salto" por pontos geocodificados distantes.
|
| 1034 |
df_bounds = df_mapa
|
|
|
|
| 23 |
|
| 24 |
from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
|
| 25 |
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 26 |
+
from app.core.map_layers import (
|
| 27 |
+
add_bairros_layer,
|
| 28 |
+
add_indice_marker,
|
| 29 |
+
add_popup_pagination_handlers,
|
| 30 |
+
add_zoom_responsive_circle_markers,
|
| 31 |
+
)
|
| 32 |
|
| 33 |
# ============================================================
|
| 34 |
# CONSTANTES
|
|
|
|
| 697 |
max_raio_metros = 22.0
|
| 698 |
metros_por_grau_lat = 111_320.0
|
| 699 |
|
| 700 |
+
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 701 |
+
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 702 |
+
|
| 703 |
for _, idx_labels in grupos.indices.items():
|
| 704 |
+
posicoes = np.asarray(idx_labels, dtype=int)
|
| 705 |
+
if posicoes.size <= 1:
|
| 706 |
continue
|
| 707 |
+
base_lat = float(df_plot.iat[int(posicoes[0]), lat_plot_pos])
|
| 708 |
+
base_lon = float(df_plot.iat[int(posicoes[0]), lon_plot_pos])
|
|
|
|
| 709 |
if not np.isfinite(base_lat) or not np.isfinite(base_lon):
|
| 710 |
continue
|
| 711 |
|
|
|
|
| 714 |
cos_lat = max(abs(math.cos(math.radians(base_lat))), 1e-6)
|
| 715 |
metros_por_grau_lon = metros_por_grau_lat * cos_lat
|
| 716 |
|
| 717 |
+
for pos, pos_idx in enumerate(posicoes):
|
| 718 |
if pos == 0:
|
| 719 |
continue
|
| 720 |
|
|
|
|
| 731 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 732 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 733 |
|
| 734 |
+
df_plot.iat[int(pos_idx), lat_plot_pos] = base_lat + delta_lat
|
| 735 |
+
df_plot.iat[int(pos_idx), lon_plot_pos] = base_lon + delta_lon
|
| 736 |
|
| 737 |
return df_plot
|
| 738 |
|
| 739 |
|
| 740 |
def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
| 741 |
+
def _limitar_texto(valor: str, limite: int) -> str:
|
| 742 |
+
txt = str(valor)
|
| 743 |
+
if limite <= 0 or len(txt) <= limite:
|
| 744 |
+
return txt
|
| 745 |
+
if limite <= 3:
|
| 746 |
+
return txt[:limite]
|
| 747 |
+
return txt[: limite - 3] + "..."
|
| 748 |
+
|
| 749 |
if not itens:
|
| 750 |
html = (
|
| 751 |
"<div style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden;\">"
|
|
|
|
| 755 |
)
|
| 756 |
return html, 360
|
| 757 |
|
| 758 |
+
max_colunas_por_pagina = 2
|
| 759 |
+
max_chars_chave = 20
|
| 760 |
+
max_chars_valor = 20
|
| 761 |
+
char_px = 7.2
|
| 762 |
+
gap_cols_px = 12
|
| 763 |
+
popup_padding_horizontal_px = 30
|
| 764 |
+
coluna_largura_px = int(round((max_chars_chave + max_chars_valor) * char_px + 28))
|
| 765 |
+
itens_por_pagina = max_itens_pagina * max_colunas_por_pagina
|
| 766 |
+
paginas = [itens[i:i + itens_por_pagina] for i in range(0, len(itens), itens_por_pagina)]
|
| 767 |
+
itens_primeira_pagina = len(paginas[0]) if paginas else 0
|
| 768 |
+
colunas_visiveis = max(1, min(max_colunas_por_pagina, int(math.ceil(itens_primeira_pagina / max_itens_pagina)) if itens_primeira_pagina else 1))
|
| 769 |
+
popup_largura_px = popup_padding_horizontal_px + (coluna_largura_px * colunas_visiveis) + (gap_cols_px * (colunas_visiveis - 1))
|
| 770 |
|
| 771 |
+
pages_html = []
|
| 772 |
for page_idx, page_items in enumerate(paginas):
|
| 773 |
+
colunas = [page_items[i:i + max_itens_pagina] for i in range(0, len(page_items), max_itens_pagina)]
|
| 774 |
+
colunas_html = []
|
| 775 |
+
for col_itens in colunas:
|
| 776 |
+
trs_parts = []
|
| 777 |
+
for c, v in col_itens:
|
| 778 |
+
c_full = str(c)
|
| 779 |
+
v_full = str(v)
|
| 780 |
+
c_txt = escape(_limitar_texto(c_full, max_chars_chave))
|
| 781 |
+
v_txt = escape(_limitar_texto(v_full, max_chars_valor))
|
| 782 |
+
c_title = escape(c_full)
|
| 783 |
+
v_title = escape(v_full)
|
| 784 |
+
trs_parts.append(
|
| 785 |
+
"<tr style='border-bottom:1px solid #e9ecef;'>"
|
| 786 |
+
"<td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>"
|
| 787 |
+
f"<span title='{c_title}'>{c_txt}</span>"
|
| 788 |
+
"</td>"
|
| 789 |
+
"<td style='padding:4px 0; text-align:right; color:#495057; width:50%; max-width:50%; overflow:hidden; white-space:nowrap;'>"
|
| 790 |
+
f"<span title='{v_title}'>{v_txt}</span>"
|
| 791 |
+
"</td>"
|
| 792 |
+
"</tr>"
|
| 793 |
+
)
|
| 794 |
+
trs = "".join(trs_parts)
|
| 795 |
+
colunas_html.append(
|
| 796 |
+
f"<div style='flex:0 0 {coluna_largura_px}px; width:{coluna_largura_px}px; min-width:{coluna_largura_px}px; overflow:hidden;'>"
|
| 797 |
+
f"<table style='border-collapse:collapse; table-layout:fixed; font-size:12px; width:{coluna_largura_px}px;'>{trs}</table>"
|
| 798 |
+
"</div>"
|
| 799 |
+
)
|
| 800 |
display = "block" if page_idx == 0 else "none"
|
| 801 |
pages_html.append(
|
| 802 |
+
f"<div class='mesa-popup-page' data-page='{page_idx + 1}' style='display:{display};'>"
|
| 803 |
+
f"<div style='display:flex; gap:12px; align-items:flex-start; flex-wrap:nowrap;'>{''.join(colunas_html)}</div>"
|
| 804 |
"</div>"
|
| 805 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
|
| 807 |
controls_html = ""
|
| 808 |
if len(paginas) > 1:
|
| 809 |
controls_html = (
|
| 810 |
+
"<div class='mesa-popup-controls' style='display:flex; gap:5px; flex-wrap:nowrap; margin-top:8px; align-items:center; justify-content:center; white-space:nowrap; width:100%;'>"
|
| 811 |
+
f"<button type='button' data-page-nav='first' data-a='first' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">«</button>"
|
| 812 |
+
f"<button type='button' data-page-nav='prev' data-a='prev' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">‹</button>"
|
| 813 |
+
"<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>"
|
| 814 |
+
f"<button type='button' data-page-nav='next' data-a='next' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">›</button>"
|
| 815 |
+
f"<button type='button' data-page-nav='last' data-a='last' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">»</button>"
|
| 816 |
"</div>"
|
| 817 |
)
|
| 818 |
|
| 819 |
html = (
|
| 820 |
+
f"<div id='{popup_uid}' data-pager='1' data-current-page='1' data-page-start='1' data-page-window='1' "
|
| 821 |
+
f"style=\"font-family:'Segoe UI'; border-radius:8px; overflow:hidden; width:{popup_largura_px}px; max-width:{popup_largura_px}px;\">"
|
| 822 |
"<div style=\"background:#6c757d; color:white; padding:10px 15px; font-weight:600;\">Dados do Registro</div>"
|
| 823 |
"<div style=\"padding:12px 15px; background:#f8f9fa;\">"
|
| 824 |
+
f"<div class='mesa-popup-pages' style='width:100%;'>{''.join(pages_html)}</div>"
|
| 825 |
f"{controls_html}"
|
| 826 |
"</div></div>"
|
| 827 |
)
|
| 828 |
+
return html, popup_largura_px
|
| 829 |
|
| 830 |
# ============================================================
|
| 831 |
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
|
|
|
| 1056 |
secondary_area_unit='hectares'
|
| 1057 |
).add_to(m)
|
| 1058 |
add_zoom_responsive_circle_markers(m)
|
| 1059 |
+
add_popup_pagination_handlers(m)
|
| 1060 |
|
| 1061 |
# Ajusta bounds robustos para evitar "salto" por pontos geocodificados distantes.
|
| 1062 |
df_bounds = df_mapa
|
frontend/src/App.jsx
CHANGED
|
@@ -22,6 +22,7 @@ export default function App() {
|
|
| 22 |
const [showStartupIntro, setShowStartupIntro] = useState(true)
|
| 23 |
const [sessionId, setSessionId] = useState('')
|
| 24 |
const [bootError, setBootError] = useState('')
|
|
|
|
| 25 |
|
| 26 |
const [authLoading, setAuthLoading] = useState(true)
|
| 27 |
const [authUser, setAuthUser] = useState(null)
|
|
@@ -241,6 +242,18 @@ export default function App() {
|
|
| 241 |
setLogsOpen(false)
|
| 242 |
}
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
return (
|
| 245 |
<div className="app-shell">
|
| 246 |
<header className="app-header app-header-logo-only">
|
|
@@ -453,7 +466,7 @@ export default function App() {
|
|
| 453 |
) : null}
|
| 454 |
|
| 455 |
<div className="tab-pane" hidden={activeTab !== 'Pesquisa'}>
|
| 456 |
-
<PesquisaTab sessionId={sessionId} />
|
| 457 |
</div>
|
| 458 |
|
| 459 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
|
@@ -469,7 +482,7 @@ export default function App() {
|
|
| 469 |
</div>
|
| 470 |
|
| 471 |
<div className="tab-pane" hidden={activeTab !== 'Avaliação Beta'}>
|
| 472 |
-
<AvaliacaoBetaTab sessionId={sessionId} />
|
| 473 |
</div>
|
| 474 |
</>
|
| 475 |
)
|
|
|
|
| 22 |
const [showStartupIntro, setShowStartupIntro] = useState(true)
|
| 23 |
const [sessionId, setSessionId] = useState('')
|
| 24 |
const [bootError, setBootError] = useState('')
|
| 25 |
+
const [avaliacaoBetaQuickLoad, setAvaliacaoBetaQuickLoad] = useState(null)
|
| 26 |
|
| 27 |
const [authLoading, setAuthLoading] = useState(true)
|
| 28 |
const [authUser, setAuthUser] = useState(null)
|
|
|
|
| 242 |
setLogsOpen(false)
|
| 243 |
}
|
| 244 |
|
| 245 |
+
function onUsarModeloEmAvaliacao(modelo) {
|
| 246 |
+
const modeloId = String(modelo?.id || '').trim()
|
| 247 |
+
if (!modeloId) return
|
| 248 |
+
setAvaliacaoBetaQuickLoad({
|
| 249 |
+
requestKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
| 250 |
+
modeloId,
|
| 251 |
+
nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId),
|
| 252 |
+
})
|
| 253 |
+
setActiveTab('Avaliação Beta')
|
| 254 |
+
setShowStartupIntro(false)
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
return (
|
| 258 |
<div className="app-shell">
|
| 259 |
<header className="app-header app-header-logo-only">
|
|
|
|
| 466 |
) : null}
|
| 467 |
|
| 468 |
<div className="tab-pane" hidden={activeTab !== 'Pesquisa'}>
|
| 469 |
+
<PesquisaTab sessionId={sessionId} onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao} />
|
| 470 |
</div>
|
| 471 |
|
| 472 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
|
|
|
| 482 |
</div>
|
| 483 |
|
| 484 |
<div className="tab-pane" hidden={activeTab !== 'Avaliação Beta'}>
|
| 485 |
+
<AvaliacaoBetaTab sessionId={sessionId} quickLoadRequest={avaliacaoBetaQuickLoad} />
|
| 486 |
</div>
|
| 487 |
</>
|
| 488 |
)
|
frontend/src/components/AvaliacaoBetaTab.jsx
CHANGED
|
@@ -63,7 +63,7 @@ function formatarFonteRepositorio(fonte) {
|
|
| 63 |
return 'Fonte: pasta local'
|
| 64 |
}
|
| 65 |
|
| 66 |
-
export default function AvaliacaoBetaTab({ sessionId }) {
|
| 67 |
const [loading, setLoading] = useState(false)
|
| 68 |
const [error, setError] = useState('')
|
| 69 |
const [status, setStatus] = useState('')
|
|
@@ -88,6 +88,7 @@ export default function AvaliacaoBetaTab({ sessionId }) {
|
|
| 88 |
const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
|
| 89 |
|
| 90 |
const uploadInputRef = useRef(null)
|
|
|
|
| 91 |
|
| 92 |
const repoModeloOptions = useMemo(
|
| 93 |
() => (repoModelos || []).map((item) => ({
|
|
@@ -206,14 +207,16 @@ export default function AvaliacaoBetaTab({ sessionId }) {
|
|
| 206 |
})
|
| 207 |
}
|
| 208 |
|
| 209 |
-
async function onCarregarModeloRepositorio() {
|
| 210 |
-
|
|
|
|
| 211 |
setModeloLoadSource('repo')
|
|
|
|
| 212 |
await withBusy(async () => {
|
| 213 |
-
const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId,
|
| 214 |
setStatus(uploadResp?.status || '')
|
| 215 |
setBadgeHtml(uploadResp?.badge_html || '')
|
| 216 |
-
const modeloSelecionado = repoModeloOptions.find((item) => item.value ===
|
| 217 |
const nomeModelo = uploadResp?.nome_modelo || modeloSelecionado?.label || ''
|
| 218 |
const exibirResp = await api.exibirVisualizacao(sessionId)
|
| 219 |
aplicarRespostaExibicao(exibirResp, nomeModelo)
|
|
@@ -221,6 +224,17 @@ export default function AvaliacaoBetaTab({ sessionId }) {
|
|
| 221 |
})
|
| 222 |
}
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
function onUploadInputChange(event) {
|
| 225 |
const input = event.target
|
| 226 |
const file = input.files?.[0] ?? null
|
|
|
|
| 63 |
return 'Fonte: pasta local'
|
| 64 |
}
|
| 65 |
|
| 66 |
+
export default function AvaliacaoBetaTab({ sessionId, quickLoadRequest = null }) {
|
| 67 |
const [loading, setLoading] = useState(false)
|
| 68 |
const [error, setError] = useState('')
|
| 69 |
const [status, setStatus] = useState('')
|
|
|
|
| 88 |
const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
|
| 89 |
|
| 90 |
const uploadInputRef = useRef(null)
|
| 91 |
+
const quickLoadHandledRef = useRef('')
|
| 92 |
|
| 93 |
const repoModeloOptions = useMemo(
|
| 94 |
() => (repoModelos || []).map((item) => ({
|
|
|
|
| 207 |
})
|
| 208 |
}
|
| 209 |
|
| 210 |
+
async function onCarregarModeloRepositorio(modeloIdOverride = '') {
|
| 211 |
+
const alvoModeloId = String(modeloIdOverride || repoModeloSelecionado || '').trim()
|
| 212 |
+
if (!sessionId || !alvoModeloId) return
|
| 213 |
setModeloLoadSource('repo')
|
| 214 |
+
setRepoModeloSelecionado(alvoModeloId)
|
| 215 |
await withBusy(async () => {
|
| 216 |
+
const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, alvoModeloId)
|
| 217 |
setStatus(uploadResp?.status || '')
|
| 218 |
setBadgeHtml(uploadResp?.badge_html || '')
|
| 219 |
+
const modeloSelecionado = repoModeloOptions.find((item) => item.value === alvoModeloId)
|
| 220 |
const nomeModelo = uploadResp?.nome_modelo || modeloSelecionado?.label || ''
|
| 221 |
const exibirResp = await api.exibirVisualizacao(sessionId)
|
| 222 |
aplicarRespostaExibicao(exibirResp, nomeModelo)
|
|
|
|
| 224 |
})
|
| 225 |
}
|
| 226 |
|
| 227 |
+
useEffect(() => {
|
| 228 |
+
const requestKey = String(quickLoadRequest?.requestKey || '').trim()
|
| 229 |
+
const modeloId = String(quickLoadRequest?.modeloId || '').trim()
|
| 230 |
+
if (!sessionId || !requestKey || !modeloId) return
|
| 231 |
+
if (quickLoadHandledRef.current === requestKey) return
|
| 232 |
+
quickLoadHandledRef.current = requestKey
|
| 233 |
+
setModeloLoadSource('repo')
|
| 234 |
+
setRepoModeloSelecionado(modeloId)
|
| 235 |
+
void onCarregarModeloRepositorio(modeloId)
|
| 236 |
+
}, [quickLoadRequest, sessionId])
|
| 237 |
+
|
| 238 |
function onUploadInputChange(event) {
|
| 239 |
const input = event.target
|
| 240 |
const file = input.files?.[0] ?? null
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -452,7 +452,7 @@ function ChipAutocompleteInput({
|
|
| 452 |
)
|
| 453 |
}
|
| 454 |
|
| 455 |
-
export default function PesquisaTab({ sessionId }) {
|
| 456 |
const [loading, setLoading] = useState(false)
|
| 457 |
const [error, setError] = useState('')
|
| 458 |
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
|
|
@@ -490,6 +490,7 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 490 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 491 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 492 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
|
|
|
| 493 |
|
| 494 |
const sugestoes = result.sugestoes || {}
|
| 495 |
const opcoesTipoModelo = useMemo(
|
|
@@ -502,6 +503,31 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 502 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 503 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
async function buscarModelos(nextFilters = filters) {
|
| 506 |
setLoading(true)
|
| 507 |
setError('')
|
|
@@ -598,6 +624,16 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 598 |
})
|
| 599 |
}
|
| 600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
function preencherModeloAberto(resp) {
|
| 602 |
setModeloAbertoDados(resp?.dados || null)
|
| 603 |
setModeloAbertoEstatisticas(resp?.estatisticas || null)
|
|
@@ -633,6 +669,9 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 633 |
id: modelo.id,
|
| 634 |
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
|
| 635 |
})
|
|
|
|
|
|
|
|
|
|
| 636 |
} catch (err) {
|
| 637 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
| 638 |
} finally {
|
|
@@ -644,6 +683,7 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 644 |
setModeloAbertoMeta(null)
|
| 645 |
setModeloAbertoError('')
|
| 646 |
setModeloAbertoActiveTab('mapa')
|
|
|
|
| 647 |
}
|
| 648 |
|
| 649 |
async function onModeloAbertoMapChange(nextVar) {
|
|
@@ -938,11 +978,12 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 938 |
{error ? <div className="error-line inline-error">{error}</div> : null}
|
| 939 |
</SectionBlock>
|
| 940 |
|
| 941 |
-
<
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
|
|
|
| 946 |
<div className="pesquisa-results-toolbar">
|
| 947 |
<div className="pesquisa-summary-line">
|
| 948 |
<strong>{formatCount(result.total_filtrado)}</strong>{' '}
|
|
@@ -969,21 +1010,21 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 969 |
return (
|
| 970 |
<article key={modelo.id} className={`pesquisa-card${selecionado ? ' is-selected' : ''}`}>
|
| 971 |
<div className="pesquisa-card-top">
|
| 972 |
-
<
|
| 973 |
-
|
| 974 |
-
<
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
</
|
| 987 |
</div>
|
| 988 |
<div className="pesquisa-card-body">
|
| 989 |
<div className="pesquisa-card-dados-list">
|
|
@@ -1005,7 +1046,8 @@ export default function PesquisaTab({ sessionId }) {
|
|
| 1005 |
})}
|
| 1006 |
</div>
|
| 1007 |
)}
|
| 1008 |
-
|
|
|
|
| 1009 |
|
| 1010 |
<SectionBlock step="3" title="Mapa" subtitle="Plote os modelos selecionados com cores distintas e legenda.">
|
| 1011 |
<div className="pesquisa-summary-line">
|
|
|
|
| 452 |
)
|
| 453 |
}
|
| 454 |
|
| 455 |
+
export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null }) {
|
| 456 |
const [loading, setLoading] = useState(false)
|
| 457 |
const [error, setError] = useState('')
|
| 458 |
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
|
|
|
|
| 490 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 491 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 492 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 493 |
+
const sectionResultadosRef = useRef(null)
|
| 494 |
|
| 495 |
const sugestoes = result.sugestoes || {}
|
| 496 |
const opcoesTipoModelo = useMemo(
|
|
|
|
| 503 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 504 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 505 |
|
| 506 |
+
function scrollToElementTop(el, behavior = 'smooth', offsetPx = 0) {
|
| 507 |
+
if (!el || typeof window === 'undefined') return
|
| 508 |
+
const rect = el.getBoundingClientRect()
|
| 509 |
+
const targetTop = Math.max(0, window.scrollY + rect.top - offsetPx)
|
| 510 |
+
window.scrollTo({ top: targetTop, behavior })
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
function scrollParaAbasGeraisNoTopo() {
|
| 514 |
+
if (typeof document === 'undefined') return
|
| 515 |
+
const tabsEl = document.querySelector('.tabs')
|
| 516 |
+
if (tabsEl) {
|
| 517 |
+
scrollToElementTop(tabsEl, 'smooth', 0)
|
| 518 |
+
return
|
| 519 |
+
}
|
| 520 |
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function scrollParaResultadosNoTopo() {
|
| 524 |
+
window.requestAnimationFrame(() => {
|
| 525 |
+
window.requestAnimationFrame(() => {
|
| 526 |
+
scrollToElementTop(sectionResultadosRef.current, 'smooth', 0)
|
| 527 |
+
})
|
| 528 |
+
})
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
async function buscarModelos(nextFilters = filters) {
|
| 532 |
setLoading(true)
|
| 533 |
setError('')
|
|
|
|
| 624 |
})
|
| 625 |
}
|
| 626 |
|
| 627 |
+
function onUsarEmAvaliacao(modelo) {
|
| 628 |
+
if (!sessionId) {
|
| 629 |
+
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
|
| 630 |
+
return
|
| 631 |
+
}
|
| 632 |
+
if (typeof onUsarModeloEmAvaliacao === 'function') {
|
| 633 |
+
onUsarModeloEmAvaliacao(modelo)
|
| 634 |
+
}
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
function preencherModeloAberto(resp) {
|
| 638 |
setModeloAbertoDados(resp?.dados || null)
|
| 639 |
setModeloAbertoEstatisticas(resp?.estatisticas || null)
|
|
|
|
| 669 |
id: modelo.id,
|
| 670 |
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
|
| 671 |
})
|
| 672 |
+
window.requestAnimationFrame(() => {
|
| 673 |
+
scrollParaAbasGeraisNoTopo()
|
| 674 |
+
})
|
| 675 |
} catch (err) {
|
| 676 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
| 677 |
} finally {
|
|
|
|
| 683 |
setModeloAbertoMeta(null)
|
| 684 |
setModeloAbertoError('')
|
| 685 |
setModeloAbertoActiveTab('mapa')
|
| 686 |
+
scrollParaResultadosNoTopo()
|
| 687 |
}
|
| 688 |
|
| 689 |
async function onModeloAbertoMapChange(nextVar) {
|
|
|
|
| 978 |
{error ? <div className="error-line inline-error">{error}</div> : null}
|
| 979 |
</SectionBlock>
|
| 980 |
|
| 981 |
+
<div ref={sectionResultadosRef}>
|
| 982 |
+
<SectionBlock
|
| 983 |
+
step="2"
|
| 984 |
+
title="Resultados"
|
| 985 |
+
subtitle="Modelos aceitos para os parametros do avaliando informado."
|
| 986 |
+
>
|
| 987 |
<div className="pesquisa-results-toolbar">
|
| 988 |
<div className="pesquisa-summary-line">
|
| 989 |
<strong>{formatCount(result.total_filtrado)}</strong>{' '}
|
|
|
|
| 1010 |
return (
|
| 1011 |
<article key={modelo.id} className={`pesquisa-card${selecionado ? ' is-selected' : ''}`}>
|
| 1012 |
<div className="pesquisa-card-top">
|
| 1013 |
+
<h4 className="pesquisa-card-title">{modelo.nome_modelo || modelo.arquivo}</h4>
|
| 1014 |
+
<div className="pesquisa-card-actions">
|
| 1015 |
+
<button
|
| 1016 |
+
type="button"
|
| 1017 |
+
className={`btn-pesquisa-map-toggle${selecionado ? ' is-selected' : ''}`}
|
| 1018 |
+
onClick={() => onToggleSelecionado(modelo.id)}
|
| 1019 |
+
>
|
| 1020 |
+
{selecionado ? 'Desmarcar para mapa' : 'Marcar para mapa'}
|
| 1021 |
+
</button>
|
| 1022 |
+
<button type="button" className="btn-pesquisa-open" onClick={() => void onAbrirModelo(modelo)}>
|
| 1023 |
+
Abrir modelo
|
| 1024 |
+
</button>
|
| 1025 |
+
<button type="button" className="btn-pesquisa-eval" onClick={() => onUsarEmAvaliacao(modelo)}>
|
| 1026 |
+
Usar em avaliação
|
| 1027 |
+
</button>
|
| 1028 |
</div>
|
| 1029 |
<div className="pesquisa-card-body">
|
| 1030 |
<div className="pesquisa-card-dados-list">
|
|
|
|
| 1046 |
})}
|
| 1047 |
</div>
|
| 1048 |
)}
|
| 1049 |
+
</SectionBlock>
|
| 1050 |
+
</div>
|
| 1051 |
|
| 1052 |
<SectionBlock step="3" title="Mapa" subtitle="Plote os modelos selecionados com cores distintas e legenda.">
|
| 1053 |
<div className="pesquisa-summary-line">
|
frontend/src/styles.css
CHANGED
|
@@ -1675,82 +1675,67 @@ button.pesquisa-coluna-remove:hover {
|
|
| 1675 |
.pesquisa-card-top {
|
| 1676 |
display: flex;
|
| 1677 |
flex-direction: column;
|
| 1678 |
-
gap:
|
| 1679 |
min-width: 0;
|
| 1680 |
flex: 1 1 auto;
|
| 1681 |
}
|
| 1682 |
|
| 1683 |
-
.pesquisa-card-
|
| 1684 |
-
display: flex;
|
| 1685 |
-
align-items: flex-start;
|
| 1686 |
-
justify-content: space-between;
|
| 1687 |
-
gap: 14px;
|
| 1688 |
-
min-width: 0;
|
| 1689 |
-
}
|
| 1690 |
-
|
| 1691 |
-
.pesquisa-card-head h4 {
|
| 1692 |
margin: 0;
|
| 1693 |
font-family: 'Sora', sans-serif;
|
| 1694 |
color: #2e4358;
|
| 1695 |
-
font-size: 0.
|
| 1696 |
line-height: 1.32;
|
| 1697 |
overflow-wrap: anywhere;
|
| 1698 |
-
flex: 1 1 auto;
|
| 1699 |
-
}
|
| 1700 |
-
|
| 1701 |
-
.pesquisa-card-head p {
|
| 1702 |
-
margin: 3px 0 0;
|
| 1703 |
-
color: #5f758b;
|
| 1704 |
-
font-size: 0.8rem;
|
| 1705 |
-
line-height: 1.35;
|
| 1706 |
-
overflow-wrap: anywhere;
|
| 1707 |
}
|
| 1708 |
|
| 1709 |
-
.pesquisa-card-
|
| 1710 |
-
display:
|
| 1711 |
-
|
| 1712 |
-
justify-content: flex-end;
|
| 1713 |
gap: 8px;
|
| 1714 |
-
|
| 1715 |
-
align-self: center;
|
| 1716 |
}
|
| 1717 |
|
| 1718 |
-
.
|
| 1719 |
-
|
| 1720 |
-
--btn-bg-end: #238a40;
|
| 1721 |
-
--btn-border: #1b7435;
|
| 1722 |
-
--btn-shadow-soft: rgba(35, 138, 64, 0.22);
|
| 1723 |
-
--btn-shadow-strong: rgba(35, 138, 64, 0.3);
|
| 1724 |
-
min-width: 74px;
|
| 1725 |
min-height: 32px;
|
| 1726 |
padding: 6px 10px;
|
| 1727 |
font-size: 0.78rem;
|
| 1728 |
letter-spacing: 0.02em;
|
| 1729 |
}
|
| 1730 |
|
| 1731 |
-
.pesquisa-
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
padding: 5px 8px;
|
| 1739 |
-
width: fit-content;
|
| 1740 |
-
max-width: 100%;
|
| 1741 |
-
font-size: 0.8rem;
|
| 1742 |
-
font-weight: 700;
|
| 1743 |
-
color: #48627b;
|
| 1744 |
}
|
| 1745 |
|
| 1746 |
-
.pesquisa-
|
| 1747 |
-
|
| 1748 |
-
|
| 1749 |
-
|
|
|
|
|
|
|
|
|
|
| 1750 |
}
|
| 1751 |
|
| 1752 |
-
.pesquisa-
|
| 1753 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1754 |
}
|
| 1755 |
|
| 1756 |
.pesquisa-card-status-row .status-pill {
|
|
@@ -4546,14 +4531,8 @@ button.btn-download-subtle {
|
|
| 4546 |
scrollbar-width: thin;
|
| 4547 |
}
|
| 4548 |
|
| 4549 |
-
.pesquisa-card-
|
| 4550 |
-
|
| 4551 |
-
}
|
| 4552 |
-
|
| 4553 |
-
.pesquisa-card-controls {
|
| 4554 |
-
width: 100%;
|
| 4555 |
-
justify-content: flex-start;
|
| 4556 |
-
align-self: flex-start;
|
| 4557 |
}
|
| 4558 |
|
| 4559 |
.pesquisa-results-toolbar {
|
|
|
|
| 1675 |
.pesquisa-card-top {
|
| 1676 |
display: flex;
|
| 1677 |
flex-direction: column;
|
| 1678 |
+
gap: 10px;
|
| 1679 |
min-width: 0;
|
| 1680 |
flex: 1 1 auto;
|
| 1681 |
}
|
| 1682 |
|
| 1683 |
+
.pesquisa-card-title {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1684 |
margin: 0;
|
| 1685 |
font-family: 'Sora', sans-serif;
|
| 1686 |
color: #2e4358;
|
| 1687 |
+
font-size: 0.94rem;
|
| 1688 |
line-height: 1.32;
|
| 1689 |
overflow-wrap: anywhere;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1690 |
}
|
| 1691 |
|
| 1692 |
+
.pesquisa-card-actions {
|
| 1693 |
+
display: grid;
|
| 1694 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
| 1695 |
gap: 8px;
|
| 1696 |
+
min-width: 0;
|
|
|
|
| 1697 |
}
|
| 1698 |
|
| 1699 |
+
.pesquisa-card-actions button {
|
| 1700 |
+
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1701 |
min-height: 32px;
|
| 1702 |
padding: 6px 10px;
|
| 1703 |
font-size: 0.78rem;
|
| 1704 |
letter-spacing: 0.02em;
|
| 1705 |
}
|
| 1706 |
|
| 1707 |
+
.btn-pesquisa-map-toggle {
|
| 1708 |
+
--btn-bg-start: #edf4fb;
|
| 1709 |
+
--btn-bg-end: #dfeaf5;
|
| 1710 |
+
--btn-border: #b9ccdf;
|
| 1711 |
+
--btn-shadow-soft: rgba(88, 116, 144, 0.12);
|
| 1712 |
+
--btn-shadow-strong: rgba(88, 116, 144, 0.18);
|
| 1713 |
+
color: #35506a;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1714 |
}
|
| 1715 |
|
| 1716 |
+
.btn-pesquisa-map-toggle.is-selected {
|
| 1717 |
+
--btn-bg-start: #f8bd74;
|
| 1718 |
+
--btn-bg-end: #f1a047;
|
| 1719 |
+
--btn-border: #e28a29;
|
| 1720 |
+
--btn-shadow-soft: rgba(226, 138, 41, 0.18);
|
| 1721 |
+
--btn-shadow-strong: rgba(226, 138, 41, 0.25);
|
| 1722 |
+
color: #fff;
|
| 1723 |
}
|
| 1724 |
|
| 1725 |
+
.btn-pesquisa-open {
|
| 1726 |
+
--btn-bg-start: #2ea94f;
|
| 1727 |
+
--btn-bg-end: #238a40;
|
| 1728 |
+
--btn-border: #1b7435;
|
| 1729 |
+
--btn-shadow-soft: rgba(35, 138, 64, 0.22);
|
| 1730 |
+
--btn-shadow-strong: rgba(35, 138, 64, 0.3);
|
| 1731 |
+
}
|
| 1732 |
+
|
| 1733 |
+
.btn-pesquisa-eval {
|
| 1734 |
+
--btn-bg-start: #4e95cf;
|
| 1735 |
+
--btn-bg-end: #3d82be;
|
| 1736 |
+
--btn-border: #2e6d9f;
|
| 1737 |
+
--btn-shadow-soft: rgba(61, 130, 190, 0.2);
|
| 1738 |
+
--btn-shadow-strong: rgba(61, 130, 190, 0.28);
|
| 1739 |
}
|
| 1740 |
|
| 1741 |
.pesquisa-card-status-row .status-pill {
|
|
|
|
| 4531 |
scrollbar-width: thin;
|
| 4532 |
}
|
| 4533 |
|
| 4534 |
+
.pesquisa-card-actions {
|
| 4535 |
+
grid-template-columns: 1fr;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4536 |
}
|
| 4537 |
|
| 4538 |
.pesquisa-results-toolbar {
|