Guilherme Silberfarb Costa commited on
Commit
440f380
·
1 Parent(s): 2b65468

alteracoes nos mapas

Browse files
backend/app/api/elaboracao.py CHANGED
@@ -173,6 +173,8 @@ class UpdateMapaPayload(SessionPayload):
173
  variavel_mapa: str | None = None
174
  modo_mapa: str | None = None
175
  escala_extremo_abs: float | None = None
 
 
176
 
177
 
178
  class ColunaDataMercadoPayload(SessionPayload):
@@ -564,7 +566,13 @@ def export_base(session_id: str, filtered: bool = True) -> FileResponse:
564
  @router.post("/map/update")
565
  def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
566
  session = session_store.get(payload.session_id)
567
- return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa, payload.modo_mapa)
 
 
 
 
 
 
568
 
569
 
570
  @router.post("/map/popup")
@@ -581,6 +589,8 @@ def residuos_map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
581
  payload.variavel_mapa,
582
  payload.modo_mapa,
583
  payload.escala_extremo_abs,
 
 
584
  )
585
 
586
 
 
173
  variavel_mapa: str | None = None
174
  modo_mapa: str | None = None
175
  escala_extremo_abs: float | None = None
176
+ ponto_raio_min: float | None = None
177
+ ponto_raio_max: float | None = None
178
 
179
 
180
  class ColunaDataMercadoPayload(SessionPayload):
 
566
  @router.post("/map/update")
567
  def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
568
  session = session_store.get(payload.session_id)
569
+ return elaboracao_service.atualizar_mapa(
570
+ session,
571
+ payload.variavel_mapa,
572
+ payload.modo_mapa,
573
+ ponto_raio_min=payload.ponto_raio_min,
574
+ ponto_raio_max=payload.ponto_raio_max,
575
+ )
576
 
577
 
578
  @router.post("/map/popup")
 
589
  payload.variavel_mapa,
590
  payload.modo_mapa,
591
  payload.escala_extremo_abs,
592
+ ponto_raio_min=payload.ponto_raio_min,
593
+ ponto_raio_max=payload.ponto_raio_max,
594
  )
595
 
596
 
backend/app/api/visualizacao.py CHANGED
@@ -27,6 +27,8 @@ class ExibirPayload(SessionPayload):
27
  class MapaPayload(SessionPayload):
28
  variavel_mapa: str | None = None
29
  trabalhos_tecnicos_modelos_modo: str | None = None
 
 
30
 
31
 
32
  class SecaoPayload(SessionPayload):
@@ -146,6 +148,8 @@ def map_update(payload: MapaPayload, request: Request) -> dict[str, Any]:
146
  session,
147
  payload.variavel_mapa,
148
  trabalhos_tecnicos_modelos_modo=payload.trabalhos_tecnicos_modelos_modo,
 
 
149
  api_base_url=str(request.base_url).rstrip("/"),
150
  popup_auth_token=getattr(request.state, "auth_token", None),
151
  )
 
27
  class MapaPayload(SessionPayload):
28
  variavel_mapa: str | None = None
29
  trabalhos_tecnicos_modelos_modo: str | None = None
30
+ ponto_raio_min: float | None = None
31
+ ponto_raio_max: float | None = None
32
 
33
 
34
  class SecaoPayload(SessionPayload):
 
148
  session,
149
  payload.variavel_mapa,
150
  trabalhos_tecnicos_modelos_modo=payload.trabalhos_tecnicos_modelos_modo,
151
+ ponto_raio_min=payload.ponto_raio_min,
152
+ ponto_raio_max=payload.ponto_raio_max,
153
  api_base_url=str(request.base_url).rstrip("/"),
154
  popup_auth_token=getattr(request.state, "auth_token", None),
155
  )
backend/app/core/elaboracao/charts.py CHANGED
@@ -24,6 +24,7 @@ from app.core.map_layers import (
24
  add_popup_pagination_handlers,
25
  add_zoom_responsive_circle_markers,
26
  )
 
27
 
28
  # ============================================================
29
  # CONSTANTES DE ESTILO
@@ -963,6 +964,8 @@ def criar_mapa(
963
  cor_stops: list[float] | None = None,
964
  cor_tick_values: list[float] | None = None,
965
  cor_tick_labels: list[str] | None = None,
 
 
966
  ):
967
  """
968
  Cria mapa Folium com os dados.
@@ -1087,7 +1090,7 @@ def criar_mapa(
1087
  colormap.add_to(m)
1088
 
1089
  # Escala de tamanho proporcional
1090
- raio_min, raio_max = 3, 18
1091
  tamanho_func = None
1092
  if tamanho_col and tamanho_col in df_mapa.columns:
1093
  t_min = df_mapa[tamanho_col].min()
@@ -1102,7 +1105,8 @@ def criar_mapa(
1102
  camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
1103
  lat_plot_col = "__mesa_lat_plot__"
1104
  lon_plot_col = "__mesa_lon_plot__"
1105
- if not modo_calor and not modo_superficie:
 
1106
  df_plot_pontos = _aplicar_jitter_sobrepostos(
1107
  df_mapa,
1108
  lat_col=lat_real,
@@ -1307,13 +1311,19 @@ def criar_mapa(
1307
 
1308
  # Controles
1309
  folium.LayerControl().add_to(m)
1310
- plugins.Fullscreen().add_to(m)
1311
  plugins.MeasureControl(
 
1312
  primary_length_unit='meters',
1313
  secondary_length_unit='kilometers',
1314
  primary_area_unit='sqmeters',
1315
  secondary_area_unit='hectares'
1316
  ).add_to(m)
 
 
 
 
 
 
1317
  add_zoom_responsive_circle_markers(m)
1318
  add_popup_pagination_handlers(m)
1319
 
 
24
  add_popup_pagination_handlers,
25
  add_zoom_responsive_circle_markers,
26
  )
27
+ from app.core.map_sizing import normalizar_faixa_raio_pontos
28
 
29
  # ============================================================
30
  # CONSTANTES DE ESTILO
 
964
  cor_stops: list[float] | None = None,
965
  cor_tick_values: list[float] | None = None,
966
  cor_tick_labels: list[str] | None = None,
967
+ ponto_raio_min: float | None = None,
968
+ ponto_raio_max: float | None = None,
969
  ):
970
  """
971
  Cria mapa Folium com os dados.
 
1090
  colormap.add_to(m)
1091
 
1092
  # Escala de tamanho proporcional
1093
+ raio_min, raio_max = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
1094
  tamanho_func = None
1095
  if tamanho_col and tamanho_col in df_mapa.columns:
1096
  t_min = df_mapa[tamanho_col].min()
 
1105
  camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
1106
  lat_plot_col = "__mesa_lat_plot__"
1107
  lon_plot_col = "__mesa_lon_plot__"
