diff --git a/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Edicao.cs b/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Edicao.cs
index ed500f3d1..b051cd1de 100644
--- a/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Edicao.cs
+++ b/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Edicao.cs
@@ -1001,6 +1001,228 @@ namespace OperationControl.Mapas
}
+ ///
+ /// Sugere um nome previsível para a versão persistida do
+ /// DocumentoTrabalho sem alterar o documento nem a sessão.
+ ///
+ public string ObterNomeArquivoSugeridoMapaEditado()
+ {
+ MapaDocumento? documento =
+ _estadoEdicao
+ .DocumentoTrabalho;
+
+
+ string nomeBase =
+ string.Empty;
+
+
+ if (
+ documento != null &&
+ !string.IsNullOrWhiteSpace(
+ documento.CaminhoOrigem
+ )
+ )
+ {
+ nomeBase =
+ System.IO.Path
+ .GetFileNameWithoutExtension(
+ documento.CaminhoOrigem
+ );
+ }
+
+
+ if (
+ string.IsNullOrWhiteSpace(
+ nomeBase
+ )
+ )
+ {
+ nomeBase =
+ documento?.Nome;
+ }
+
+
+ if (
+ string.IsNullOrWhiteSpace(
+ nomeBase
+ )
+ )
+ {
+ nomeBase =
+ "mapa";
+ }
+
+
+ char[] invalidos =
+ System.IO.Path
+ .GetInvalidFileNameChars();
+
+
+ nomeBase =
+ new string(
+ nomeBase
+ .Select(
+ c =>
+ invalidos.Contains(c)
+ ? '_'
+ : c
+ )
+ .ToArray()
+ )
+ .Trim();
+
+
+ if (
+ !nomeBase.EndsWith(
+ "_operacional",
+ StringComparison.OrdinalIgnoreCase
+ )
+ )
+ {
+ nomeBase +=
+ "_operacional";
+ }
+
+
+ return
+ nomeBase +
+ ".geojson";
+ }
+
+
+ ///
+ /// Retorna a pasta do arquivo que originou o mapa somente
+ /// como conveniência para o SaveFileDialog.
+ ///
+ public string? ObterDiretorioOrigemMapaEdicao()
+ {
+ string? caminhoOrigem =
+ _estadoEdicao
+ .DocumentoTrabalho?
+ .CaminhoOrigem;
+
+
+ if (
+ string.IsNullOrWhiteSpace(
+ caminhoOrigem
+ )
+ )
+ {
+ return null;
+ }
+
+
+ string? diretorio =
+ System.IO.Path
+ .GetDirectoryName(
+ caminhoOrigem
+ );
+
+
+ return
+ !string.IsNullOrWhiteSpace(
+ diretorio
+ ) &&
+ System.IO.Directory.Exists(
+ diretorio
+ )
+ ? diretorio
+ : null;
+ }
+
+
+ ///
+ /// Salva uma fotografia independente do DocumentoTrabalho atual.
+ ///
+ /// Não executa Apply, não fecha a sessão, não altera DocumentoFonte,
+ /// não altera o documento oficial, não limpa Undo/Redo e não toca
+ /// na seleção operacional.
+ ///
+ public bool SalvarMapaEditadoGeoJson(
+ string caminhoArquivo)
+ {
+ if (
+ !_estadoEdicao.EmEdicao ||
+ _estadoEdicao.DocumentoTrabalho == null
+ )
+ {
+ Log(
+ "[MAPA][EDICAO][SALVAR] " +
+ "Não existe sessão de edição ativa."
+ );
+
+
+ return false;
+ }
+
+
+ if (
+ string.IsNullOrWhiteSpace(
+ caminhoArquivo
+ )
+ )
+ {
+ throw new ArgumentException(
+ "O caminho para salvar o mapa não foi informado.",
+ nameof(caminhoArquivo)
+ );
+ }
+
+
+ /*
+ * ObterResultado devolve uma cópia profunda.
+ * O exportador jamais recebe DocumentoTrabalho diretamente.
+ */
+ MapaDocumento documentoSalvar =
+ _edicaoService
+ .ObterResultado(
+ _estadoEdicao
+ );
+
+
+ documentoSalvar.Modificado =
+ true;
+
+
+ /*
+ * Reaproveita a mesma fronteira que protege o Apply.
+ * Se a validação falhar, nenhum arquivo é gravado.
+ */
+ if (
+ !ValidarDocumentoAntesDoApply(
+ documentoSalvar
+ )
+ )
+ {
+ Log(
+ "[MAPA][EDICAO][SALVAR] " +
+ "Cancelado por falha de validação."
+ );
+
+
+ return false;
+ }
+
+
+ _exportService
+ .SalvarGeoJson(
+ documentoSalvar,
+ caminhoArquivo
+ );
+
+
+ Log(
+ "[MAPA][EDICAO][SALVAR] " +
+ $"OK | " +
+ $"Ruas={documentoSalvar.QuantidadeRuas} | " +
+ $"Arquivo={System.IO.Path.GetFileName(caminhoArquivo)}"
+ );
+
+
+ return true;
+ }
+
+
public MapaDocumento ObterResultadoEdicao()
{
return _edicaoService
diff --git a/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml b/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml
index c16c3e5c5..77b9a41e0 100644
--- a/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml
+++ b/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml
@@ -723,6 +723,16 @@
ToolTip="Restaura no editor o mapa que entrou originalmente no sistema. Ctrl+Z desfaz."
Click="btnRestaurarMapaFonte_Click"/>
+
+
diff --git a/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml.cs b/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml.cs
index 90dcbdab3..9c09b54c1 100644
--- a/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml.cs
+++ b/AgroBase/OperationControl/Mapas/Editing/Views/MapaEditorOverlay.xaml.cs
@@ -15,6 +15,7 @@ namespace OperationControl.Mapas.Editing.Views
public event Action? EscopoRecorteAlterado;
public event Action? BordaGnssAcaoSolicitada;
public event Action? RestaurarMapaFonteSolicitado;
+ public event Action? SalvarMapaEditadoSolicitado;
private bool _atualizandoEstado;
@@ -490,6 +491,12 @@ namespace OperationControl.Mapas.Editing.Views
}
+ private void btnSalvarMapaEditado_Click(object sender, RoutedEventArgs e)
+ {
+ SalvarMapaEditadoSolicitado?.Invoke();
+ }
+
+
private void rbGnssManual_Checked(object sender, RoutedEventArgs e)
{
if (_atualizandoEstado)
diff --git a/AgroBase/OperationControl/Mapas/Services/MapaExportService.cs b/AgroBase/OperationControl/Mapas/Services/MapaExportService.cs
index 7f60e6186..8702bb726 100644
--- a/AgroBase/OperationControl/Mapas/Services/MapaExportService.cs
+++ b/AgroBase/OperationControl/Mapas/Services/MapaExportService.cs
@@ -1,5 +1,8 @@
using AgroBase.Models;
+using Newtonsoft.Json;
using OperationControl.Mapas.Models;
+using System.IO;
+using System.Text;
namespace OperationControl.Mapas.Services
{
@@ -149,5 +152,163 @@ namespace OperationControl.Mapas.Services
return mapa;
}
+
+
+ ///
+ /// Persiste o documento canônico como GeoJSON operacional.
+ ///
+ /// O arquivo contém somente o mapa consolidado:
+ /// ruas, IDs, atributos e geometrias já editadas.
+ ///
+ /// Estado de UI, seleção da missão, Undo/Redo, borda GNSS
+ /// e rota de retorno não fazem parte deste arquivo.
+ ///
+ public void SalvarGeoJson(
+ MapaDocumento documento,
+ string caminhoArquivo)
+ {
+ if (documento == null)
+ {
+ throw new ArgumentNullException(
+ nameof(documento)
+ );
+ }
+
+
+ if (string.IsNullOrWhiteSpace(
+ caminhoArquivo))
+ {
+ throw new ArgumentException(
+ "O caminho do arquivo GeoJSON não foi informado.",
+ nameof(caminhoArquivo)
+ );
+ }
+
+
+ string caminhoCompleto =
+ Path.GetFullPath(
+ caminhoArquivo
+ );
+
+
+ string extensao =
+ Path.GetExtension(
+ caminhoCompleto
+ );
+
+
+ if (
+ !string.Equals(
+ extensao,
+ ".geojson",
+ StringComparison.OrdinalIgnoreCase
+ ) &&
+ !string.Equals(
+ extensao,
+ ".json",
+ StringComparison.OrdinalIgnoreCase
+ )
+ )
+ {
+ throw new InvalidOperationException(
+ "O mapa operacional deve ser salvo como .geojson ou .json."
+ );
+ }
+
+
+ string? diretorio =
+ Path.GetDirectoryName(
+ caminhoCompleto
+ );
+
+
+ if (
+ string.IsNullOrWhiteSpace(
+ diretorio
+ ) ||
+ !Directory.Exists(
+ diretorio
+ )
+ )
+ {
+ throw new DirectoryNotFoundException(
+ "A pasta escolhida para salvar o mapa não existe."
+ );
+ }
+
+
+ /*
+ * CriarMapaOperacional valida novamente o documento
+ * e garante o contrato canônico usado pelo rover:
+ *
+ * properties.Id == geometry.id
+ * coordinates = [longitude, latitude]
+ */
+ MapaFeatureCollectionModel mapa =
+ CriarMapaOperacional(
+ documento
+ );
+
+
+ string json =
+ JsonConvert.SerializeObject(
+ mapa,
+ Formatting.Indented
+ );
+
+
+ /*
+ * Escrita transacional simples:
+ * grava primeiro um temporário na mesma pasta e somente
+ * depois cria/substitui o arquivo final.
+ */
+ string caminhoTemporario =
+ Path.Combine(
+ diretorio,
+ "." +
+ Path.GetFileName(
+ caminhoCompleto
+ ) +
+ "." +
+ Guid.NewGuid().ToString("N") +
+ ".tmp"
+ );
+
+
+ try
+ {
+ File.WriteAllText(
+ caminhoTemporario,
+ json,
+ new UTF8Encoding(
+ encoderShouldEmitUTF8Identifier: false
+ )
+ );
+
+
+ File.Move(
+ caminhoTemporario,
+ caminhoCompleto,
+ overwrite: true
+ );
+ }
+ finally
+ {
+ if (File.Exists(
+ caminhoTemporario))
+ {
+ try
+ {
+ File.Delete(
+ caminhoTemporario
+ );
+ }
+ catch
+ {
+ // O arquivo final já salvo continua válido.
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
index 61c65d9c6..af22046da 100644
--- a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
+++ b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
@@ -74,6 +74,17 @@ namespace OperationControl.ViewModels
_viewOperacaoCenter = new OperacaoCenterView();
_viewOperacaoCenter._vm.RoverSelecionadoChanged += OnRoverSelecionadoChanged;
+ /*
+ * Salvar arquivo é uma intenção de UI.
+ * O cockpit só dispara o evento, o Dock abre o diálogo
+ * e o Controller continua dono do documento e da exportação.
+ */
+ _viewOperacaoCenter
+ .Mapa
+ .EditorOverlay
+ .SalvarMapaEditadoSolicitado +=
+ OnSalvarMapaEditadoSolicitado;
+
_viewOperacaoLeft = new OperacaoLeftView();
_viewOperacaoRight = new OperacaoRightView();
@@ -1152,7 +1163,7 @@ namespace OperationControl.ViewModels
string mensagem =
- "As alterações não foram aplicadas porque " +
+ "A ação foi cancelada porque " +
"o mapa editado não passou na validação." +
Environment.NewLine +
Environment.NewLine +
@@ -1173,6 +1184,121 @@ namespace OperationControl.ViewModels
}
+ private void OnSalvarMapaEditadoSolicitado()
+ {
+ try
+ {
+ if (!_mapas.EdicaoAtiva)
+ return;
+
+
+ string nomeSugerido =
+ _mapas
+ .ObterNomeArquivoSugeridoMapaEditado();
+
+
+ string? diretorioOrigem =
+ _mapas
+ .ObterDiretorioOrigemMapaEdicao();
+
+
+ var dialog =
+ new Microsoft.Win32.SaveFileDialog
+ {
+ Title =
+ "Salvar versão editada do mapa",
+
+ Filter =
+ "GeoJSON (*.geojson)|*.geojson",
+
+ DefaultExt =
+ ".geojson",
+
+ AddExtension =
+ true,
+
+ FileName =
+ nomeSugerido,
+
+ OverwritePrompt =
+ true
+ };
+
+
+ if (
+ !string.IsNullOrWhiteSpace(
+ diretorioOrigem
+ ) &&
+ System.IO.Directory.Exists(
+ diretorioOrigem
+ )
+ )
+ {
+ dialog.InitialDirectory =
+ diretorioOrigem;
+ }
+
+
+ bool? resultado =
+ dialog.ShowDialog();
+
+
+ if (resultado != true)
+ return;
+
+
+ bool salvo =
+ _mapas
+ .SalvarMapaEditadoGeoJson(
+ dialog.FileName
+ );
+
+
+ if (!salvo)
+ return;
+
+
+ Models.Variaveis.MostrarLog(
+ "[MAPA][EDICAO][SALVAR][UI] " +
+ dialog.FileName
+ );
+
+
+ MessageBox.Show(
+ "Mapa operacional salvo com sucesso." +
+ Environment.NewLine +
+ Environment.NewLine +
+ dialog.FileName +
+ Environment.NewLine +
+ Environment.NewLine +
+ "A edição continua aberta e o histórico " +
+ "Desfazer/Refazer foi preservado.",
+ "Mapa salvo",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ }
+ catch (Exception ex)
+ {
+ Models.Variaveis.MostrarLog(
+ "[MAPA][EDICAO][SALVAR][ERRO] " +
+ ex
+ );
+
+
+ MessageBox.Show(
+ "Não foi possível salvar o mapa editado." +
+ Environment.NewLine +
+ Environment.NewLine +
+ ex.Message,
+ "Erro ao salvar mapa",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ }
+
+
/*
* Wrappers públicos mantidos porque existem consumidores
* externos ao Dock que já chamam essas APIs.