This commit is contained in:
Diego Freitas 2026-09-16 15:45:46 -03:00
commit bef19294e5
5 changed files with 527 additions and 1 deletions

View File

@ -1001,6 +1001,228 @@ namespace OperationControl.Mapas
}
/// <summary>
/// Sugere um nome previsível para a versão persistida do
/// DocumentoTrabalho sem alterar o documento nem a sessão.
/// </summary>
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";
}
/// <summary>
/// Retorna a pasta do arquivo que originou o mapa somente
/// como conveniência para o SaveFileDialog.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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

View File

@ -723,6 +723,16 @@
ToolTip="Restaura no editor o mapa que entrou originalmente no sistema. Ctrl+Z desfaz."
Click="btnRestaurarMapaFonte_Click"/>
<Button
x:Name="btnSalvarMapaEditado"
Content="💾 Salvar versão editada"
Height="40"
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Style="{StaticResource ApplyButtonStyle}"
ToolTip="Salva a geometria atual do editor em GeoJSON sem aplicar nem encerrar a edição."
Click="btnSalvarMapaEditado_Click"/>
</StackPanel>
<!-- STATUS -->

View File

@ -15,6 +15,7 @@ namespace OperationControl.Mapas.Editing.Views
public event Action<MapaEscopoRecorte>? EscopoRecorteAlterado;
public event Action<MapaAcaoBordaGnss>? 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)

View File

@ -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;
}
/// <summary>
/// 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.
/// </summary>
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.
}
}
}
}
}
}

View File

@ -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.