1108
+ usar_jitter_pontos = bool(tamanho_col and tamanho_col in df_mapa.columns)
1109
+ if usar_jitter_pontos and not modo_calor and not modo_superficie:
1110
  df_plot_pontos = _aplicar_jitter_sobrepostos(
1111
  df_mapa,
1112
  lat_col=lat_real,
 
1311
 
1312
  # Controles
1313
  folium.LayerControl().add_to(m)
 
1314
  plugins.MeasureControl(
1315
+ position='topright',
1316
  primary_length_unit='meters',
1317
  secondary_length_unit='kilometers',
1318
  primary_area_unit='sqmeters',
1319
  secondary_area_unit='hectares'
1320
  ).add_to(m)
1321
+ plugins.Fullscreen(
1322
+ position="topright",
1323
+ title="Abrir mapa em tela cheia",
1324
+ title_cancel="Sair da tela cheia",
1325
+ force_separate_button=True,
1326
+ ).add_to(m)
1327
  add_zoom_responsive_circle_markers(m)
1328
  add_popup_pagination_handlers(m)
1329
 
backend/app/core/map_jitter.py CHANGED
@@ -28,8 +28,8 @@ def aplicar_jitter_dados_mercado(
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
 
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").astype(float)
32
+ df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce").astype(float)
33
 
34
  if len(df_plot) <= 1:
35
  return df_plot
backend/app/core/map_sizing.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any
5
+
6
+
7
+ PONTO_RAIO_MIN_PADRAO = 3.0
8
+ PONTO_RAIO_MAX_PADRAO = 18.0
9
+ PONTO_RAIO_MIN_LIMITE = 1.0
10
+ PONTO_RAIO_MAX_LIMITE = 64.0
11
+
12
+
13
+ def _normalizar_numero(value: Any, fallback: float) -> float:
14
+ try:
15
+ number = float(value)
16
+ except (TypeError, ValueError):
17
+ return float(fallback)
18
+ if not math.isfinite(number):
19
+ return float(fallback)
20
+ return float(number)
21
+
22
+
23
+ def normalizar_faixa_raio_pontos(
24
+ raio_min: Any = None,
25
+ raio_max: Any = None,
26
+ ) -> tuple[float, float]:
27
+ min_norm = _normalizar_numero(raio_min, PONTO_RAIO_MIN_PADRAO)
28
+ max_norm = _normalizar_numero(raio_max, PONTO_RAIO_MAX_PADRAO)
29
+ min_norm = min(max(min_norm, PONTO_RAIO_MIN_LIMITE), PONTO_RAIO_MAX_LIMITE)
30
+ max_norm = min(max(max_norm, PONTO_RAIO_MIN_LIMITE), PONTO_RAIO_MAX_LIMITE)
31
+ if min_norm > max_norm:
32
+ min_norm, max_norm = max_norm, min_norm
33
+ return float(min_norm), float(max_norm)
backend/app/core/visualizacao/app.py CHANGED
@@ -23,6 +23,7 @@ from app.core.map_layers import (
23
  add_trabalhos_tecnicos_markers,
24
  add_zoom_responsive_circle_markers,
25
  )
 
26
 
27
 
28
  COR_PRINCIPAL = "#FF8C00"
@@ -724,6 +725,8 @@ def criar_mapa(
724
  popup_endpoint=None,
725
  popup_auth_token=None,
726
  avaliandos_tecnicos=None,
 
 
727
  ):
728
  lat_real = None
729
  lon_real = None
@@ -799,7 +802,7 @@ def criar_mapa(
799
  )
800
  colormap.add_to(mapa)
801
 
802
- raio_min, raio_max = 3, 18
803
  tamanho_func = None
804
  tamanho_key = None
805
  if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
@@ -819,13 +822,23 @@ def criar_mapa(
819
  camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
820
  lat_plot_key = "__mesa_lat_plot__"
821
  lon_plot_key = "__mesa_lon_plot__"
822
- df_plot_pontos = _aplicar_jitter_sobrepostos(
823
- df_mapa,
824
- lat_col=lat_key,
825
- lon_col=lon_key,
826
- lat_plot_col=lat_plot_key,
827
- lon_plot_col=lon_plot_key,
828
  )
 
 
 
 
 
 
 
 
 
 
 
 
829
 
830
  tooltip_col = None
831
  tooltip_key = None
@@ -916,18 +929,29 @@ def criar_mapa(
916
  camada_indices.add_to(mapa)
917
 
918
  if avaliandos_tecnicos:
919
- camada_trabalhos_tecnicos = folium.FeatureGroup(name="Trabalhos tecnicos", show=True)
 
 
 
 
 
920
  add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
921
  camada_trabalhos_tecnicos.add_to(mapa)
922
 
923
  folium.LayerControl().add_to(mapa)
924
- plugins.Fullscreen().add_to(mapa)
925
  plugins.MeasureControl(
 
926
  primary_length_unit="meters",
927
  secondary_length_unit="kilometers",
928
  primary_area_unit="sqmeters",
929
  secondary_area_unit="hectares",
930
  ).add_to(mapa)
 
 
 
 
 
 
931
  add_zoom_responsive_circle_markers(mapa)
932
  add_popup_pagination_handlers(mapa)
933
 
 
23
  add_trabalhos_tecnicos_markers,
24
  add_zoom_responsive_circle_markers,
25
  )
26
+ from app.core.map_sizing import normalizar_faixa_raio_pontos
27
 
28
 
29
  COR_PRINCIPAL = "#FF8C00"
 
725
  popup_endpoint=None,
726
  popup_auth_token=None,
727
  avaliandos_tecnicos=None,
728
+ ponto_raio_min=None,
729
+ ponto_raio_max=None,
730
  ):
731
  lat_real = None
732
  lon_real = None
 
802
  )
803
  colormap.add_to(mapa)
804
 
805
+ raio_min, raio_max = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
806
  tamanho_func = None
807
  tamanho_key = None
808
  if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
 
822
  camada_indices = folium.FeatureGroup(name="Índices", show=False) if mostrar_indices else None
823
  lat_plot_key = "__mesa_lat_plot__"
824
  lon_plot_key = "__mesa_lon_plot__"
825
+ usar_jitter_pontos = bool(
826
+ tamanho_col
827
+ and tamanho_col != "Visualização Padrão"
828
+ and tamanho_col in df_mapa.columns
 
 
829
  )
830
+ if usar_jitter_pontos:
831
+ df_plot_pontos = _aplicar_jitter_sobrepostos(
832
+ df_mapa,
833
+ lat_col=lat_key,
834
+ lon_col=lon_key,
835
+ lat_plot_col=lat_plot_key,
836
+ lon_plot_col=lon_plot_key,
837
+ )
838
+ else:
839
+ df_plot_pontos = df_mapa.copy()
840
+ df_plot_pontos[lat_plot_key] = df_plot_pontos[lat_key]
841
+ df_plot_pontos[lon_plot_key] = df_plot_pontos[lon_key]
842
 
843
  tooltip_col = None
844
  tooltip_key = None
 
929
  camada_indices.add_to(mapa)
930
 
931
  if avaliandos_tecnicos:
932
+ trabalhos_tecnicos_show = not (
933
+ tamanho_col
934
+ and tamanho_col != "Visualização Padrão"
935
+ and tamanho_col in df_mapa.columns
936
+ )
937
+ camada_trabalhos_tecnicos = folium.FeatureGroup(name="Trabalhos tecnicos", show=trabalhos_tecnicos_show)
938
  add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
939
  camada_trabalhos_tecnicos.add_to(mapa)
940
 
941
  folium.LayerControl().add_to(mapa)
 
942
  plugins.MeasureControl(
943
+ position="topright",
944
  primary_length_unit="meters",
945
  secondary_length_unit="kilometers",
946
  primary_area_unit="sqmeters",
947
  secondary_area_unit="hectares",
948
  ).add_to(mapa)
949
+ plugins.Fullscreen(
950
+ position="topright",
951
+ title="Abrir mapa em tela cheia",
952
+ title_cancel="Sair da tela cheia",
953
+ force_separate_button=True,
954
+ ).add_to(mapa)
955
  add_zoom_responsive_circle_markers(mapa)
956
  add_popup_pagination_handlers(mapa)
957
 
backend/app/core/visualizacao/map_payload.py CHANGED
@@ -13,7 +13,9 @@ from app.core.elaboracao.charts import (
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
 
@@ -264,6 +266,8 @@ def build_elaboracao_map_payload(
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
 
@@ -358,7 +362,7 @@ def build_elaboracao_map_payload(
358
  except (TypeError, ValueError):
359
  pass
360
 
361
- raio_min, raio_max = 3.0, 18.0
362
  tamanho_func = None
363
  if tamanho_col and tamanho_col in df_mapa.columns:
364
  serie_tamanho = pd.to_numeric(df_mapa[tamanho_col], errors="coerce")
@@ -376,10 +380,15 @@ def build_elaboracao_map_payload(
376
  mostrar_indices = not modo_calor and not modo_superficie and len(df_mapa) <= 800
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]
@@ -586,8 +595,10 @@ def build_elaboracao_map_payload(
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()):
@@ -756,6 +767,8 @@ def build_visualizacao_map_payload(
756
  col_y: str | None = None,
757
  avaliandos_tecnicos: list[dict[str, Any]] | None = None,
758
  bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
 
 
759
  ) -> dict[str, Any] | None:
760
  lat_real = _detectar_coluna(df, _LAT_ALIASES)
761
  lon_real = _detectar_coluna(df, _LON_ALIASES)
@@ -788,6 +801,7 @@ def build_visualizacao_map_payload(
788
  cor_col_resolvida = cor_col
789
  if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col_resolvida:
790
  cor_col_resolvida = tamanho_col
 
791
 
792
  colormap = None
793
  cor_key = None
@@ -813,7 +827,7 @@ def build_visualizacao_map_payload(
813
  "colors": ["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
814
  }
815
 
816
- raio_min, raio_max = 3.0, 18.0
817
  tamanho_func = None
818
  tamanho_key = None
819
  if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
@@ -835,9 +849,23 @@ def build_visualizacao_map_payload(
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,8 +884,10 @@ def build_visualizacao_map_payload(
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()):
@@ -969,5 +999,6 @@ def build_visualizacao_map_payload(
969
  "bairros_geojson_url": bairros_geojson_url,
970
  "legend": legend,
971
  "market_points": market_points,
 
972
  "trabalhos_tecnicos_points": build_trabalhos_tecnicos_marker_payloads(avaliandos_tecnicos),
973
  }
 
13
  _mascara_dentro_poligono,
14
  _normalizar_stops_cor,
15
  )
16
+ from app.core.map_jitter import aplicar_jitter_dados_mercado
17
  from app.core.map_layers import build_trabalhos_tecnicos_marker_payloads
18
+ from app.core.map_sizing import normalizar_faixa_raio_pontos
19
  from app.core.visualizacao.app import COR_PRINCIPAL, formatar_monetario
20
 
21
 
 
266
  cor_tick_labels: list[str] | None = None,
267
  bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
268
  popup_source: str | None = "mercado",
269
+ ponto_raio_min: float | None = None,
270
+ ponto_raio_max: float | None = None,
271
  ) -> dict[str, Any] | None:
272
  modo_normalizado = str(modo or "pontos").strip().lower()
273
 
 
362
  except (TypeError, ValueError):
363
  pass
364
 
365
+ raio_min, raio_max = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
366
  tamanho_func = None
367
  if tamanho_col and tamanho_col in df_mapa.columns:
368
  serie_tamanho = pd.to_numeric(df_mapa[tamanho_col], errors="coerce")
 
380
  mostrar_indices = not modo_calor and not modo_superficie and len(df_mapa) <= 800
381
  lat_plot_col = "__mesa_lat_plot__"
382
  lon_plot_col = "__mesa_lon_plot__"
383
+ usar_jitter_pontos = bool(tamanho_col and tamanho_col in df_mapa.columns)
384
+ if usar_jitter_pontos and not modo_calor and not modo_superficie:
385
+ df_plot_pontos = aplicar_jitter_dados_mercado(
386
+ df_mapa,
387
+ lat_col=lat_real,
388
+ lon_col=lon_real,
389
+ lat_plot_col=lat_plot_col,
390
+ lon_plot_col=lon_plot_col,
391
+ )
392
  else:
393
  df_plot_pontos = df_mapa.copy()
394
  df_plot_pontos[lat_plot_col] = df_plot_pontos[lat_real]
 
595
  else:
596
  market_points: list[dict[str, Any]] = []
597
  indices_markers: list[dict[str, Any]] = []
598
+ grupo_lat_col = lat_plot_col if usar_jitter_pontos else lat_real
599
+ grupo_lon_col = lon_plot_col if usar_jitter_pontos else lon_real
600
  grupos_coord = df_plot_pontos.groupby(
601
+ [df_plot_pontos[grupo_lat_col].round(7), df_plot_pontos[grupo_lon_col].round(7)],
602
  sort=False,
603
  ).indices
604
  for marker_ordem, posicoes_raw in enumerate(grupos_coord.values()):
 
767
  col_y: str | None = None,
768
  avaliandos_tecnicos: list[dict[str, Any]] | None = None,
769
  bairros_geojson_url: str = "/api/visualizacao/map/bairros.geojson",
770
+ ponto_raio_min: float | None = None,
771
+ ponto_raio_max: float | None = None,
772
  ) -> dict[str, Any] | None:
773
  lat_real = _detectar_coluna(df, _LAT_ALIASES)
774
  lon_real = _detectar_coluna(df, _LON_ALIASES)
 
801
  cor_col_resolvida = cor_col
802
  if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col_resolvida:
803
  cor_col_resolvida = tamanho_col
804
+ trabalhos_tecnicos_show = not (tamanho_col and tamanho_col != "Visualização Padrão")
805
 
806
  colormap = None
807
  cor_key = None
 
827
  "colors": ["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
828
  }
829
 
830
+ raio_min, raio_max = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
831
  tamanho_func = None
832
  tamanho_key = None
833
  if tamanho_col and tamanho_col != "Visualização Padrão" and tamanho_col in df_mapa.columns:
 
849
  show_indices = False
850
  lat_plot_key = "__mesa_lat_plot__"
851
  lon_plot_key = "__mesa_lon_plot__"
852
+ usar_jitter_pontos = bool(
853
+ tamanho_col
854
+ and tamanho_col != "Visualização Padrão"
855
+ and tamanho_col in df_mapa.columns
856
+ )
857
+ if usar_jitter_pontos:
858
+ df_plot_pontos = aplicar_jitter_dados_mercado(
859
+ df_mapa,
860
+ lat_col=lat_key,
861
+ lon_col=lon_key,
862
+ lat_plot_col=lat_plot_key,
863
+ lon_plot_col=lon_plot_key,
864
+ )
865
+ else:
866
+ df_plot_pontos = df_mapa.copy()
867
+ df_plot_pontos[lat_plot_key] = df_plot_pontos[lat_key]
868
+ df_plot_pontos[lon_plot_key] = df_plot_pontos[lon_key]
869
 
870
  tooltip_col = None
871
  tooltip_key = None
 
884
  raio_padrao = 4.0 if total_pontos_plot <= 2500 else 3.0
885
 
886
  market_points: list[dict[str, Any]] = []
887
+ grupo_lat_key = lat_plot_key if usar_jitter_pontos else lat_key
888
+ grupo_lon_key = lon_plot_key if usar_jitter_pontos else lon_key
889
  grupos_coord = df_plot_pontos.groupby(
890
+ [df_plot_pontos[grupo_lat_key].round(7), df_plot_pontos[grupo_lon_key].round(7)],
891
  sort=False,
892
  ).indices
893
  for marker_ordem, posicoes_raw in enumerate(grupos_coord.values()):
 
999
  "bairros_geojson_url": bairros_geojson_url,
1000
  "legend": legend,
1001
  "market_points": market_points,
1002
+ "trabalhos_tecnicos_show": trabalhos_tecnicos_show,
1003
  "trabalhos_tecnicos_points": build_trabalhos_tecnicos_marker_payloads(avaliandos_tecnicos),
1004
  }
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 import app as visualizacao_app
58
  from app.core.visualizacao.map_payload import build_elaboracao_map_payload
59
  from app.models.session import SessionState
@@ -3134,7 +3135,14 @@ def exportar_equacao(session: SessionState, mode: str) -> tuple[str, str]:
3134
  return caminho, nome
3135
 
3136
 
3137
- def atualizar_mapa(session: SessionState, var_mapa: str | None, modo_mapa: str | None = None) -> dict[str, Any]:
 
 
 
 
 
 
 
3138
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
3139
  if df is None:
3140
  raise HTTPException(status_code=400, detail="Carregue dados primeiro")
@@ -3144,9 +3152,27 @@ def atualizar_mapa(session: SessionState, var_mapa: str | None, modo_mapa: str |
3144
  if tamanho_col is None:
3145
  modo = "pontos"
3146
  session.mapa_habilitado = True
3147
- mapa_html = charts.criar_mapa(df, tamanho_col=tamanho_col, modo=modo)
3148
- mapa_payload = build_elaboracao_map_payload(df, tamanho_col=tamanho_col, modo=modo)
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]:
@@ -3221,6 +3247,9 @@ def atualizar_mapa_residuos(
3221
  var_mapa: str | None,
3222
  modo_mapa: str | None = None,
3223
  escala_extremo_abs: float | None = None,
 
 
 
3224
  ) -> dict[str, Any]:
3225
  tabela_metricas = session.tabela_metricas_estado
3226
  if tabela_metricas is None or tabela_metricas.empty:
@@ -3246,6 +3275,7 @@ def atualizar_mapa_residuos(
3246
  if extremo_abs is None
3247
  else f"Resíduo Pad. (escala fixa -{extremo_txt} a +{extremo_txt})"
3248
  )
 
3249
 
3250
  mapa_html = charts.criar_mapa(
3251
  df,
@@ -3257,6 +3287,8 @@ def atualizar_mapa_residuos(
3257
  cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
3258
  cor_tick_values=ticks_valores,
3259
  cor_tick_labels=ticks_labels,
 
 
3260
  )
3261
  mapa_payload = build_elaboracao_map_payload(
3262
  df,
@@ -3269,6 +3301,8 @@ def atualizar_mapa_residuos(
3269
  cor_tick_values=ticks_valores,
3270
  cor_tick_labels=ticks_labels,
3271
  popup_source="residuos",
 
 
3272
  )
3273
  return {
3274
  "mapa_html": mapa_html,
@@ -3276,6 +3310,8 @@ def atualizar_mapa_residuos(
3276
  "variavel_mapa": var_escolhida,
3277
  "modo_mapa": modo,
3278
  "escala_extremo_abs": extremo_abs,
 
 
3279
  }
3280
 
3281
 
 
54
  formatar_micronumerosidade_html,
55
  formatar_outliers_anteriores_html,
56
  )
57
+ from app.core.map_sizing import normalizar_faixa_raio_pontos
58
  from app.core.visualizacao import app as visualizacao_app
59
  from app.core.visualizacao.map_payload import build_elaboracao_map_payload
60
  from app.models.session import SessionState
 
3135
  return caminho, nome
3136
 
3137
 
3138
+ def atualizar_mapa(
3139
+ session: SessionState,
3140
+ var_mapa: str | None,
3141
+ modo_mapa: str | None = None,
3142
+ *,
3143
+ ponto_raio_min: float | None = None,
3144
+ ponto_raio_max: float | None = None,
3145
+ ) -> dict[str, Any]:
3146
  df = session.df_filtrado if session.df_filtrado is not None else session.df_original
3147
  if df is None:
3148
  raise HTTPException(status_code=400, detail="Carregue dados primeiro")
 
3152
  if tamanho_col is None:
3153
  modo = "pontos"
3154
  session.mapa_habilitado = True
3155
+ ponto_raio_min_norm, ponto_raio_max_norm = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
3156
+ mapa_html = charts.criar_mapa(
3157
+ df,
3158
+ tamanho_col=tamanho_col,
3159
+ modo=modo,
3160
+ ponto_raio_min=ponto_raio_min_norm,
3161
+ ponto_raio_max=ponto_raio_max_norm,
3162
+ )
3163
+ mapa_payload = build_elaboracao_map_payload(
3164
+ df,
3165
+ tamanho_col=tamanho_col,
3166
+ modo=modo,
3167
+ ponto_raio_min=ponto_raio_min_norm,
3168
+ ponto_raio_max=ponto_raio_max_norm,
3169
+ )
3170
+ return {
3171
+ "mapa_html": mapa_html,
3172
+ "mapa_payload": mapa_payload,
3173
+ "ponto_raio_min": ponto_raio_min_norm,
3174
+ "ponto_raio_max": ponto_raio_max_norm,
3175
+ }
3176
 
3177
 
3178
  def carregar_popup_ponto_mapa(session: SessionState, row_id: int, source: str | None = None) -> dict[str, Any]:
 
3247
  var_mapa: str | None,
3248
  modo_mapa: str | None = None,
3249
  escala_extremo_abs: float | None = None,
3250
+ *,
3251
+ ponto_raio_min: float | None = None,
3252
+ ponto_raio_max: float | None = None,
3253
  ) -> dict[str, Any]:
3254
  tabela_metricas = session.tabela_metricas_estado
3255
  if tabela_metricas is None or tabela_metricas.empty:
 
3275
  if extremo_abs is None
3276
  else f"Resíduo Pad. (escala fixa -{extremo_txt} a +{extremo_txt})"
3277
  )
3278
+ ponto_raio_min_norm, ponto_raio_max_norm = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
3279
 
3280
  mapa_html = charts.criar_mapa(
3281
  df,
 
3287
  cor_colors=["#2e7d32", "#f1c40f", "#ffffff", "#f1c40f", "#c62828"],
3288
  cor_tick_values=ticks_valores,
3289
  cor_tick_labels=ticks_labels,
3290
+ ponto_raio_min=ponto_raio_min_norm,
3291
+ ponto_raio_max=ponto_raio_max_norm,
3292
  )
3293
  mapa_payload = build_elaboracao_map_payload(
3294
  df,
 
3301
  cor_tick_values=ticks_valores,
3302
  cor_tick_labels=ticks_labels,
3303
  popup_source="residuos",
3304
+ ponto_raio_min=ponto_raio_min_norm,
3305
+ ponto_raio_max=ponto_raio_max_norm,
3306
  )
3307
  return {
3308
  "mapa_html": mapa_html,
 
3310
  "variavel_mapa": var_escolhida,
3311
  "modo_mapa": modo,
3312
  "escala_extremo_abs": extremo_abs,
3313
+ "ponto_raio_min": ponto_raio_min_norm,
3314
+ "ponto_raio_max": ponto_raio_max_norm,
3315
  }
3316
 
3317
 
backend/app/services/pesquisa_service.py CHANGED
@@ -1729,10 +1729,22 @@ def _renderizar_mapa_modelos(
1729
  if avaliandos_geo:
1730
  camada_avaliando.add_to(mapa)
1731
 
1732
- plugins.Fullscreen().add_to(mapa)
1733
  add_zoom_responsive_circle_markers(mapa)
1734
  layer_control = folium.LayerControl(collapsed=False)
1735
  layer_control.add_to(mapa)
 
 
 
 
 
 
 
 
 
 
 
 
 
1736
  _adicionar_interacao_hover_legenda_modelos(mapa, layer_control, modelos_plotados)
1737
  if bounds:
1738
  lat_values = [float(coord[0]) for coord in bounds]
 
1729
  if avaliandos_geo:
1730
  camada_avaliando.add_to(mapa)
1731
 
 
1732
  add_zoom_responsive_circle_markers(mapa)
1733
  layer_control = folium.LayerControl(collapsed=False)
1734
  layer_control.add_to(mapa)
1735
+ plugins.MeasureControl(
1736
+ position="topright",
1737
+ primary_length_unit="meters",
1738
+ secondary_length_unit="kilometers",
1739
+ primary_area_unit="sqmeters",
1740
+ secondary_area_unit="hectares",
1741
+ ).add_to(mapa)
1742
+ plugins.Fullscreen(
1743
+ position="topright",
1744
+ title="Abrir mapa em tela cheia",
1745
+ title_cancel="Sair da tela cheia",
1746
+ force_separate_button=True,
1747
+ ).add_to(mapa)
1748
  _adicionar_interacao_hover_legenda_modelos(mapa, layer_control, modelos_plotados)
1749
  if bounds:
1750
  lat_values = [float(coord[0]) for coord in bounds]
backend/app/services/trabalhos_tecnicos_service.py CHANGED
@@ -1597,7 +1597,19 @@ def gerar_mapa_trabalhos(
1597
  ).add_to(mapa)
1598
 
1599
  folium.LayerControl(collapsed=False).add_to(mapa)
1600
- plugins.Fullscreen().add_to(mapa)
 
 
 
 
 
 
 
 
 
 
 
 
1601
  add_zoom_responsive_circle_markers(mapa)
1602
  add_popup_pagination_handlers(mapa)
1603
 
@@ -1696,7 +1708,19 @@ def _criar_mapa_trabalho(nome_trabalho: str, imoveis: list[dict[str, Any]]) -> s
1696
  ).add_to(camada)
1697
  camada.add_to(mapa)
1698
  folium.LayerControl().add_to(mapa)
1699
- plugins.Fullscreen().add_to(mapa)
 
 
 
 
 
 
 
 
 
 
 
 
1700
  add_zoom_responsive_circle_markers(mapa)
1701
  add_popup_pagination_handlers(mapa)
1702
 
 
1597
  ).add_to(mapa)
1598
 
1599
  folium.LayerControl(collapsed=False).add_to(mapa)
1600
+ plugins.MeasureControl(
1601
+ position="topright",
1602
+ primary_length_unit="meters",
1603
+ secondary_length_unit="kilometers",
1604
+ primary_area_unit="sqmeters",
1605
+ secondary_area_unit="hectares",
1606
+ ).add_to(mapa)
1607
+ plugins.Fullscreen(
1608
+ position="topright",
1609
+ title="Abrir mapa em tela cheia",
1610
+ title_cancel="Sair da tela cheia",
1611
+ force_separate_button=True,
1612
+ ).add_to(mapa)
1613
  add_zoom_responsive_circle_markers(mapa)
1614
  add_popup_pagination_handlers(mapa)
1615
 
 
1708
  ).add_to(camada)
1709
  camada.add_to(mapa)
1710
  folium.LayerControl().add_to(mapa)
1711
+ plugins.MeasureControl(
1712
+ position="topright",
1713
+ primary_length_unit="meters",
1714
+ secondary_length_unit="kilometers",
1715
+ primary_area_unit="sqmeters",
1716
+ secondary_area_unit="hectares",
1717
+ ).add_to(mapa)
1718
+ plugins.Fullscreen(
1719
+ position="topright",
1720
+ title="Abrir mapa em tela cheia",
1721
+ title_cancel="Sair da tela cheia",
1722
+ force_separate_button=True,
1723
+ ).add_to(mapa)
1724
  add_zoom_responsive_circle_markers(mapa)
1725
  add_popup_pagination_handlers(mapa)
1726
 
backend/app/services/visualizacao_service.py CHANGED
@@ -23,6 +23,7 @@ from app.core.map_layers import (
23
  add_zoom_responsive_circle_markers,
24
  get_bairros_geojson,
25
  )
 
26
  from app.core.elaboracao.core import (
27
  PERCENTUAL_RUIDO_TOL,
28
  _migrar_pacote_v1_para_v2,
@@ -397,7 +398,19 @@ def _criar_mapa_knn_destaque(
397
  if camada_avaliando is not None:
398
  camada_avaliando.add_to(mapa)
399
  folium.LayerControl().add_to(mapa)
400
- plugins.Fullscreen().add_to(mapa)
 
 
 
 
 
 
 
 
 
 
 
 
401
  add_zoom_responsive_circle_markers(mapa)
402
  add_popup_pagination_handlers(mapa)
403
 
@@ -1414,6 +1427,8 @@ def atualizar_mapa(
1414
  session: SessionState,
1415
  variavel_mapa: str | None,
1416
  trabalhos_tecnicos_modelos_modo: Any = None,
 
 
1417
  api_base_url: str | None = None,
1418
  popup_auth_token: str | None = None,
1419
  ) -> dict[str, Any]:
@@ -1427,6 +1442,7 @@ def atualizar_mapa(
1427
  if variavel_mapa and variavel_mapa != "Visualização Padrão":
1428
  tamanho_col = variavel_mapa
1429
 
 
1430
  avaliandos_tecnicos, trabalhos_tecnicos, trabalhos_tecnicos_modelos_modo_norm = _carregar_trabalhos_tecnicos_visualizacao(
1431
  session,
1432
  trabalhos_tecnicos_modelos_modo,
@@ -1442,6 +1458,8 @@ def atualizar_mapa(
1442
  col_y=info["nome_y"],
1443
  avaliandos_tecnicos=avaliandos_tecnicos,
1444
  bairros_geojson_url=bairros_geojson_url,
 
 
1445
  )
1446
  mapa_html = ""
1447
  if mapa_payload is None:
@@ -1454,12 +1472,16 @@ def atualizar_mapa(
1454
  popup_endpoint=popup_endpoint,
1455
  popup_auth_token=popup_auth_token,
1456
  avaliandos_tecnicos=avaliandos_tecnicos,
 
 
1457
  )
1458
  return {
1459
  "mapa_html": mapa_html,
1460
  "mapa_payload": mapa_payload,
1461
  "trabalhos_tecnicos": trabalhos_tecnicos,
1462
  "trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
 
 
1463
  }
1464
 
1465
 
 
23
  add_zoom_responsive_circle_markers,
24
  get_bairros_geojson,
25
  )
26
+ from app.core.map_sizing import normalizar_faixa_raio_pontos
27
  from app.core.elaboracao.core import (
28
  PERCENTUAL_RUIDO_TOL,
29
  _migrar_pacote_v1_para_v2,
 
398
  if camada_avaliando is not None:
399
  camada_avaliando.add_to(mapa)
400
  folium.LayerControl().add_to(mapa)
401
+ plugins.MeasureControl(
402
+ position="topright",
403
+ primary_length_unit="meters",
404
+ secondary_length_unit="kilometers",
405
+ primary_area_unit="sqmeters",
406
+ secondary_area_unit="hectares",
407
+ ).add_to(mapa)
408
+ plugins.Fullscreen(
409
+ position="topright",
410
+ title="Abrir mapa em tela cheia",
411
+ title_cancel="Sair da tela cheia",
412
+ force_separate_button=True,
413
+ ).add_to(mapa)
414
  add_zoom_responsive_circle_markers(mapa)
415
  add_popup_pagination_handlers(mapa)
416
 
 
1427
  session: SessionState,
1428
  variavel_mapa: str | None,
1429
  trabalhos_tecnicos_modelos_modo: Any = None,
1430
+ ponto_raio_min: float | None = None,
1431
+ ponto_raio_max: float | None = None,
1432
  api_base_url: str | None = None,
1433
  popup_auth_token: str | None = None,
1434
  ) -> dict[str, Any]:
 
1442
  if variavel_mapa and variavel_mapa != "Visualização Padrão":
1443
  tamanho_col = variavel_mapa
1444
 
1445
+ ponto_raio_min_norm, ponto_raio_max_norm = normalizar_faixa_raio_pontos(ponto_raio_min, ponto_raio_max)
1446
  avaliandos_tecnicos, trabalhos_tecnicos, trabalhos_tecnicos_modelos_modo_norm = _carregar_trabalhos_tecnicos_visualizacao(
1447
  session,
1448
  trabalhos_tecnicos_modelos_modo,
 
1458
  col_y=info["nome_y"],
1459
  avaliandos_tecnicos=avaliandos_tecnicos,
1460
  bairros_geojson_url=bairros_geojson_url,
1461
+ ponto_raio_min=ponto_raio_min_norm,
1462
+ ponto_raio_max=ponto_raio_max_norm,
1463
  )
1464
  mapa_html = ""
1465
  if mapa_payload is None:
 
1472
  popup_endpoint=popup_endpoint,
1473
  popup_auth_token=popup_auth_token,
1474
  avaliandos_tecnicos=avaliandos_tecnicos,
1475
+ ponto_raio_min=ponto_raio_min_norm,
1476
+ ponto_raio_max=ponto_raio_max_norm,
1477
  )
1478
  return {
1479
  "mapa_html": mapa_html,
1480
  "mapa_payload": mapa_payload,
1481
  "trabalhos_tecnicos": trabalhos_tecnicos,
1482
  "trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
1483
+ "ponto_raio_min": ponto_raio_min_norm,
1484
+ "ponto_raio_max": ponto_raio_max_norm,
1485
  }
1486
 
1487
 
frontend/src/api.js CHANGED
@@ -347,16 +347,20 @@ export const api = {
347
  confirmar_substituicao: confirmarSubstituicao,
348
  }),
349
  exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
350
- updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos') => postJson('/api/elaboracao/map/update', {
351
  session_id: sessionId,
352
  variavel_mapa: variavelMapa,
353
  modo_mapa: modoMapa,
 
 
354
  }),
355
- updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos', escalaExtremoAbs = null) => postJson('/api/elaboracao/residuos/map/update', {
356
  session_id: sessionId,
357
  variavel_mapa: variavelMapa,
358
  modo_mapa: modoMapa,
359
  escala_extremo_abs: escalaExtremoAbs,
 
 
360
  }),
361
  previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }),
362
  applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }),
@@ -376,10 +380,12 @@ export const api = {
376
  trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
377
  }),
378
  evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }),
379
- updateVisualizacaoMap: (sessionId, variavelMapa, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes') => postJson('/api/visualizacao/map/update', {
380
  session_id: sessionId,
381
  variavel_mapa: variavelMapa,
382
  trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
 
 
383
  }),
384
  evaluationCalculateViz: (sessionId, valoresX, indiceBase, avaliando = null) => postJson('/api/visualizacao/evaluation/calculate', {
385
  session_id: sessionId,
 
347
  confirmar_substituicao: confirmarSubstituicao,
348
  }),
349
  exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
350
+ updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos', pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/elaboracao/map/update', {
351
  session_id: sessionId,
352
  variavel_mapa: variavelMapa,
353
  modo_mapa: modoMapa,
354
+ ponto_raio_min: pontoRaioMin,
355
+ ponto_raio_max: pontoRaioMax,
356
  }),
357
+ updateElaboracaoResiduosMap: (sessionId, variavelMapa, modoMapa = 'pontos', escalaExtremoAbs = null, pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/elaboracao/residuos/map/update', {
358
  session_id: sessionId,
359
  variavel_mapa: variavelMapa,
360
  modo_mapa: modoMapa,
361
  escala_extremo_abs: escalaExtremoAbs,
362
+ ponto_raio_min: pontoRaioMin,
363
+ ponto_raio_max: pontoRaioMax,
364
  }),
365
  previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }),
366
  applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }),
 
380
  trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
381
  }),
382
  evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }),
383
+ updateVisualizacaoMap: (sessionId, variavelMapa, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes', pontoRaioMin = null, pontoRaioMax = null) => postJson('/api/visualizacao/map/update', {
384
  session_id: sessionId,
385
  variavel_mapa: variavelMapa,
386
  trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
387
+ ponto_raio_min: pontoRaioMin,
388
+ ponto_raio_max: pontoRaioMax,
389
  }),
390
  evaluationCalculateViz: (sessionId, valoresX, indiceBase, avaliando = null) => postJson('/api/visualizacao/evaluation/calculate', {
391
  session_id: sessionId,
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -2,6 +2,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import { tableToCsvBlob } from '../csv'
4
  import { buildElaboracaoModeloLink } from '../deepLinks'
 
 
 
 
 
5
  import Plotly from 'plotly.js-dist-min'
6
  import DataTable from './DataTable'
7
  import EquationFormatsPanel from './EquationFormatsPanel'
@@ -1607,11 +1612,15 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1607
  const [mapaPayload, setMapaPayload] = useState(null)
1608
  const [mapaVariavel, setMapaVariavel] = useState(MAPA_VARIAVEL_PADRAO)
1609
  const [mapaModo, setMapaModo] = useState(MAPA_MODO_PONTOS)
 
 
1610
  const [mapaGerado, setMapaGerado] = useState(false)
1611
  const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
1612
  const [mapaResiduosPayload, setMapaResiduosPayload] = useState(null)
1613
  const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
1614
  const [mapaResiduosExtremoAbs, setMapaResiduosExtremoAbs] = useState(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
 
 
1615
  const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
1616
 
1617
  const [coordsInfo, setCoordsInfo] = useState(null)
@@ -1770,6 +1779,8 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1770
 
1771
  const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
1772
  const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
 
 
1773
  const importPreviewColumns = useMemo(
1774
  () => (Array.isArray(importPreview?.columns) ? importPreview.columns.map((item) => String(item)) : []),
1775
  [importPreview],
@@ -1786,6 +1797,12 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
1786
  () => importSelectedColumns.length >= 2,
1787
  [importSelectedColumns],
1788
  )
 
 
 
 
 
 
1789
  const colunasXDisponiveis = useMemo(
1790
  () => (colunaY ? colunasNumericas.filter((coluna) => coluna !== colunaY) : []),
1791
  [colunasNumericas, colunaY],
@@ -4795,8 +4812,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4795
  setMapaModo(nextModo)
4796
  }
4797
  if (!sessionId || !mapaGerado) return
 
4798
  await withBusy(async () => {
4799
- const resp = await api.updateElaboracaoMap(sessionId, value, nextModo)
4800
  applyMapaResponse(resp)
4801
  })
4802
  }
@@ -4804,8 +4822,22 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4804
  async function onMapModeChange(value) {
4805
  setMapaModo(value)
4806
  if (!sessionId || !mapaGerado) return
 
4807
  await withBusy(async () => {
4808
- const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, value)
 
 
 
 
 
 
 
 
 
 
 
 
 
4809
  applyMapaResponse(resp)
4810
  })
4811
  }
@@ -4816,8 +4848,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4816
  if (modoAtual !== mapaModo) {
4817
  setMapaModo(modoAtual)
4818
  }
 
4819
  await withBusy(async () => {
4820
- const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, modoAtual)
4821
  applyMapaResponse(resp)
4822
  setMapaGerado(true)
4823
  })
@@ -4827,8 +4860,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4827
  setMapaResiduosModo(value)
4828
  if (!sessionId || !mapaResiduosGerado) return
4829
  const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
 
4830
  await withBusy(async () => {
4831
- const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value, extremoAbs)
4832
  applyMapaResiduosResponse(resp)
4833
  const extremoResp = Number(resp?.escala_extremo_abs)
4834
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
@@ -4844,8 +4878,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4844
  setMapaResiduosExtremoAbs(raw)
4845
  const extremoAbs = parseMapaResiduosExtremoAbs(raw)
4846
  if (!sessionId || !mapaResiduosGerado) return
 
4847
  await withBusy(async () => {
4848
- const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
4849
  applyMapaResiduosResponse(resp)
4850
  const extremoResp = Number(resp?.escala_extremo_abs)
4851
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
@@ -4859,8 +4894,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4859
  async function onGerarMapaResiduos() {
4860
  if (!sessionId) return
4861
  const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
 
4862
  await withBusy(async () => {
4863
- const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs)
4864
  applyMapaResiduosResponse(resp)
4865
  const extremoResp = Number(resp?.escala_extremo_abs)
4866
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
@@ -4872,6 +4908,20 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
4872
  })
4873
  }
4874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4875
  function onDownloadTableCsv(table, fileNameBase) {
4876
  const blob = tableToCsvBlob(table)
4877
  if (!blob) {
@@ -5665,7 +5715,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
5665
 
5666
  {!importSelectionReady ? (
5667
  <div className="section1-empty-hint">
5668
- Selecione pelo menos 2 colunas para liberar as próximas etapas.
5669
  </div>
5670
  ) : null}
5671
 
@@ -6125,6 +6175,32 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
6125
  </select>
6126
  </div>
6127
  ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6128
  </div>
6129
  <div className="download-actions-bar">
6130
  <button
@@ -7466,6 +7542,32 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
7466
  ))}
7467
  </select>
7468
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7469
  </div>
7470
  <div className="residuos-map-scale-hint">
7471
  {mapaResiduosExtremoAbsAtivo === null
 
2
  import { api, downloadBlob } from '../api'
3
  import { tableToCsvBlob } from '../csv'
4
  import { buildElaboracaoModeloLink } from '../deepLinks'
5
+ import {
6
+ MAP_POINT_RADIUS_MAX_DEFAULT,
7
+ MAP_POINT_RADIUS_MIN_DEFAULT,
8
+ normalizeMapPointRadiusRange,
9
+ } from '../mapPointSizing'
10
  import Plotly from 'plotly.js-dist-min'
11
  import DataTable from './DataTable'
12
  import EquationFormatsPanel from './EquationFormatsPanel'
 
1612
  const [mapaPayload, setMapaPayload] = useState(null)
1613
  const [mapaVariavel, setMapaVariavel] = useState(MAPA_VARIAVEL_PADRAO)
1614
  const [mapaModo, setMapaModo] = useState(MAPA_MODO_PONTOS)
1615
+ const [mapaPontoRaioMin, setMapaPontoRaioMin] = useState(String(MAP_POINT_RADIUS_MIN_DEFAULT))
1616
+ const [mapaPontoRaioMax, setMapaPontoRaioMax] = useState(String(MAP_POINT_RADIUS_MAX_DEFAULT))
1617
  const [mapaGerado, setMapaGerado] = useState(false)
1618
  const [mapaResiduosHtml, setMapaResiduosHtml] = useState('')
1619
  const [mapaResiduosPayload, setMapaResiduosPayload] = useState(null)
1620
  const [mapaResiduosModo, setMapaResiduosModo] = useState(MAPA_MODO_PONTOS)
1621
  const [mapaResiduosExtremoAbs, setMapaResiduosExtremoAbs] = useState(MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT)
1622
+ const [mapaResiduosPontoRaioMin, setMapaResiduosPontoRaioMin] = useState(String(MAP_POINT_RADIUS_MIN_DEFAULT))
1623
+ const [mapaResiduosPontoRaioMax, setMapaResiduosPontoRaioMax] = useState(String(MAP_POINT_RADIUS_MAX_DEFAULT))
1624
  const [mapaResiduosGerado, setMapaResiduosGerado] = useState(false)
1625
 
1626
  const [coordsInfo, setCoordsInfo] = useState(null)
 
1779
 
1780
  const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
1781
  const mapaModoDisponivel = mapaVariavel !== MAPA_VARIAVEL_PADRAO
1782
+ const mapaRaioPontosDisponivel = mapaModoDisponivel && mapaModo === MAPA_MODO_PONTOS
1783
+ const mapaResiduosRaioPontosDisponivel = mapaResiduosModo === MAPA_MODO_PONTOS
1784
  const importPreviewColumns = useMemo(
1785
  () => (Array.isArray(importPreview?.columns) ? importPreview.columns.map((item) => String(item)) : []),
1786
  [importPreview],
 
1797
  () => importSelectedColumns.length >= 2,
1798
  [importSelectedColumns],
1799
  )
1800
+ const importColumnsLoadedMessage = useMemo(() => {
1801
+ const total = importPreviewColumns.length
1802
+ const colunaLabel = total === 1 ? 'coluna carregada' : 'colunas carregadas'
1803
+ const referenciaLabel = total === 1 ? 'dela' : 'delas'
1804
+ return `${total} ${colunaLabel}. Selecione pelo menos 2 ${referenciaLabel} para liberar as próximas etapas.`
1805
+ }, [importPreviewColumns])
1806
  const colunasXDisponiveis = useMemo(
1807
  () => (colunaY ? colunasNumericas.filter((coluna) => coluna !== colunaY) : []),
1808
  [colunasNumericas, colunaY],
 
4812
  setMapaModo(nextModo)
4813
  }
4814
  if (!sessionId || !mapaGerado) return
4815
+ const raioRange = normalizeMapPointRadiusRange(mapaPontoRaioMin, mapaPontoRaioMax)
4816
  await withBusy(async () => {
4817
+ const resp = await api.updateElaboracaoMap(sessionId, value, nextModo, raioRange.min, raioRange.max)
4818
  applyMapaResponse(resp)
4819
  })
4820
  }
 
4822
  async function onMapModeChange(value) {
4823
  setMapaModo(value)
4824
  if (!sessionId || !mapaGerado) return
4825
+ const raioRange = normalizeMapPointRadiusRange(mapaPontoRaioMin, mapaPontoRaioMax)
4826
  await withBusy(async () => {
4827
+ const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, value, raioRange.min, raioRange.max)
4828
+ applyMapaResponse(resp)
4829
+ })
4830
+ }
4831
+
4832
+ async function onMapPointRadiusChange(kind, value) {
4833
+ const nextMin = kind === 'min' ? value : mapaPontoRaioMin
4834
+ const nextMax = kind === 'max' ? value : mapaPontoRaioMax
4835
+ if (kind === 'min') setMapaPontoRaioMin(value)
4836
+ else setMapaPontoRaioMax(value)
4837
+ if (!sessionId || !mapaGerado || !mapaRaioPontosDisponivel) return
4838
+ const raioRange = normalizeMapPointRadiusRange(nextMin, nextMax)
4839
+ await withBusy(async () => {
4840
+ const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, MAPA_MODO_PONTOS, raioRange.min, raioRange.max)
4841
  applyMapaResponse(resp)
4842
  })
4843
  }
 
4848
  if (modoAtual !== mapaModo) {
4849
  setMapaModo(modoAtual)
4850
  }
4851
+ const raioRange = normalizeMapPointRadiusRange(mapaPontoRaioMin, mapaPontoRaioMax)
4852
  await withBusy(async () => {
4853
+ const resp = await api.updateElaboracaoMap(sessionId, mapaVariavel, modoAtual, raioRange.min, raioRange.max)
4854
  applyMapaResponse(resp)
4855
  setMapaGerado(true)
4856
  })
 
4860
  setMapaResiduosModo(value)
4861
  if (!sessionId || !mapaResiduosGerado) return
4862
  const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
4863
+ const raioRange = normalizeMapPointRadiusRange(mapaResiduosPontoRaioMin, mapaResiduosPontoRaioMax)
4864
  await withBusy(async () => {
4865
+ const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, value, extremoAbs, raioRange.min, raioRange.max)
4866
  applyMapaResiduosResponse(resp)
4867
  const extremoResp = Number(resp?.escala_extremo_abs)
4868
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
 
4878
  setMapaResiduosExtremoAbs(raw)
4879
  const extremoAbs = parseMapaResiduosExtremoAbs(raw)
4880
  if (!sessionId || !mapaResiduosGerado) return
4881
+ const raioRange = normalizeMapPointRadiusRange(mapaResiduosPontoRaioMin, mapaResiduosPontoRaioMax)
4882
  await withBusy(async () => {
4883
+ const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs, raioRange.min, raioRange.max)
4884
  applyMapaResiduosResponse(resp)
4885
  const extremoResp = Number(resp?.escala_extremo_abs)
4886
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
 
4894
  async function onGerarMapaResiduos() {
4895
  if (!sessionId) return
4896
  const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
4897
+ const raioRange = normalizeMapPointRadiusRange(mapaResiduosPontoRaioMin, mapaResiduosPontoRaioMax)
4898
  await withBusy(async () => {
4899
+ const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, mapaResiduosModo, extremoAbs, raioRange.min, raioRange.max)
4900
  applyMapaResiduosResponse(resp)
4901
  const extremoResp = Number(resp?.escala_extremo_abs)
4902
  if (Number.isFinite(extremoResp) && extremoResp > 0) {
 
4908
  })
4909
  }
4910
 
4911
+ async function onMapaResiduosPointRadiusChange(kind, value) {
4912
+ const nextMin = kind === 'min' ? value : mapaResiduosPontoRaioMin
4913
+ const nextMax = kind === 'max' ? value : mapaResiduosPontoRaioMax
4914
+ if (kind === 'min') setMapaResiduosPontoRaioMin(value)
4915
+ else setMapaResiduosPontoRaioMax(value)
4916
+ if (!sessionId || !mapaResiduosGerado || !mapaResiduosRaioPontosDisponivel) return
4917
+ const extremoAbs = parseMapaResiduosExtremoAbs(mapaResiduosExtremoAbs)
4918
+ const raioRange = normalizeMapPointRadiusRange(nextMin, nextMax)
4919
+ await withBusy(async () => {
4920
+ const resp = await api.updateElaboracaoResiduosMap(sessionId, MAPA_RESIDUOS_VARIAVEL, MAPA_MODO_PONTOS, extremoAbs, raioRange.min, raioRange.max)
4921
+ applyMapaResiduosResponse(resp)
4922
+ })
4923
+ }
4924
+
4925
  function onDownloadTableCsv(table, fileNameBase) {
4926
  const blob = tableToCsvBlob(table)
4927
  if (!blob) {
 
5715
 
5716
  {!importSelectionReady ? (
5717
  <div className="section1-empty-hint">
5718
+ {importColumnsLoadedMessage}
5719
  </div>
5720
  ) : null}
5721
 
 
6175
  </select>
6176
  </div>
6177
  ) : null}
6178
+ {mapaRaioPontosDisponivel ? (
6179
+ <>
6180
+ <div className="dados-mapa-control-field dados-mapa-radius-field">
6181
+ <label>Raio mínimo</label>
6182
+ <input
6183
+ type="number"
6184
+ min="1"
6185
+ max="64"
6186
+ step="0.5"
6187
+ value={mapaPontoRaioMin}
6188
+ onChange={(e) => void onMapPointRadiusChange('min', e.target.value)}
6189
+ />
6190
+ </div>
6191
+ <div className="dados-mapa-control-field dados-mapa-radius-field">
6192
+ <label>Raio máximo</label>
6193
+ <input
6194
+ type="number"
6195
+ min="1"
6196
+ max="64"
6197
+ step="0.5"
6198
+ value={mapaPontoRaioMax}
6199
+ onChange={(e) => void onMapPointRadiusChange('max', e.target.value)}
6200
+ />
6201
+ </div>
6202
+ </>
6203
+ ) : null}
6204
  </div>
6205
  <div className="download-actions-bar">
6206
  <button
 
7542
  ))}
7543
  </select>
7544
  </div>
7545
+ {mapaResiduosRaioPontosDisponivel ? (
7546
+ <>
7547
+ <div className="dados-mapa-control-field dados-mapa-radius-field">
7548
+ <label>Raio mínimo</label>
7549
+ <input
7550
+ type="number"
7551
+ min="1"
7552
+ max="64"
7553
+ step="0.5"
7554
+ value={mapaResiduosPontoRaioMin}
7555
+ onChange={(e) => void onMapaResiduosPointRadiusChange('min', e.target.value)}
7556
+ />
7557
+ </div>
7558
+ <div className="dados-mapa-control-field dados-mapa-radius-field">
7559
+ <label>Raio máximo</label>
7560
+ <input
7561
+ type="number"
7562
+ min="1"
7563
+ max="64"
7564
+ step="0.5"
7565
+ value={mapaResiduosPontoRaioMax}
7566
+ onChange={(e) => void onMapaResiduosPointRadiusChange('max', e.target.value)}
7567
+ />
7568
+ </div>
7569
+ </>
7570
+ ) : null}
7571
  </div>
7572
  <div className="residuos-map-scale-hint">
7573
  {mapaResiduosExtremoAbsAtivo === null
frontend/src/components/LeafletMapFrame.jsx CHANGED
@@ -1,6 +1,5 @@
1
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
2
  import L from 'leaflet'
3
- import 'leaflet.fullscreen'
4
  import 'leaflet.heat'
5
  import { apiUrl, downloadBlob, getAuthToken } from '../api'
6
 
@@ -399,7 +398,7 @@ function buildLegacyOverlayLayers(payload) {
399
  overlays.push({
400
  id: 'trabalhos-tecnicos',
401
  label: 'Trabalhos técnicos',
402
- show: true,
403
  markers: payload.trabalhos_tecnicos_points,
404
  })
405
  }
@@ -631,6 +630,64 @@ function fitMapToPayloadBounds(map, payload, padding = 48) {
631
  return false
632
  }
633
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
634
  async function waitForTileLayerLoad(tileLayer, timeoutMs = 6500) {
635
  if (!tileLayer) return
636
  await new Promise((resolve) => {
@@ -1018,7 +1075,7 @@ async function captureContainerRegionBlob(container, selectionRect = null, scale
1018
  return canvasToBlob(canvas)
1019
  }
1020
 
1021
- const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId }, ref) {
1022
  const hostRef = useRef(null)
1023
  const mapRef = useRef(null)
1024
  const payloadRef = useRef(payload)
@@ -1785,9 +1842,6 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
1785
  window.setTimeout(() => bindLayerControlHover(layerControl), 180)
1786
  }
1787
  }
1788
- if (payload.controls?.fullscreen && typeof L.control.fullscreen === 'function') {
1789
- L.control.fullscreen({ position: 'topleft' }).addTo(map)
1790
- }
1791
  if (payload.controls?.measure) {
1792
  const disableInteractionsForMeasure = () => {
1793
  if (restoreMapInteractions) return
@@ -2049,6 +2103,10 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
2049
  measureControl.addTo(map)
2050
  }
2051
 
 
 
 
 
2052
  if (payload.legend?.title) {
2053
  const legendControl = construirLegendaControl(payload.legend)
2054
  if (legendControl) {
@@ -2093,7 +2151,7 @@ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId
2093
  }
2094
  map.remove()
2095
  }
2096
- }, [payload, sessionId])
2097
 
2098
  return (
2099
  <div className="map-frame leaflet-map-host">
 
1
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
2
  import L from 'leaflet'
 
3
  import 'leaflet.heat'
4
  import { apiUrl, downloadBlob, getAuthToken } from '../api'
5
 
 
398
  overlays.push({
399
  id: 'trabalhos-tecnicos',
400
  label: 'Trabalhos técnicos',
401
+ show: payload?.trabalhos_tecnicos_show !== false,
402
  markers: payload.trabalhos_tecnicos_points,
403
  })
404
  }
 
630
  return false
631
  }
632
 
633
+ function addFullscreenControl(map, onToggleFullscreen) {
634
+ const control = L.control({ position: 'topright' })
635
+ let root = null
636
+
637
+ control.onAdd = () => {
638
+ root = L.DomUtil.create('div', 'mesa-leaflet-fullscreen leaflet-bar leaflet-control')
639
+ const button = L.DomUtil.create('a', 'mesa-leaflet-fullscreen-toggle', root)
640
+ button.href = '#'
641
+ button.title = 'Abrir mapa em tela cheia'
642
+ button.setAttribute('aria-label', 'Abrir mapa em tela cheia')
643
+ button.innerHTML = '<span class="mesa-sr-only">Abrir mapa em tela cheia</span>'
644
+
645
+ const updateState = () => {
646
+ const active = Boolean(document.fullscreenElement)
647
+ root?.classList.toggle('is-active', active)
648
+ button.title = active ? 'Sair da tela cheia' : 'Abrir mapa em tela cheia'
649
+ button.setAttribute('aria-label', active ? 'Sair da tela cheia' : 'Abrir mapa em tela cheia')
650
+ }
651
+
652
+ const fallbackToggle = async () => {
653
+ if (document.fullscreenElement) {
654
+ await document.exitFullscreen()
655
+ return
656
+ }
657
+ const container = map.getContainer()
658
+ if (typeof container?.requestFullscreen === 'function') {
659
+ await container.requestFullscreen()
660
+ }
661
+ }
662
+
663
+ L.DomEvent.disableClickPropagation(root)
664
+ L.DomEvent.disableScrollPropagation(root)
665
+ L.DomEvent.on(button, 'click', (event) => {
666
+ L.DomEvent.stop(event)
667
+ const result = onToggleFullscreen ? onToggleFullscreen(event) : fallbackToggle()
668
+ Promise.resolve(result).catch(() => {}).finally(() => {
669
+ window.setTimeout(() => {
670
+ map.invalidateSize?.(true)
671
+ }, 120)
672
+ })
673
+ })
674
+ document.addEventListener('fullscreenchange', updateState)
675
+ root._mesaFullscreenCleanup = () => {
676
+ document.removeEventListener('fullscreenchange', updateState)
677
+ }
678
+ updateState()
679
+ return root
680
+ }
681
+
682
+ control.onRemove = () => {
683
+ root?._mesaFullscreenCleanup?.()
684
+ root = null
685
+ }
686
+
687
+ control.addTo(map)
688
+ return control
689
+ }
690
+
691
  async function waitForTileLayerLoad(tileLayer, timeoutMs = 6500) {
692
  if (!tileLayer) return
693
  await new Promise((resolve) => {
 
1075
  return canvasToBlob(canvas)
1076
  }
1077
 
1078
+ const LeafletMapFrame = forwardRef(function LeafletMapFrame({ payload, sessionId, onToggleFullscreen }, ref) {
1079
  const hostRef = useRef(null)
1080
  const mapRef = useRef(null)
1081
  const payloadRef = useRef(payload)
 
1842
  window.setTimeout(() => bindLayerControlHover(layerControl), 180)
1843
  }
1844
  }
 
 
 
1845
  if (payload.controls?.measure) {
1846
  const disableInteractionsForMeasure = () => {
1847
  if (restoreMapInteractions) return
 
2103
  measureControl.addTo(map)
2104
  }
2105
 
2106
+ if (payload.controls?.fullscreen) {
2107
+ addFullscreenControl(map, onToggleFullscreen)
2108
+ }
2109
+
2110
  if (payload.legend?.title) {
2111
  const legendControl = construirLegendaControl(payload.legend)
2112
  if (legendControl) {
 
2151
  }
2152
  map.remove()
2153
  }
2154
+ }, [payload, sessionId, onToggleFullscreen])
2155
 
2156
  return (
2157
  <div className="map-frame leaflet-map-host">
frontend/src/components/MapFrame.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
2
  import LeafletMapFrame from './LeafletMapFrame'
3
 
4
  function hashHtml(value) {
@@ -12,9 +12,11 @@ function hashHtml(value) {
12
  }
13
 
14
  const MapFrame = forwardRef(function MapFrame({ html, payload = null, sessionId = '' }, ref) {
 
15
  const iframeRef = useRef(null)
16
  const leafletRef = useRef(null)
17
  const timersRef = useRef([])
 
18
  const frameKey = useMemo(() => hashHtml(html || ''), [html])
19
 
20
  const clearTimers = useCallback(() => {
@@ -125,12 +127,49 @@ const MapFrame = forwardRef(function MapFrame({ html, payload = null, sessionId
125
  })
126
  }, [clearTimers, recenterFromLayers])
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  useEffect(() => {
129
  return () => {
130
  clearTimers()
131
  }
132
  }, [clearTimers])
133
 
 
 
 
 
 
 
 
 
 
 
 
134
  useImperativeHandle(ref, () => ({
135
  fitToPayloadBounds(padding = 48) {
136
  if (leafletRef.current?.fitToPayloadBounds) {
@@ -152,24 +191,37 @@ const MapFrame = forwardRef(function MapFrame({ html, payload = null, sessionId
152
  },
153
  }), [])
154
 
155
- if (payload && payload.type === 'mesa_leaflet_payload') {
156
- return <LeafletMapFrame ref={leafletRef} payload={payload} sessionId={sessionId} />
157
- }
158
-
159
- if (!html) {
160
  return <div className="empty-box">Mapa indisponivel.</div>
161
  }
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  return (
164
- <iframe
165
- key={frameKey}
166
- ref={iframeRef}
167
- title="mapa"
168
- className="map-frame"
169
- srcDoc={html}
170
- sandbox="allow-scripts allow-same-origin allow-popups"
171
- onLoad={scheduleRecenter}
172
- />
173
  )
174
  })
175
 
 
1
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
2
  import LeafletMapFrame from './LeafletMapFrame'
3
 
4
  function hashHtml(value) {
 
12
  }
13
 
14
  const MapFrame = forwardRef(function MapFrame({ html, payload = null, sessionId = '' }, ref) {
15
+ const shellRef = useRef(null)
16
  const iframeRef = useRef(null)
17
  const leafletRef = useRef(null)
18
  const timersRef = useRef([])
19
+ const [isFullscreen, setIsFullscreen] = useState(false)
20
  const frameKey = useMemo(() => hashHtml(html || ''), [html])
21
 
22
  const clearTimers = useCallback(() => {
 
127
  })
128
  }, [clearTimers, recenterFromLayers])
129
 
130
+ const refreshMapSize = useCallback(() => {
131
+ if (leafletRef.current?.fitToPayloadBounds) {
132
+ window.setTimeout(() => {
133
+ leafletRef.current?.fitToPayloadBounds?.(48)
134
+ }, 120)
135
+ return
136
+ }
137
+ scheduleRecenter()
138
+ }, [scheduleRecenter])
139
+
140
+ const toggleFullscreen = useCallback(async (event) => {
141
+ event.preventDefault()
142
+ event.stopPropagation()
143
+ const shell = shellRef.current
144
+ if (!shell) return
145
+ try {
146
+ if (document.fullscreenElement === shell) {
147
+ await document.exitFullscreen()
148
+ } else if (typeof shell.requestFullscreen === 'function') {
149
+ await shell.requestFullscreen()
150
+ }
151
+ } catch {
152
+ // Navegadores podem bloquear fullscreen fora de interação direta.
153
+ }
154
+ }, [])
155
+
156
  useEffect(() => {
157
  return () => {
158
  clearTimers()
159
  }
160
  }, [clearTimers])
161
 
162
+ useEffect(() => {
163
+ function onFullscreenChange() {
164
+ const active = document.fullscreenElement === shellRef.current
165
+ setIsFullscreen(active)
166
+ refreshMapSize()
167
+ }
168
+
169
+ document.addEventListener('fullscreenchange', onFullscreenChange)
170
+ return () => document.removeEventListener('fullscreenchange', onFullscreenChange)
171
+ }, [refreshMapSize])
172
+
173
  useImperativeHandle(ref, () => ({
174
  fitToPayloadBounds(padding = 48) {
175
  if (leafletRef.current?.fitToPayloadBounds) {
 
191
  },
192
  }), [])
193
 
194
+ if (!(payload && payload.type === 'mesa_leaflet_payload') && !html) {
 
 
 
 
195
  return <div className="empty-box">Mapa indisponivel.</div>
196
  }
197
 
198
+ const mapContent = payload && payload.type === 'mesa_leaflet_payload'
199
+ ? (
200
+ <LeafletMapFrame
201
+ ref={leafletRef}
202
+ payload={payload}
203
+ sessionId={sessionId}
204
+ onToggleFullscreen={toggleFullscreen}
205
+ />
206
+ )
207
+ : (
208
+ <iframe
209
+ key={frameKey}
210
+ ref={iframeRef}
211
+ title="mapa"
212
+ className="map-frame"
213
+ srcDoc={html}
214
+ sandbox="allow-scripts allow-same-origin allow-popups"
215
+ allow="fullscreen"
216
+ allowFullScreen
217
+ onLoad={scheduleRecenter}
218
+ />
219
+ )
220
+
221
  return (
222
+ <div ref={shellRef} className={`map-frame-shell${isFullscreen ? ' is-fullscreen' : ''}`}>
223
+ {mapContent}
224
+ </div>
 
 
 
 
 
 
225
  )
226
  })
227
 
frontend/src/components/PesquisaTab.jsx CHANGED
@@ -2,6 +2,11 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
  import { createPortal } from 'react-dom'
3
  import { api, downloadBlob } from '../api'
4
  import { buildPesquisaLink, buildPesquisaRoutePayload, getPesquisaFilterDefaults, hasPesquisaRoutePayload } from '../deepLinks'
 
 
 
 
 
5
  import DataTable from './DataTable'
6
  import EquationFormatsPanel from './EquationFormatsPanel'
7
  import LoadingOverlay from './LoadingOverlay'
@@ -1051,6 +1056,8 @@ export default function PesquisaTab({
1051
  const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
1052
  const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
1053
  const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
 
 
1054
  const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
1055
  const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
1056
  const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
@@ -1075,6 +1082,7 @@ export default function PesquisaTab({
1075
  const modeloAbertoOpenVersionRef = useRef(0)
1076
 
1077
  const sugestoes = result.sugestoes || {}
 
1078
  const opcoesTipoModelo = useMemo(
1079
  () => {
1080
  const atual = String(filters.tipoModelo || '').trim()
@@ -1511,6 +1519,8 @@ export default function PesquisaTab({
1511
  setModeloAbertoMapaPayload(null)
1512
  setModeloAbertoMapaChoices(['Visualização Padrão'])
1513
  setModeloAbertoMapaVar('Visualização Padrão')
 
 
1514
  setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
1515
  setModeloAbertoTrabalhosTecnicos([])
1516
  setModeloAbertoPlotObsCalc(null)
@@ -1691,11 +1701,14 @@ export default function PesquisaTab({
1691
  async function atualizarMapaModeloAberto(
1692
  nextVar = modeloAbertoMapaVar,
1693
  nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
 
 
1694
  ) {
1695
  if (!sessionId) return
1696
  setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
1697
  try {
1698
- const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
 
1699
  setModeloAbertoMapaHtml(resp?.mapa_html || '')
1700
  setModeloAbertoMapaPayload(resp?.mapa_payload || null)
1701
  setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
@@ -1706,6 +1719,19 @@ export default function PesquisaTab({
1706
  }
1707
  }
1708
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1709
  async function onModeloAbertoMapChange(nextVar) {
1710
  setModeloAbertoMapaVar(nextVar)
1711
  try {
@@ -1931,6 +1957,32 @@ export default function PesquisaTab({
1931
  <option value="selecionados_e_outras_versoes">Incluir demais versões do modelo</option>
1932
  </select>
1933
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1934
  </div>
1935
  <MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
1936
  </>
 
2
  import { createPortal } from 'react-dom'
3
  import { api, downloadBlob } from '../api'
4
  import { buildPesquisaLink, buildPesquisaRoutePayload, getPesquisaFilterDefaults, hasPesquisaRoutePayload } from '../deepLinks'
5
+ import {
6
+ MAP_POINT_RADIUS_MAX_DEFAULT,
7
+ MAP_POINT_RADIUS_MIN_DEFAULT,
8
+ normalizeMapPointRadiusRange,
9
+ } from '../mapPointSizing'
10
  import DataTable from './DataTable'
11
  import EquationFormatsPanel from './EquationFormatsPanel'
12
  import LoadingOverlay from './LoadingOverlay'
 
1056
  const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
1057
  const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
1058
  const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
1059
+ const [modeloAbertoMapaRaioMin, setModeloAbertoMapaRaioMin] = useState(String(MAP_POINT_RADIUS_MIN_DEFAULT))
1060
+ const [modeloAbertoMapaRaioMax, setModeloAbertoMapaRaioMax] = useState(String(MAP_POINT_RADIUS_MAX_DEFAULT))
1061
  const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
1062
  const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
1063
  const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
 
1082
  const modeloAbertoOpenVersionRef = useRef(0)
1083
 
1084
  const sugestoes = result.sugestoes || {}
1085
+ const modeloAbertoMapaRaioDisponivel = modeloAbertoMapaVar !== 'Visualização Padrão'
1086
  const opcoesTipoModelo = useMemo(
1087
  () => {
1088
  const atual = String(filters.tipoModelo || '').trim()
 
1519
  setModeloAbertoMapaPayload(null)
1520
  setModeloAbertoMapaChoices(['Visualização Padrão'])
1521
  setModeloAbertoMapaVar('Visualização Padrão')
1522
+ setModeloAbertoMapaRaioMin(String(MAP_POINT_RADIUS_MIN_DEFAULT))
1523
+ setModeloAbertoMapaRaioMax(String(MAP_POINT_RADIUS_MAX_DEFAULT))
1524
  setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
1525
  setModeloAbertoTrabalhosTecnicos([])
1526
  setModeloAbertoPlotObsCalc(null)
 
1701
  async function atualizarMapaModeloAberto(
1702
  nextVar = modeloAbertoMapaVar,
1703
  nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
1704
+ nextRaioMin = modeloAbertoMapaRaioMin,
1705
+ nextRaioMax = modeloAbertoMapaRaioMax,
1706
  ) {
1707
  if (!sessionId) return
1708
  setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
1709
  try {
1710
+ const raioRange = normalizeMapPointRadiusRange(nextRaioMin, nextRaioMax)
1711
+ const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo, raioRange.min, raioRange.max)
1712
  setModeloAbertoMapaHtml(resp?.mapa_html || '')
1713
  setModeloAbertoMapaPayload(resp?.mapa_payload || null)
1714
  setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
 
1719
  }
1720
  }
1721
 
1722
+ async function onModeloAbertoMapRadiusChange(kind, value) {
1723
+ const nextMin = kind === 'min' ? value : modeloAbertoMapaRaioMin
1724
+ const nextMax = kind === 'max' ? value : modeloAbertoMapaRaioMax
1725
+ if (kind === 'min') setModeloAbertoMapaRaioMin(value)
1726
+ else setModeloAbertoMapaRaioMax(value)
1727
+ if (!modeloAbertoMapaRaioDisponivel) return
1728
+ try {
1729
+ await atualizarMapaModeloAberto(modeloAbertoMapaVar, modeloAbertoTrabalhosTecnicosModelosModo, nextMin, nextMax)
1730
+ } catch (err) {
1731
+ setModeloAbertoError(err.message || 'Falha ao atualizar tamanho dos pontos.')
1732
+ }
1733
+ }
1734
+
1735
  async function onModeloAbertoMapChange(nextVar) {
1736
  setModeloAbertoMapaVar(nextVar)
1737
  try {
 
1957
  <option value="selecionados_e_outras_versoes">Incluir demais versões do modelo</option>
1958
  </select>
1959
  </label>
1960
+ {modeloAbertoMapaRaioDisponivel ? (
1961
+ <>
1962
+ <label className="pesquisa-field pesquisa-mapa-radius-field">
1963
+ Raio mínimo
1964
+ <input
1965
+ type="number"
1966
+ min="1"
1967
+ max="64"
1968
+ step="0.5"
1969
+ value={modeloAbertoMapaRaioMin}
1970
+ onChange={(event) => void onModeloAbertoMapRadiusChange('min', event.target.value)}
1971
+ />
1972
+ </label>
1973
+ <label className="pesquisa-field pesquisa-mapa-radius-field">
1974
+ Raio máximo
1975
+ <input
1976
+ type="number"
1977
+ min="1"
1978
+ max="64"
1979
+ step="0.5"
1980
+ value={modeloAbertoMapaRaioMax}
1981
+ onChange={(event) => void onModeloAbertoMapRadiusChange('max', event.target.value)}
1982
+ />
1983
+ </label>
1984
+ </>
1985
+ ) : null}
1986
  </div>
1987
  <MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
1988
  </>
frontend/src/components/RepositorioTab.jsx CHANGED
@@ -1,6 +1,11 @@
1
  import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import { buildRepositorioModeloLink, hasMesaDeepLink, normalizeMesaDeepLink, parseMesaDeepLink } from '../deepLinks'
 
 
 
 
 
4
  import DataTable from './DataTable'
5
  import EquationFormatsPanel from './EquationFormatsPanel'
6
  import ListPagination from './ListPagination'
@@ -191,6 +196,8 @@ export default function RepositorioTab({
191
  const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
192
  const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
193
  const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
 
 
194
  const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
195
  const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
196
  const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
@@ -207,6 +214,7 @@ export default function RepositorioTab({
207
  const lastOpenRequestKeyRef = useRef('')
208
  const pendingTabRequestsRef = useRef({})
209
  const modeloOpenVersionRef = useRef(0)
 
210
 
211
  const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
212
  const totalModelos = modelos.length
@@ -372,6 +380,8 @@ export default function RepositorioTab({
372
  setModeloAbertoMapaPayload(null)
373
  setModeloAbertoMapaChoices(['Visualização Padrão'])
374
  setModeloAbertoMapaVar('Visualização Padrão')
 
 
375
  setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
376
  setModeloAbertoTrabalhosTecnicos([])
377
  setModeloAbertoPlotObsCalc(null)
@@ -580,11 +590,14 @@ export default function RepositorioTab({
580
  async function atualizarMapaModeloAberto(
581
  nextVar = modeloAbertoMapaVar,
582
  nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
 
 
583
  ) {
584
  if (!sessionId) return
585
  setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
586
  try {
587
- const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
 
588
  setModeloAbertoMapaHtml(resp?.mapa_html || '')
589
  setModeloAbertoMapaPayload(resp?.mapa_payload || null)
590
  setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
@@ -595,6 +608,19 @@ export default function RepositorioTab({
595
  }
596
  }
597
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  async function onModeloAbertoMapChange(nextVar) {
599
  setModeloAbertoMapaVar(nextVar)
600
  try {
@@ -728,6 +754,32 @@ export default function RepositorioTab({
728
  <option value="selecionados_e_outras_versoes">Incluir demais versões do modelo</option>
729
  </select>
730
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  </div>
732
  <MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
733
  </>
 
1
  import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import { buildRepositorioModeloLink, hasMesaDeepLink, normalizeMesaDeepLink, parseMesaDeepLink } from '../deepLinks'
4
+ import {
5
+ MAP_POINT_RADIUS_MAX_DEFAULT,
6
+ MAP_POINT_RADIUS_MIN_DEFAULT,
7
+ normalizeMapPointRadiusRange,
8
+ } from '../mapPointSizing'
9
  import DataTable from './DataTable'
10
  import EquationFormatsPanel from './EquationFormatsPanel'
11
  import ListPagination from './ListPagination'
 
196
  const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
197
  const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
198
  const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
199
+ const [modeloAbertoMapaRaioMin, setModeloAbertoMapaRaioMin] = useState(String(MAP_POINT_RADIUS_MIN_DEFAULT))
200
+ const [modeloAbertoMapaRaioMax, setModeloAbertoMapaRaioMax] = useState(String(MAP_POINT_RADIUS_MAX_DEFAULT))
201
  const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
202
  const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
203
  const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
 
214
  const lastOpenRequestKeyRef = useRef('')
215
  const pendingTabRequestsRef = useRef({})
216
  const modeloOpenVersionRef = useRef(0)
217
+ const modeloAbertoMapaRaioDisponivel = modeloAbertoMapaVar !== 'Visualização Padrão'
218
 
219
  const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
220
  const totalModelos = modelos.length
 
380
  setModeloAbertoMapaPayload(null)
381
  setModeloAbertoMapaChoices(['Visualização Padrão'])
382
  setModeloAbertoMapaVar('Visualização Padrão')
383
+ setModeloAbertoMapaRaioMin(String(MAP_POINT_RADIUS_MIN_DEFAULT))
384
+ setModeloAbertoMapaRaioMax(String(MAP_POINT_RADIUS_MAX_DEFAULT))
385
  setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
386
  setModeloAbertoTrabalhosTecnicos([])
387
  setModeloAbertoPlotObsCalc(null)
 
590
  async function atualizarMapaModeloAberto(
591
  nextVar = modeloAbertoMapaVar,
592
  nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
593
+ nextRaioMin = modeloAbertoMapaRaioMin,
594
+ nextRaioMax = modeloAbertoMapaRaioMax,
595
  ) {
596
  if (!sessionId) return
597
  setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
598
  try {
599
+ const raioRange = normalizeMapPointRadiusRange(nextRaioMin, nextRaioMax)
600
+ const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo, raioRange.min, raioRange.max)
601
  setModeloAbertoMapaHtml(resp?.mapa_html || '')
602
  setModeloAbertoMapaPayload(resp?.mapa_payload || null)
603
  setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
 
608
  }
609
  }
610
 
611
+ async function onModeloAbertoMapRadiusChange(kind, value) {
612
+ const nextMin = kind === 'min' ? value : modeloAbertoMapaRaioMin
613
+ const nextMax = kind === 'max' ? value : modeloAbertoMapaRaioMax
614
+ if (kind === 'min') setModeloAbertoMapaRaioMin(value)
615
+ else setModeloAbertoMapaRaioMax(value)
616
+ if (!modeloAbertoMapaRaioDisponivel) return
617
+ try {
618
+ await atualizarMapaModeloAberto(modeloAbertoMapaVar, modeloAbertoTrabalhosTecnicosModelosModo, nextMin, nextMax)
619
+ } catch (err) {
620
+ setModeloAbertoError(err.message || 'Falha ao atualizar tamanho dos pontos.')
621
+ }
622
+ }
623
+
624
  async function onModeloAbertoMapChange(nextVar) {
625
  setModeloAbertoMapaVar(nextVar)
626
  try {
 
754
  <option value="selecionados_e_outras_versoes">Incluir demais versões do modelo</option>
755
  </select>
756
  </label>
757
+ {modeloAbertoMapaRaioDisponivel ? (
758
+ <>
759
+ <label className="pesquisa-field pesquisa-mapa-radius-field">
760
+ Raio mínimo
761
+ <input
762
+ type="number"
763
+ min="1"
764
+ max="64"
765
+ step="0.5"
766
+ value={modeloAbertoMapaRaioMin}
767
+ onChange={(event) => void onModeloAbertoMapRadiusChange('min', event.target.value)}
768
+ />
769
+ </label>
770
+ <label className="pesquisa-field pesquisa-mapa-radius-field">
771
+ Raio máximo
772
+ <input
773
+ type="number"
774
+ min="1"
775
+ max="64"
776
+ step="0.5"
777
+ value={modeloAbertoMapaRaioMax}
778
+ onChange={(event) => void onModeloAbertoMapRadiusChange('max', event.target.value)}
779
+ />
780
+ </label>
781
+ </>
782
+ ) : null}
783
  </div>
784
  <MapFrame html={modeloAbertoMapaHtml} payload={modeloAbertoMapaPayload} sessionId={sessionId} />
785
  </>
frontend/src/mapPointSizing.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MAP_POINT_RADIUS_MIN_DEFAULT = 3
2
+ export const MAP_POINT_RADIUS_MAX_DEFAULT = 18
3
+ export const MAP_POINT_RADIUS_MIN_LIMIT = 1
4
+ export const MAP_POINT_RADIUS_MAX_LIMIT = 64
5
+
6
+ function parseFiniteNumber(value, fallback) {
7
+ if (value === null || value === undefined || value === '') return fallback
8
+ const parsed = Number(String(value).trim().replace(',', '.'))
9
+ return Number.isFinite(parsed) ? parsed : fallback
10
+ }
11
+
12
+ export function normalizeMapPointRadiusRange(minValue, maxValue) {
13
+ let min = parseFiniteNumber(minValue, MAP_POINT_RADIUS_MIN_DEFAULT)
14
+ let max = parseFiniteNumber(maxValue, MAP_POINT_RADIUS_MAX_DEFAULT)
15
+ min = Math.min(MAP_POINT_RADIUS_MAX_LIMIT, Math.max(MAP_POINT_RADIUS_MIN_LIMIT, min))
16
+ max = Math.min(MAP_POINT_RADIUS_MAX_LIMIT, Math.max(MAP_POINT_RADIUS_MIN_LIMIT, max))
17
+ if (min > max) {
18
+ const temp = min
19
+ min = max
20
+ max = temp
21
+ }
22
+ return { min, max }
23
+ }
frontend/src/styles.css CHANGED
@@ -2161,11 +2161,17 @@ textarea {
2161
  margin: 0;
2162
  }
2163
 
2164
- .dados-mapa-control-field select {
 
2165
  width: 100%;
2166
  }
2167
 
 
 
 
 
2168
  .dados-mapa-controls + .map-frame,
 
2169
  .dados-mapa-controls + .empty-box {
2170
  margin-top: 6px;
2171
  }
@@ -2262,6 +2268,7 @@ textarea {
2262
  }
2263
 
2264
  .visualizacao-mapa-controls + .map-frame,
 
2265
  .visualizacao-mapa-controls + .empty-box {
2266
  margin-top: 10px;
2267
  }
@@ -5651,6 +5658,37 @@ button.import-preview-clear-btn {
5651
  background: #fff;
5652
  }
5653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5654
  .leaflet-map-host {
5655
  position: relative;
5656
  overflow: hidden;
@@ -5995,7 +6033,7 @@ button.import-preview-clear-btn {
5995
  border-top-color: rgba(207, 217, 227, 0.95);
5996
  }
5997
 
5998
- .leaflet-map-host .leaflet-control-fullscreen,
5999
  .leaflet-map-host .leaflet-control-measure {
6000
  border: none;
6001
  box-shadow: 0 10px 24px rgba(27, 48, 72, 0.16);
@@ -6025,6 +6063,44 @@ button.import-preview-clear-btn {
6025
  box-shadow: none;
6026
  }
6027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6028
  .leaflet-map-host .mesa-leaflet-measure .mesa-leaflet-measure-toggle {
6029
  width: 38px;
6030
  height: 38px;
 
2161
  margin: 0;
2162
  }
2163
 
2164
+ .dados-mapa-control-field select,
2165
+ .dados-mapa-control-field input {
2166
  width: 100%;
2167
  }
2168
 
2169
+ .dados-mapa-radius-field {
2170
+ min-width: 128px;
2171
+ }
2172
+
2173
  .dados-mapa-controls + .map-frame,
2174
+ .dados-mapa-controls + .map-frame-shell,
2175
  .dados-mapa-controls + .empty-box {
2176
  margin-top: 6px;
2177
  }
 
2268
  }
2269
 
2270
  .visualizacao-mapa-controls + .map-frame,
2271
+ .visualizacao-mapa-controls + .map-frame-shell,
2272
  .visualizacao-mapa-controls + .empty-box {
2273
  margin-top: 10px;
2274
  }
 
5658
  background: #fff;
5659
  }
5660
 
5661
+ .map-frame-shell {
5662
+ position: relative;
5663
+ width: 100%;
5664
+ max-width: 100%;
5665
+ }
5666
+
5667
+ .map-frame-shell .map-frame {
5668
+ margin: 0;
5669
+ }
5670
+
5671
+ .map-frame-shell:fullscreen {
5672
+ display: flex;
5673
+ width: 100vw;
5674
+ height: 100vh;
5675
+ padding: 14px;
5676
+ box-sizing: border-box;
5677
+ background: #eef4f8;
5678
+ }
5679
+
5680
+ .map-frame-shell:fullscreen .map-frame {
5681
+ flex: 1;
5682
+ min-height: calc(100vh - 28px);
5683
+ height: calc(100vh - 28px);
5684
+ }
5685
+
5686
+ .map-frame-shell:fullscreen .leaflet-map-canvas,
5687
+ .map-frame-shell:fullscreen .leaflet-map-host .leaflet-container {
5688
+ min-height: calc(100vh - 28px);
5689
+ height: calc(100vh - 28px);
5690
+ }
5691
+
5692
  .leaflet-map-host {
5693
  position: relative;
5694
  overflow: hidden;
 
6033
  border-top-color: rgba(207, 217, 227, 0.95);
6034
  }
6035
 
6036
+ .leaflet-map-host .mesa-leaflet-fullscreen,
6037
  .leaflet-map-host .leaflet-control-measure {
6038
  border: none;
6039
  box-shadow: 0 10px 24px rgba(27, 48, 72, 0.16);
 
6063
  box-shadow: none;
6064
  }
6065
 
6066
+ .leaflet-map-host .mesa-leaflet-fullscreen {
6067
+ background: transparent;
6068
+ border: none;
6069
+ box-shadow: none;
6070
+ }
6071
+
6072
+ .leaflet-map-host .mesa-leaflet-fullscreen .mesa-leaflet-fullscreen-toggle {
6073
+ width: 38px;
6074
+ height: 38px;
6075
+ position: relative;
6076
+ display: block;
6077
+ background: rgba(255, 255, 255, 0.96);
6078
+ border: 1px solid rgba(193, 207, 220, 0.95);
6079
+ border-radius: 10px;
6080
+ box-shadow: 0 10px 24px rgba(27, 48, 72, 0.16);
6081
+ }
6082
+
6083
+ .leaflet-map-host .mesa-leaflet-fullscreen .mesa-leaflet-fullscreen-toggle::before {
6084
+ content: '';
6085
+ position: absolute;
6086
+ inset: 0;
6087
+ margin: auto;
6088
+ width: 18px;
6089
+ height: 18px;
6090
+ background-repeat: no-repeat;
6091
+ background-position: center;
6092
+ background-size: 18px 18px;
6093
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2324405b' d='M5 5h6v2H8.41l3.3 3.29-1.42 1.42L7 8.41V11H5V5Zm8 0h6v6h-2V8.41l-3.29 3.3-1.42-1.42L15.59 7H13V5Zm-2 14H5v-6h2v2.59l3.29-3.3 1.42 1.42L8.41 17H11v2Zm8 0h-6v-2h2.59l-3.3-3.29 1.42-1.42 3.29 3.3V13h2v6Z'/%3E%3C/svg%3E");
6094
+ }
6095
+
6096
+ .leaflet-map-host .mesa-leaflet-fullscreen.is-active .mesa-leaflet-fullscreen-toggle::before {
6097
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2324405b' d='M9 3h2v6H5V7h2.59L4.29 3.71 5.71 2.29 9 5.59V3Zm6 0v2.59l3.29-3.3 1.42 1.42L16.41 7H19v2h-6V3h2ZM5 15h6v6H9v-2.59l-3.29 3.3-1.42-1.42L7.59 17H5v-2Zm14 0v2h-2.59l3.3 3.29-1.42 1.42-3.29-3.3V21h-2v-6h6Z'/%3E%3C/svg%3E");
6098
+ }
6099
+
6100
+ .leaflet-map-host .mesa-leaflet-fullscreen .mesa-leaflet-fullscreen-toggle:hover {
6101
+ background: #eef5fb;
6102
+ }
6103
+
6104
  .leaflet-map-host .mesa-leaflet-measure .mesa-leaflet-measure-toggle {
6105
  width: 38px;
6106
  height: 38px;