diff --git a/AgroBase/OperationControl/Controls/MapViewControl.xaml b/AgroBase/OperationControl/Controls/MapViewControl.xaml
index e7359bdc2..77ba2cc29 100644
--- a/AgroBase/OperationControl/Controls/MapViewControl.xaml
+++ b/AgroBase/OperationControl/Controls/MapViewControl.xaml
@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:Mapsui.UI.Wpf;assembly=Mapsui.UI.Wpf"
xmlns:editing="clr-namespace:OperationControl.Mapas.Editing.Views"
+ xmlns:creation="clr-namespace:OperationControl.Mapas.Creation.Views"
x:Name="Root"
MinHeight="100"
MinWidth="300"
@@ -91,5 +92,8 @@
+
+
+
\ No newline at end of file
diff --git a/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Criacao.cs b/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Criacao.cs
new file mode 100644
index 000000000..3db5ba518
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Controller/MapaOperacaoController.Criacao.cs
@@ -0,0 +1,1514 @@
+using AgroBase.Models;
+using Mapsui.Layers;
+using OperationControl.Controls;
+using OperationControl.Mapas.Creation;
+using OperationControl.Mapas.Interaction;
+using OperationControl.Mapas.Models;
+using OperationControl.Mapas.Rendering;
+using System.Threading;
+using static AgroBase.Models.Enums;
+
+namespace OperationControl.Mapas
+{
+ public sealed partial class MapaOperacaoController
+ {
+ /*
+ * =========================================================
+ * CRIAÇÃO DE MAPA
+ * =========================================================
+ *
+ * Este partial é deliberadamente separado do editor.
+ *
+ * Editor:
+ * MapaDocumento existente -> DocumentoTrabalho.
+ *
+ * Criação:
+ * levantamento GNSS -> estado transitório -> MapaDocumento.
+ *
+ * Durante este degrau o resultado ainda NÃO é salvo nem
+ * instalado no Workspace. Isso entra no cockpit/salvamento.
+ * =========================================================
+ */
+
+
+ public event Action?
+ EstadoCriacaoAlterado;
+
+
+ private readonly
+ MapaEstadoCriacao
+ _estadoCriacao =
+ new();
+
+
+ private readonly
+ MapaCriacaoService
+ _criacaoService =
+ new();
+
+
+ private readonly
+ MapaCriacaoRenderService
+ _criacaoRenderService =
+ new();
+
+
+ private readonly Dictionary<
+ MapViewControl,
+ MemoryLayer>
+ _layersCriacao =
+ new();
+
+
+ private readonly HashSet<
+ MapViewControl>
+ _mapasEmCriacao =
+ new();
+
+
+ private readonly Dictionary<
+ MapViewControl,
+ MapaModoInteracao>
+ _modoInteracaoAntesCriacao =
+ new();
+
+
+ private int
+ _capturaAutomaticaCriacaoUiAgendada;
+
+
+ private int
+ _atualizacaoCriacaoGnssUiAgendada;
+
+
+ private long
+ _ultimaAtualizacaoCriacaoGnssUiMs;
+
+
+ public bool CriacaoMapaAtiva =>
+ _estadoCriacao
+ .EmCriacao;
+
+
+ public bool CapturaCriacaoAutomaticaAtiva =>
+ _estadoCriacao.EmCriacao &&
+ _estadoCriacao.GravacaoAutomaticaAtiva;
+
+
+ public MapaEstadoCriacao
+ EstadoCriacaoMapa =>
+ _estadoCriacao;
+
+
+ /*
+ * =========================================================
+ * ENTRADA GNSS LOCAL
+ * =========================================================
+ */
+
+ ///
+ /// Entrada explícita do GNSS local para as ferramentas
+ /// de levantamento do mapa.
+ ///
+ /// O Dock chama este método com o mesmo GPSModel que já
+ /// entrega à representação da Base.
+ ///
+ /// AtualizarSnapshotGnssLocal pertence ao partial de
+ /// Borda GNSS e já faz Clone(), portanto não retemos a
+ /// referência viva recebida da camada de serviço.
+ ///
+ public void AtualizarGnssLocal(
+ GPSModel? dados)
+ {
+ if (dados == null)
+ return;
+
+
+ AtualizarSnapshotGnssLocal(
+ dados
+ );
+
+
+ AgendarCapturaAutomaticaCriacaoSeNecessario();
+ AgendarAtualizacaoCockpitCriacaoGnss();
+ }
+
+
+ private void AgendarAtualizacaoCockpitCriacaoGnss()
+ {
+ if (!_estadoCriacao.EmCriacao)
+ return;
+
+ long agoraMs = Environment.TickCount64;
+ long ultimaMs = Interlocked.Read(
+ ref _ultimaAtualizacaoCriacaoGnssUiMs
+ );
+
+ // Status visual limitado a aproximadamente 4 Hz.
+ if (agoraMs - ultimaMs < 250)
+ return;
+
+ if (Interlocked.Exchange(
+ ref _atualizacaoCriacaoGnssUiAgendada,
+ 1
+ ) != 0)
+ {
+ return;
+ }
+
+ var dispatcher =
+ System.Windows.Application.Current?.Dispatcher;
+
+ if (dispatcher == null)
+ {
+ Interlocked.Exchange(
+ ref _atualizacaoCriacaoGnssUiAgendada,
+ 0
+ );
+ return;
+ }
+
+ dispatcher.BeginInvoke(
+ new Action(() =>
+ {
+ try
+ {
+ if (!_estadoCriacao.EmCriacao)
+ return;
+
+ Interlocked.Exchange(
+ ref _ultimaAtualizacaoCriacaoGnssUiMs,
+ Environment.TickCount64
+ );
+
+ EstadoCriacaoAlterado?.Invoke();
+ }
+ finally
+ {
+ Interlocked.Exchange(
+ ref _atualizacaoCriacaoGnssUiAgendada,
+ 0
+ );
+ }
+ }),
+ System.Windows.Threading.DispatcherPriority.Background
+ );
+ }
+
+
+ /*
+ * =========================================================
+ * SESSÃO
+ * =========================================================
+ */
+
+ public void IniciarCriacaoMapa(
+ MapViewControl? mapaControl,
+ string? nome,
+ TipoMapaOperacao tipo,
+ double distanciaMinimaAutomaticaMetros = 0.50)
+ {
+ if (
+ mapaControl?.Mapa?.Map == null
+ )
+ {
+ throw new ArgumentNullException(
+ nameof(mapaControl)
+ );
+ }
+
+
+ if (_estadoEdicao.EmEdicao)
+ {
+ throw new InvalidOperationException(
+ "Finalize ou cancele a edição atual antes de criar um novo mapa."
+ );
+ }
+
+
+ if (_estadoCriacao.EmCriacao)
+ {
+ throw new InvalidOperationException(
+ "Já existe uma sessão de criação de mapa ativa."
+ );
+ }
+
+
+ _criacaoService
+ .Iniciar(
+ _estadoCriacao,
+ nome,
+ tipo,
+ distanciaMinimaAutomaticaMetros
+ );
+
+
+ RegistrarMapa(
+ mapaControl
+ );
+
+
+ _modoInteracaoAntesCriacao[
+ mapaControl
+ ] =
+ mapaControl
+ .ModoInteracao;
+
+
+ _mapasEmCriacao
+ .Add(
+ mapaControl
+ );
+
+
+ /*
+ * A criação não depende de clique geográfico.
+ * O operador continua podendo pan/zoom normalmente.
+ */
+ DefinirModoInteracao(
+ mapaControl,
+ MapaModoInteracao
+ .Visualizacao
+ );
+
+
+ /*
+ * Se já existia um mapa oficial carregado,
+ * escondemos SOMENTE o render desta View.
+ *
+ * O Workspace permanece intacto e será restaurado
+ * se a criação for cancelada.
+ */
+ OcultarWorkspaceDuranteCriacao(
+ mapaControl
+ );
+
+
+ RenderizarCriacaoNoMapa(
+ mapaControl
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ "Sessão iniciada | " +
+ $"Tipo={tipo} | " +
+ $"DistAuto={distanciaMinimaAutomaticaMetros:F2} m"
+ );
+ }
+
+
+ public void CancelarCriacaoMapa()
+ {
+ if (!_estadoCriacao.EmCriacao)
+ return;
+
+
+ MapViewControl[]
+ mapas =
+ _mapasEmCriacao
+ .ToArray();
+
+
+ /*
+ * Cancela primeiro o gravador para qualquer delegate
+ * de UI já enfileirado falhar nas guardas internas.
+ */
+ _criacaoService
+ .PararGravacaoAutomatica(
+ _estadoCriacao,
+ out _
+ );
+
+
+ _criacaoService
+ .Cancelar(
+ _estadoCriacao
+ );
+
+
+ foreach (
+ MapViewControl mapa
+ in mapas)
+ {
+ RemoverRenderCriacao(
+ mapa
+ );
+
+
+ MapaModoInteracao
+ modoRestaurar =
+ _modoInteracaoAntesCriacao
+ .TryGetValue(
+ mapa,
+ out MapaModoInteracao anterior
+ )
+ ? anterior
+ : MapaModoInteracao
+ .Visualizacao;
+
+
+ _mapasEmCriacao
+ .Remove(
+ mapa
+ );
+
+
+ _modoInteracaoAntesCriacao
+ .Remove(
+ mapa
+ );
+
+
+ PrepararMapa(
+ mapa,
+ modoRestaurar,
+ enquadrarDocumento:
+ false
+ );
+ }
+
+
+ _mapasEmCriacao
+ .Clear();
+
+ _modoInteracaoAntesCriacao
+ .Clear();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ "Sessão cancelada."
+ );
+ }
+
+
+ public void DefinirNomeCriacaoMapa(
+ string? nome)
+ {
+ _criacaoService
+ .DefinirNome(
+ _estadoCriacao,
+ nome
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+ }
+
+
+ /*
+ * =========================================================
+ * CAPTURA MANUAL
+ * =========================================================
+ */
+
+ public bool RegistrarPontoCriacaoManual()
+ {
+ if (!_estadoCriacao.EmCriacao)
+ return false;
+
+
+ GPSModel?
+ leitura =
+ ObterSnapshotGnssLocal();
+
+
+ if (
+ !TentarValidarLeituraGnss(
+ leitura,
+ out double idade,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][PONTO][IGNORADO] " +
+ motivo
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ return false;
+ }
+
+
+ MapaPontoCriacao
+ ponto =
+ CriarPontoCriacao(
+ leitura!
+ );
+
+
+ bool adicionado =
+ _criacaoService
+ .TentarAdicionarPontoManual(
+ _estadoCriacao,
+ ponto,
+ out motivo
+ );
+
+
+ if (!adicionado)
+ {
+ Log(
+ "[MAPA][CRIACAO][PONTO][IGNORADO] " +
+ motivo
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ return false;
+ }
+
+
+ AtualizarVisualCriacao();
+
+
+ if (_estadoCriacao.QuantidadePontosTotal == 1)
+ {
+ EnquadrarCriacaoNoMapa();
+ }
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO][PONTO] " +
+ $"Rua={_estadoCriacao.RuaAtual?.Numero ?? 0} | " +
+ $"N={_estadoCriacao.RuaAtual?.QuantidadePontos ?? 0} | " +
+ $"Fix={leitura!.QualidadeFix} | " +
+ $"Idade={idade:F2}s"
+ );
+
+
+ return true;
+ }
+
+
+ public bool RemoverUltimoPontoCriacao()
+ {
+ if (
+ !_criacaoService
+ .TentarRemoverUltimoPonto(
+ _estadoCriacao,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][REMOVER-PONTO][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ AtualizarVisualCriacao();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ "Último ponto da rua atual removido."
+ );
+
+
+ return true;
+ }
+
+
+ /*
+ * =========================================================
+ * RUAS
+ * =========================================================
+ */
+
+ public bool CriarNovaRuaMapa()
+ {
+ if (
+ !_criacaoService
+ .TentarCriarNovaRua(
+ _estadoCriacao,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][NOVA-RUA][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ AtualizarVisualCriacao();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ $"Nova rua | Atual={_estadoCriacao.RuaAtual?.Numero ?? 0}"
+ );
+
+
+ return true;
+ }
+
+
+ ///
+ /// Número da rua é 1-based para ficar natural para a UI.
+ ///
+ public bool SelecionarRuaCriacao(
+ int numeroRua)
+ {
+ int indice =
+ numeroRua - 1;
+
+
+ if (
+ !_criacaoService
+ .TentarSelecionarRua(
+ _estadoCriacao,
+ indice,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][SELECIONAR-RUA][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ AtualizarVisualCriacao();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ return true;
+ }
+
+
+ ///
+ /// Número da rua é 1-based para ficar natural para a UI.
+ ///
+ public bool ExcluirRuaCriacao(
+ int numeroRua)
+ {
+ int indice =
+ numeroRua - 1;
+
+
+ if (
+ !_criacaoService
+ .TentarExcluirRua(
+ _estadoCriacao,
+ indice,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][EXCLUIR-RUA][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ AtualizarVisualCriacao();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ $"Rua removida | Ruas={_estadoCriacao.QuantidadeRuas}"
+ );
+
+
+ return true;
+ }
+
+
+ public void LimparLevantamentoCriacao()
+ {
+ _criacaoService
+ .LimparPontos(
+ _estadoCriacao
+ );
+
+
+ AtualizarVisualCriacao();
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO] " +
+ "Levantamento limpo."
+ );
+ }
+
+
+ /*
+ * =========================================================
+ * MODO AUTOMÁTICO
+ * =========================================================
+ */
+
+ public bool DefinirModoCapturaCriacao(
+ MapaModoCapturaCriacao modo)
+ {
+ bool alterado =
+ _criacaoService
+ .DefinirModoCaptura(
+ _estadoCriacao,
+ modo,
+ out string motivo
+ );
+
+
+ if (!alterado)
+ {
+ if (
+ !string.IsNullOrWhiteSpace(
+ motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][MODO][IGNORADO] " +
+ motivo
+ );
+ }
+
+ return false;
+ }
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO][MODO] " +
+ $"Atual={modo}"
+ );
+
+
+ return true;
+ }
+
+
+ public bool IniciarGravacaoAutomaticaCriacao()
+ {
+ if (!_estadoCriacao.EmCriacao)
+ return false;
+
+
+ GPSModel?
+ leitura =
+ ObterSnapshotGnssLocal();
+
+
+ if (
+ !TentarValidarLeituraGnss(
+ leitura,
+ out double idade,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][AUTO][INICIO][IGNORADO] " +
+ motivo
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ return false;
+ }
+
+
+ if (
+ !_criacaoService
+ .IniciarGravacaoAutomatica(
+ _estadoCriacao,
+ out motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][AUTO][INICIO][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ /*
+ * Mesmo comportamento já validado na borda GNSS:
+ * a posição do clique em "Iniciar" entra imediatamente.
+ */
+ ProcessarPontoAutomaticoCriacao(
+ leitura!,
+ forcarPrimeiroPonto:
+ true
+ );
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO][AUTO] " +
+ "Gravação iniciada | " +
+ $"Rua={_estadoCriacao.RuaAtual?.Numero ?? 0} | " +
+ $"DistMin={_estadoCriacao.DistanciaMinimaCapturaAutomaticaMetros:F2} m | " +
+ $"Fix={leitura!.QualidadeFix} | " +
+ $"Idade={idade:F2}s"
+ );
+
+
+ return true;
+ }
+
+
+ public bool FinalizarGravacaoAutomaticaCriacao()
+ {
+ if (
+ !_criacaoService
+ .PararGravacaoAutomatica(
+ _estadoCriacao,
+ out string motivo
+ )
+ )
+ {
+ Log(
+ "[MAPA][CRIACAO][AUTO][FIM][IGNORADO] " +
+ motivo
+ );
+
+ return false;
+ }
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO][AUTO] " +
+ $"Gravação finalizada | " +
+ $"Rua={_estadoCriacao.RuaAtual?.Numero ?? 0} | " +
+ $"Pontos={_estadoCriacao.RuaAtual?.QuantidadePontos ?? 0}"
+ );
+
+
+ return true;
+ }
+
+
+ private void ProcessarPontoAutomaticoCriacao(
+ GPSModel leitura,
+ bool forcarPrimeiroPonto = false)
+ {
+ if (
+ leitura == null ||
+ !_estadoCriacao.EmCriacao ||
+ !_estadoCriacao.GravacaoAutomaticaAtiva
+ )
+ {
+ return;
+ }
+
+
+ if (
+ !TentarValidarLeituraGnss(
+ leitura,
+ out double idade,
+ out _
+ )
+ )
+ {
+ return;
+ }
+
+
+ MapaPontoCriacao
+ ponto =
+ CriarPontoCriacao(
+ leitura
+ );
+
+
+ bool adicionado =
+ _criacaoService
+ .TentarAdicionarPontoAutomatico(
+ _estadoCriacao,
+ ponto,
+ forcarPrimeiroPonto,
+ out _
+ );
+
+
+ if (!adicionado)
+ return;
+
+
+ AtualizarVisualCriacao();
+
+
+ if (_estadoCriacao.QuantidadePontosTotal == 1)
+ {
+ EnquadrarCriacaoNoMapa();
+ }
+
+
+ EstadoCriacaoAlterado?
+ .Invoke();
+
+
+ Log(
+ "[MAPA][CRIACAO][AUTO][PONTO] " +
+ $"Rua={_estadoCriacao.RuaAtual?.Numero ?? 0} | " +
+ $"N={_estadoCriacao.RuaAtual?.QuantidadePontos ?? 0} | " +
+ $"Fix={leitura.QualidadeFix} | " +
+ $"Idade={idade:F2}s"
+ );
+ }
+
+
+ private void AgendarCapturaAutomaticaCriacaoSeNecessario()
+ {
+ if (
+ !_estadoCriacao.EmCriacao ||
+ !_estadoCriacao.GravacaoAutomaticaAtiva
+ )
+ {
+ return;
+ }
+
+
+ if (
+ Interlocked.Exchange(
+ ref _capturaAutomaticaCriacaoUiAgendada,
+ 1
+ ) != 0
+ )
+ {
+ return;
+ }
+
+
+ var dispatcher =
+ System.Windows
+ .Application
+ .Current?
+ .Dispatcher;
+
+
+ if (dispatcher == null)
+ {
+ Interlocked.Exchange(
+ ref _capturaAutomaticaCriacaoUiAgendada,
+ 0
+ );
+
+ return;
+ }
+
+
+ dispatcher.BeginInvoke(
+ new Action(
+ () =>
+ {
+ try
+ {
+ /*
+ * Guardas repetidas de propósito.
+ *
+ * Se o operador finalizou a gravação
+ * depois do agendamento, nenhum ponto
+ * fantasma pode aparecer.
+ */
+ if (
+ !_estadoCriacao.EmCriacao ||
+ !_estadoCriacao
+ .GravacaoAutomaticaAtiva
+ )
+ {
+ return;
+ }
+
+
+ GPSModel?
+ leitura =
+ ObterSnapshotGnssLocal();
+
+
+ if (leitura == null)
+ return;
+
+
+ if (
+ !_estadoCriacao.EmCriacao ||
+ !_estadoCriacao
+ .GravacaoAutomaticaAtiva
+ )
+ {
+ return;
+ }
+
+
+ ProcessarPontoAutomaticoCriacao(
+ leitura
+ );
+ }
+ finally
+ {
+ Interlocked.Exchange(
+ ref _capturaAutomaticaCriacaoUiAgendada,
+ 0
+ );
+ }
+ }
+ ),
+ System.Windows.Threading
+ .DispatcherPriority
+ .Background
+ );
+ }
+
+
+ /*
+ * =========================================================
+ * RESUMO / RESULTADO
+ * =========================================================
+ */
+
+ public MapaResumoCriacao
+ ObterResumoCriacaoMapa()
+ {
+ MapaResumoCriacao
+ baseResumo =
+ _criacaoService
+ .CriarResumo(
+ _estadoCriacao
+ );
+
+
+ GPSModel?
+ leitura =
+ ObterSnapshotGnssLocal();
+
+
+ bool leituraValida =
+ TentarValidarLeituraGnss(
+ leitura,
+ out double idade,
+ out string motivo
+ );
+
+
+ bool qualidadeAlta =
+ leitura != null &&
+ (
+ leitura.QualidadeFix ==
+ TiposCorrecaoGPS.RTKFixo
+ ||
+ leitura.QualidadeFix ==
+ TiposCorrecaoGPS.BaseFix
+ );
+
+
+ string status =
+ !leituraValida
+ ? motivo
+ : $"{DescreverFix(leitura!.QualidadeFix)}" +
+ $" • {leitura.NumeroSatelites} sat" +
+ $" • {idade:F1} s" +
+ (
+ double.IsFinite(
+ leitura.PrecisaoCm
+ )
+ ? $" • ~{leitura.PrecisaoCm:F1} cm"
+ : string.Empty
+ );
+
+
+ return new MapaResumoCriacao
+ {
+ EmCriacao =
+ baseResumo.EmCriacao,
+
+ Nome =
+ baseResumo.Nome,
+
+ RuaAtual =
+ baseResumo.RuaAtual,
+
+ QuantidadeRuas =
+ baseResumo.QuantidadeRuas,
+
+ QuantidadeRuasValidas =
+ baseResumo.QuantidadeRuasValidas,
+
+ PontosRuaAtual =
+ baseResumo.PontosRuaAtual,
+
+ PontosTotal =
+ baseResumo.PontosTotal,
+
+ ComprimentoRuaAtualMetros =
+ baseResumo.ComprimentoRuaAtualMetros,
+
+ ComprimentoTotalMetros =
+ baseResumo.ComprimentoTotalMetros,
+
+ ModoCaptura =
+ baseResumo.ModoCaptura,
+
+ GravacaoAutomaticaAtiva =
+ baseResumo.GravacaoAutomaticaAtiva,
+
+ DistanciaMinimaAutomaticaMetros =
+ baseResumo.DistanciaMinimaAutomaticaMetros,
+
+ PodeNovaRua =
+ baseResumo.PodeNovaRua,
+
+ PodeSalvar =
+ baseResumo.PodeSalvar,
+
+ Ruas =
+ baseResumo.Ruas,
+
+ LeituraGnssValida =
+ leituraValida,
+
+ QualidadeAlta =
+ qualidadeAlta,
+
+ StatusGnss =
+ status
+ };
+ }
+
+
+ ///
+ /// Constrói uma fotografia canônica do levantamento.
+ ///
+ /// Ainda não altera o Workspace e ainda não salva arquivo.
+ ///
+ public MapaDocumento
+ ObterDocumentoCriadoMapa()
+ {
+ return
+ _criacaoService
+ .CriarDocumento(
+ _estadoCriacao
+ );
+ }
+
+
+ public string ObterNomeArquivoSugeridoMapaCriado()
+ {
+ string nome = _estadoCriacao.Nome;
+
+ if (string.IsNullOrWhiteSpace(nome))
+ nome = "NovoMapa";
+
+ char[] invalidos = System.IO.Path.GetInvalidFileNameChars();
+
+ nome = new string(
+ nome.Trim()
+ .Select(c => invalidos.Contains(c) ? '_' : c)
+ .ToArray()
+ );
+
+ return nome + ".geojson";
+ }
+
+
+ ///
+ /// levantamento -> documento -> GeoJSON -> importação normal -> Workspace.
+ /// A sessão permanece aberta se validar/salvar/importar falhar.
+ ///
+ public MapaDocumento SalvarECarregarMapaCriado(string caminhoArquivo)
+ {
+ if (!_estadoCriacao.EmCriacao)
+ {
+ throw new InvalidOperationException(
+ "Não existe uma criação de mapa ativa."
+ );
+ }
+
+ if (string.IsNullOrWhiteSpace(caminhoArquivo))
+ {
+ throw new ArgumentException(
+ "O caminho do arquivo não foi informado.",
+ nameof(caminhoArquivo)
+ );
+ }
+
+ MapaDocumento documentoCriado =
+ _criacaoService.CriarDocumento(_estadoCriacao);
+
+ // Primeiro persiste. Se falhar, o levantamento continua aberto.
+ _exportService.SalvarGeoJson(
+ documentoCriado,
+ caminhoArquivo
+ );
+
+ // O importador valida o arquivo antes de trocar o Workspace.
+ MapaDocumento carregado = CarregarMapaArquivo(
+ caminhoArquivo,
+ documentoCriado.Tipo
+ );
+
+ MapViewControl[] mapas = _mapasEmCriacao.ToArray();
+
+ var modosRestaurar = mapas.ToDictionary(
+ mapa => mapa,
+ mapa => _modoInteracaoAntesCriacao.TryGetValue(
+ mapa,
+ out MapaModoInteracao anterior
+ )
+ ? anterior
+ : MapaModoInteracao.Visualizacao
+ );
+
+ _criacaoService.Cancelar(_estadoCriacao);
+
+ foreach (MapViewControl mapa in mapas)
+ {
+ RemoverRenderCriacao(mapa);
+ _mapasEmCriacao.Remove(mapa);
+ _modoInteracaoAntesCriacao.Remove(mapa);
+
+ PrepararMapa(
+ mapa,
+ modosRestaurar[mapa],
+ enquadrarDocumento: true
+ );
+ }
+
+ _mapasEmCriacao.Clear();
+ _modoInteracaoAntesCriacao.Clear();
+
+ EstadoCriacaoAlterado?.Invoke();
+
+ Log(
+ "[MAPA][CRIACAO][SALVAR] " +
+ $"OK | Ruas={carregado.QuantidadeRuas} | " +
+ $"Arquivo={System.IO.Path.GetFileName(caminhoArquivo)}"
+ );
+
+ return carregado;
+ }
+
+
+ private void EnquadrarCriacaoNoMapa()
+ {
+ List pontos =
+ _estadoCriacao.Ruas
+ .SelectMany(rua => rua.Pontos)
+ .ToList();
+
+ if (pontos.Count == 0)
+ return;
+
+ double minLat = pontos.Min(x => x.Latitude);
+ double maxLat = pontos.Max(x => x.Latitude);
+ double minLon = pontos.Min(x => x.Longitude);
+ double maxLon = pontos.Max(x => x.Longitude);
+
+ foreach (MapViewControl mapa in _mapasEmCriacao.ToArray())
+ {
+ mapa.EnquadrarAreaWgs84(
+ minLongitude: minLon,
+ minLatitude: minLat,
+ maxLongitude: maxLon,
+ maxLatitude: maxLat,
+ margem: 1.35
+ );
+ }
+ }
+
+
+ private static MapaPontoCriacao
+ CriarPontoCriacao(
+ GPSModel leitura)
+ {
+ return new MapaPontoCriacao
+ {
+ Latitude =
+ leitura.Latitude,
+
+ Longitude =
+ leitura.Longitude,
+
+ Altitude =
+ leitura.Altitude,
+
+ PrecisaoCm =
+ leitura.PrecisaoCm,
+
+ NumeroSatelites =
+ leitura.NumeroSatelites,
+
+ QualidadeFix =
+ leitura.QualidadeFix,
+
+ Momento =
+ leitura.Momento
+ };
+ }
+
+
+ /*
+ * =========================================================
+ * RENDER
+ * =========================================================
+ */
+
+ private void AtualizarVisualCriacao()
+ {
+ foreach (
+ MapViewControl mapa
+ in _mapasEmCriacao
+ .ToArray())
+ {
+ RenderizarCriacaoNoMapa(
+ mapa
+ );
+
+
+ mapa
+ .Mapa
+ .Refresh();
+ }
+ }
+
+
+ private void RenderizarCriacaoNoMapa(
+ MapViewControl mapaControl)
+ {
+ if (
+ mapaControl?.Mapa?.Map == null
+ )
+ {
+ return;
+ }
+
+
+ if (
+ !_layersCriacao
+ .TryGetValue(
+ mapaControl,
+ out MemoryLayer? camada
+ )
+ )
+ {
+ camada =
+ _criacaoRenderService
+ .CriarCamada(
+ _estadoCriacao
+ );
+
+
+ mapaControl
+ .Mapa
+ .Map
+ .Layers
+ .Add(
+ camada
+ );
+
+
+ _layersCriacao[
+ mapaControl
+ ] =
+ camada;
+ }
+ else
+ {
+ _criacaoRenderService
+ .AtualizarCamada(
+ camada,
+ _estadoCriacao
+ );
+ }
+
+
+ /*
+ * Mantém preview acima das ruas operacionais.
+ */
+ var layers =
+ mapaControl
+ .Mapa
+ .Map
+ .Layers;
+
+
+ if (
+ layers.Contains(
+ camada
+ )
+ )
+ {
+ layers.Remove(
+ camada
+ );
+
+ layers.Add(
+ camada
+ );
+ }
+
+
+ /*
+ * Base/Rover continuam visualmente no topo.
+ */
+ GarantirEntidadesNoTopo(
+ mapaControl
+ );
+
+
+ mapaControl
+ .Mapa
+ .Refresh();
+ }
+
+
+ private void RemoverRenderCriacao(
+ MapViewControl mapaControl)
+ {
+ if (
+ mapaControl?.Mapa?.Map == null
+ )
+ {
+ return;
+ }
+
+
+ if (
+ _layersCriacao
+ .TryGetValue(
+ mapaControl,
+ out MemoryLayer? camada
+ )
+ )
+ {
+ mapaControl
+ .Mapa
+ .Map
+ .Layers
+ .Remove(
+ camada
+ );
+
+
+ _layersCriacao
+ .Remove(
+ mapaControl
+ );
+ }
+
+
+ mapaControl
+ .Mapa
+ .Refresh();
+ }
+
+
+ private void OcultarWorkspaceDuranteCriacao(
+ MapViewControl mapaControl)
+ {
+ if (
+ mapaControl?.Mapa?.Map == null
+ )
+ {
+ return;
+ }
+
+
+ if (
+ _rendersMapa
+ .TryGetValue(
+ mapaControl,
+ out MapaRenderResult? render
+ )
+ )
+ {
+ mapaControl
+ .Mapa
+ .Map
+ .Layers
+ .Remove(
+ render.Layer
+ );
+
+
+ _rendersMapa
+ .Remove(
+ mapaControl
+ );
+
+
+ mapaControl
+ .DefinirCamadaMapaNovo(
+ null
+ );
+ }
+
+
+ mapaControl
+ .Mapa
+ .Refresh();
+ }
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaCriacaoService.cs b/AgroBase/OperationControl/Mapas/Creation/MapaCriacaoService.cs
new file mode 100644
index 000000000..e92d5e0dd
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaCriacaoService.cs
@@ -0,0 +1,844 @@
+using NetTopologySuite.Geometries;
+using OperationControl.Mapas.Models;
+using OperationControl.Mapas.Services;
+using System.Globalization;
+using static AgroBase.Models.Enums;
+
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Backend puro da criação de mapas por levantamento GNSS.
+ ///
+ /// Não conhece WPF, Mapsui, Dock, Rover ou GpsService.
+ /// A futura camada Controller entrega snapshots GNSS válidos
+ /// e pede operações a este serviço.
+ ///
+ public sealed class MapaCriacaoService
+ {
+ private const double DistanciaMinimaPontoManualMetros = 0.02;
+
+ private readonly MapaValidationService _validationService;
+
+ private readonly GeometryFactory _geometryFactory =
+ new(
+ new PrecisionModel(),
+ 4326
+ );
+
+ public MapaCriacaoService(
+ MapaValidationService? validationService = null)
+ {
+ _validationService =
+ validationService
+ ?? new MapaValidationService();
+ }
+
+
+ public void Iniciar(
+ MapaEstadoCriacao estado,
+ string? nome,
+ TipoMapaOperacao tipo,
+ double distanciaMinimaAutomaticaMetros = 0.50)
+ {
+ ValidarEstado(estado);
+
+ if (estado.EmCriacao)
+ {
+ throw new InvalidOperationException(
+ "Já existe uma sessão de criação de mapa ativa."
+ );
+ }
+
+ if (
+ !double.IsFinite(distanciaMinimaAutomaticaMetros) ||
+ distanciaMinimaAutomaticaMetros <= 0
+ )
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(distanciaMinimaAutomaticaMetros),
+ "A distância mínima automática deve ser maior que zero."
+ );
+ }
+
+ estado.Limpar();
+
+ estado.EmCriacao = true;
+ estado.Nome = NormalizarNome(nome);
+ estado.Tipo = tipo;
+ estado.DistanciaMinimaCapturaAutomaticaMetros =
+ distanciaMinimaAutomaticaMetros;
+ estado.ModoCaptura = MapaModoCapturaCriacao.Manual;
+ estado.GravacaoAutomaticaAtiva = false;
+
+ estado.RuasMutaveis.Add(
+ new MapaRuaCriacao
+ {
+ Numero = 1
+ }
+ );
+
+ estado.IndiceRuaAtual = 0;
+ }
+
+
+ public void Cancelar(
+ MapaEstadoCriacao estado)
+ {
+ ValidarEstado(estado);
+ estado.Limpar();
+ }
+
+
+ public void DefinirNome(
+ MapaEstadoCriacao estado,
+ string? nome)
+ {
+ ValidarEstadoAtivo(estado);
+ estado.Nome = NormalizarNome(nome);
+ }
+
+
+ public bool DefinirModoCaptura(
+ MapaEstadoCriacao estado,
+ MapaModoCapturaCriacao modo,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de trocar o modo.";
+ return false;
+ }
+
+ estado.ModoCaptura = modo;
+ return true;
+ }
+
+
+ public bool IniciarGravacaoAutomatica(
+ MapaEstadoCriacao estado,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (
+ estado.ModoCaptura !=
+ MapaModoCapturaCriacao.Automatico
+ )
+ {
+ motivo =
+ "A captura atual não está no modo automático.";
+ return false;
+ }
+
+ if (estado.GravacaoAutomaticaAtiva)
+ return true;
+
+ if (estado.RuaAtual == null)
+ {
+ motivo =
+ "Não existe rua atual para receber pontos.";
+ return false;
+ }
+
+ estado.GravacaoAutomaticaAtiva = true;
+ return true;
+ }
+
+
+ public bool PararGravacaoAutomatica(
+ MapaEstadoCriacao estado,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+ estado.GravacaoAutomaticaAtiva = false;
+ return true;
+ }
+
+
+ public bool TentarAdicionarPontoManual(
+ MapaEstadoCriacao estado,
+ MapaPontoCriacao ponto,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (
+ estado.ModoCaptura !=
+ MapaModoCapturaCriacao.Manual
+ )
+ {
+ motivo =
+ "A captura atual está no modo automático.";
+ return false;
+ }
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de registrar um ponto manual.";
+ return false;
+ }
+
+ return TentarAdicionarPonto(
+ estado,
+ ponto,
+ DistanciaMinimaPontoManualMetros,
+ out motivo
+ );
+ }
+
+
+ public bool TentarAdicionarPontoAutomatico(
+ MapaEstadoCriacao estado,
+ MapaPontoCriacao ponto,
+ bool forcarPrimeiroPonto,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (
+ estado.ModoCaptura !=
+ MapaModoCapturaCriacao.Automatico ||
+ !estado.GravacaoAutomaticaAtiva
+ )
+ {
+ motivo =
+ "A gravação automática não está ativa.";
+ return false;
+ }
+
+ MapaRuaCriacao? rua = estado.RuaAtual;
+
+ if (rua == null)
+ {
+ motivo =
+ "Não existe rua atual para receber pontos.";
+ return false;
+ }
+
+ if (
+ forcarPrimeiroPonto &&
+ rua.QuantidadePontos == 0
+ )
+ {
+ return TentarAdicionarPonto(
+ estado,
+ ponto,
+ distanciaMinimaMetros: 0,
+ out motivo
+ );
+ }
+
+ return TentarAdicionarPonto(
+ estado,
+ ponto,
+ estado.DistanciaMinimaCapturaAutomaticaMetros,
+ out motivo
+ );
+ }
+
+
+ public bool TentarRemoverUltimoPonto(
+ MapaEstadoCriacao estado,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de remover pontos.";
+ return false;
+ }
+
+ MapaRuaCriacao? rua = estado.RuaAtual;
+
+ if (
+ rua == null ||
+ rua.QuantidadePontos <= 0
+ )
+ {
+ motivo =
+ "A rua atual não possui pontos.";
+ return false;
+ }
+
+ rua.PontosMutaveis.RemoveAt(
+ rua.QuantidadePontos - 1
+ );
+
+ return true;
+ }
+
+
+ public bool TentarCriarNovaRua(
+ MapaEstadoCriacao estado,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de criar outra rua.";
+ return false;
+ }
+
+ MapaRuaCriacao? atual = estado.RuaAtual;
+
+ if (
+ atual == null ||
+ atual.QuantidadePontos < 2
+ )
+ {
+ int numero =
+ atual?.Numero
+ ?? (estado.IndiceRuaAtual + 1);
+
+ motivo =
+ $"Registre pelo menos 2 pontos na rua {numero} antes de criar uma nova rua.";
+ return false;
+ }
+
+ estado.RuasMutaveis.Add(
+ new MapaRuaCriacao
+ {
+ Numero =
+ estado.RuasMutaveis.Count + 1
+ }
+ );
+
+ estado.IndiceRuaAtual =
+ estado.RuasMutaveis.Count - 1;
+
+ return true;
+ }
+
+
+ public bool TentarSelecionarRua(
+ MapaEstadoCriacao estado,
+ int indiceRua,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de trocar de rua.";
+ return false;
+ }
+
+ if (
+ indiceRua < 0 ||
+ indiceRua >= estado.RuasMutaveis.Count
+ )
+ {
+ motivo =
+ "A rua informada não existe.";
+ return false;
+ }
+
+ estado.IndiceRuaAtual = indiceRua;
+ return true;
+ }
+
+
+ public bool TentarExcluirRua(
+ MapaEstadoCriacao estado,
+ int indiceRua,
+ out string motivo)
+ {
+ ValidarEstadoAtivo(estado);
+ motivo = string.Empty;
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ motivo =
+ "Finalize a gravação automática antes de excluir uma rua.";
+ return false;
+ }
+
+ if (
+ indiceRua < 0 ||
+ indiceRua >= estado.RuasMutaveis.Count
+ )
+ {
+ motivo =
+ "A rua informada não existe.";
+ return false;
+ }
+
+ /*
+ * A sessão mantém pelo menos uma rua.
+ * Se existe apenas uma, excluir significa limpar a Rua 1.
+ */
+ if (estado.RuasMutaveis.Count == 1)
+ {
+ estado.RuasMutaveis[0]
+ .PontosMutaveis
+ .Clear();
+
+ estado.IndiceRuaAtual = 0;
+ return true;
+ }
+
+ estado.RuasMutaveis.RemoveAt(indiceRua);
+ RenumerarRuas(estado);
+
+ if (
+ estado.IndiceRuaAtual >=
+ estado.RuasMutaveis.Count
+ )
+ {
+ estado.IndiceRuaAtual =
+ estado.RuasMutaveis.Count - 1;
+ }
+ else if (indiceRua < estado.IndiceRuaAtual)
+ {
+ estado.IndiceRuaAtual--;
+ }
+
+ return true;
+ }
+
+
+ public void LimparPontos(
+ MapaEstadoCriacao estado)
+ {
+ ValidarEstadoAtivo(estado);
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ throw new InvalidOperationException(
+ "Finalize a gravação automática antes de limpar o levantamento."
+ );
+ }
+
+ estado.RuasMutaveis.Clear();
+ estado.RuasMutaveis.Add(
+ new MapaRuaCriacao
+ {
+ Numero = 1
+ }
+ );
+ estado.IndiceRuaAtual = 0;
+ }
+
+
+ public MapaResumoCriacao CriarResumo(
+ MapaEstadoCriacao estado)
+ {
+ ValidarEstado(estado);
+
+ MapaRuaCriacao? atual = estado.RuaAtual;
+
+ double comprimentoAtual =
+ atual == null
+ ? 0
+ : CalcularComprimentoMetros(
+ atual.Pontos
+ );
+
+ double comprimentoTotal =
+ estado.Ruas.Sum(
+ rua =>
+ CalcularComprimentoMetros(
+ rua.Pontos
+ )
+ );
+
+
+ var ruasResumo =
+ estado.Ruas
+ .Select(
+ (rua, indice) =>
+ new MapaRuaResumoCriacao
+ {
+ Numero = rua.Numero,
+ QuantidadePontos = rua.QuantidadePontos,
+ ComprimentoMetros =
+ CalcularComprimentoMetros(rua.Pontos),
+ Valida = rua.Valida,
+ Atual = indice == estado.IndiceRuaAtual
+ }
+ )
+ .ToList();
+
+ return new MapaResumoCriacao
+ {
+ EmCriacao = estado.EmCriacao,
+ Nome = estado.Nome,
+ RuaAtual = atual?.Numero ?? 0,
+ QuantidadeRuas = estado.QuantidadeRuas,
+ QuantidadeRuasValidas = estado.QuantidadeRuasValidas,
+ PontosRuaAtual = atual?.QuantidadePontos ?? 0,
+ PontosTotal = estado.QuantidadePontosTotal,
+ ComprimentoRuaAtualMetros = comprimentoAtual,
+ ComprimentoTotalMetros = comprimentoTotal,
+ ModoCaptura = estado.ModoCaptura,
+ GravacaoAutomaticaAtiva = estado.GravacaoAutomaticaAtiva,
+ DistanciaMinimaAutomaticaMetros =
+ estado.DistanciaMinimaCapturaAutomaticaMetros,
+ PodeNovaRua = estado.PodeCriarNovaRua,
+ PodeSalvar =
+ estado.PodeSalvar &&
+ !string.IsNullOrWhiteSpace(estado.Nome),
+ Ruas = ruasResumo
+ };
+ }
+
+
+ ///
+ /// Converte o levantamento transitório para o domínio canônico.
+ /// IDs finais são sempre strings numéricas sequenciais.
+ ///
+ public MapaDocumento CriarDocumento(
+ MapaEstadoCriacao estado)
+ {
+ ValidarEstadoAtivo(estado);
+
+ if (estado.GravacaoAutomaticaAtiva)
+ {
+ throw new InvalidOperationException(
+ "Finalize a gravação automática antes de gerar o mapa."
+ );
+ }
+
+ string nome = NormalizarNome(estado.Nome);
+
+ if (string.IsNullOrWhiteSpace(nome))
+ {
+ throw new InvalidOperationException(
+ "Informe um nome para o novo mapa."
+ );
+ }
+
+ if (estado.Ruas.Count == 0)
+ {
+ throw new InvalidOperationException(
+ "O levantamento não possui ruas."
+ );
+ }
+
+ for (int i = 0; i < estado.Ruas.Count; i++)
+ {
+ MapaRuaCriacao rua = estado.Ruas[i];
+
+ if (
+ rua == null ||
+ rua.QuantidadePontos < 2
+ )
+ {
+ throw new InvalidOperationException(
+ $"A rua {i + 1} precisa possuir pelo menos 2 pontos."
+ );
+ }
+ }
+
+ var documento =
+ new MapaDocumento
+ {
+ Nome = nome,
+ CaminhoOrigem = null,
+ FormatoOrigem = MapaFormatoOrigem.Desconhecido,
+ Tipo = estado.Tipo,
+ Modificado = true
+ };
+
+ for (int i = 0; i < estado.Ruas.Count; i++)
+ {
+ MapaRuaCriacao ruaCriacao = estado.Ruas[i];
+
+ string id =
+ (i + 1).ToString(
+ CultureInfo.InvariantCulture
+ );
+
+ Coordinate[] coordenadas =
+ ruaCriacao.Pontos
+ .Select(
+ ponto =>
+ new Coordinate(
+ ponto.Longitude,
+ ponto.Latitude
+ )
+ )
+ .ToArray();
+
+ LineString geometria =
+ _geometryFactory.CreateLineString(
+ coordenadas
+ );
+
+ documento.Ruas.Add(
+ new MapaRua
+ {
+ Id = id,
+ IdOriginal = null,
+ IdGeradoAutomaticamente = true,
+ Nome = nome,
+ Geometria = geometria,
+ ComprimentoMetros =
+ CalcularComprimentoMetros(
+ ruaCriacao.Pontos
+ ),
+ Dist1 = 0,
+ Dist2 = 0,
+ IndiceFeatureOrigem = i
+ }
+ );
+ }
+
+ MapaResultadoValidacao validacao =
+ _validationService.Validar(
+ documento
+ );
+
+ if (!validacao.Valido)
+ {
+ string erros =
+ string.Join(
+ Environment.NewLine,
+ validacao.Erros
+ );
+
+ throw new InvalidOperationException(
+ "O mapa criado não passou pela validação canônica:" +
+ Environment.NewLine +
+ erros
+ );
+ }
+
+ return documento;
+ }
+
+
+ public static double CalcularComprimentoMetros(
+ IReadOnlyList pontos)
+ {
+ if (
+ pontos == null ||
+ pontos.Count < 2
+ )
+ {
+ return 0;
+ }
+
+ double total = 0;
+
+ for (int i = 1; i < pontos.Count; i++)
+ {
+ total += CalcularDistanciaMetros(
+ pontos[i - 1],
+ pontos[i]
+ );
+ }
+
+ return total;
+ }
+
+
+ public static double CalcularDistanciaMetros(
+ MapaPontoCriacao a,
+ MapaPontoCriacao b)
+ {
+ if (
+ a == null ||
+ b == null
+ )
+ {
+ return double.PositiveInfinity;
+ }
+
+ const double raioTerraMetros = 6378137.0;
+
+ double lat1 = GrausParaRadianos(a.Latitude);
+ double lat2 = GrausParaRadianos(b.Latitude);
+ double dLat = GrausParaRadianos(b.Latitude - a.Latitude);
+ double dLon = GrausParaRadianos(b.Longitude - a.Longitude);
+
+ double h =
+ Math.Sin(dLat / 2.0) * Math.Sin(dLat / 2.0) +
+ Math.Cos(lat1) *
+ Math.Cos(lat2) *
+ Math.Sin(dLon / 2.0) * Math.Sin(dLon / 2.0);
+
+ h = Math.Clamp(h, 0.0, 1.0);
+
+ return
+ 2.0 *
+ raioTerraMetros *
+ Math.Atan2(
+ Math.Sqrt(h),
+ Math.Sqrt(1.0 - h)
+ );
+ }
+
+
+ private static bool TentarAdicionarPonto(
+ MapaEstadoCriacao estado,
+ MapaPontoCriacao ponto,
+ double distanciaMinimaMetros,
+ out string motivo)
+ {
+ motivo = string.Empty;
+
+ if (ponto == null)
+ {
+ motivo =
+ "O ponto informado é nulo.";
+ return false;
+ }
+
+ if (
+ !CoordenadaValida(
+ ponto.Latitude,
+ ponto.Longitude
+ )
+ )
+ {
+ motivo =
+ "O ponto informado possui coordenada inválida.";
+ return false;
+ }
+
+ MapaRuaCriacao? rua = estado.RuaAtual;
+
+ if (rua == null)
+ {
+ motivo =
+ "Não existe rua atual para receber pontos.";
+ return false;
+ }
+
+ if (
+ rua.QuantidadePontos > 0 &&
+ distanciaMinimaMetros > 0
+ )
+ {
+ MapaPontoCriacao ultimo =
+ rua.Pontos[
+ rua.QuantidadePontos - 1
+ ];
+
+ double distancia =
+ CalcularDistanciaMetros(
+ ultimo,
+ ponto
+ );
+
+ if (
+ !double.IsFinite(distancia) ||
+ distancia < distanciaMinimaMetros
+ )
+ {
+ motivo =
+ "Ponto ignorado: distância do último ponto = " +
+ $"{distancia:F3} m; mínimo = " +
+ $"{distanciaMinimaMetros:F3} m.";
+ return false;
+ }
+ }
+
+ /*
+ * O estado guarda um snapshot próprio.
+ * Nenhuma referência externa fica viva aqui dentro.
+ */
+ rua.PontosMutaveis.Add(
+ ponto.Clonar()
+ );
+
+ return true;
+ }
+
+
+ private static bool CoordenadaValida(
+ double latitude,
+ double longitude)
+ {
+ return
+ double.IsFinite(latitude) &&
+ double.IsFinite(longitude) &&
+ latitude >= -90.0 &&
+ latitude <= 90.0 &&
+ longitude >= -180.0 &&
+ longitude <= 180.0 &&
+ (
+ Math.Abs(latitude) > 1e-12 ||
+ Math.Abs(longitude) > 1e-12
+ );
+ }
+
+
+ private static void RenumerarRuas(
+ MapaEstadoCriacao estado)
+ {
+ for (
+ int i = 0;
+ i < estado.RuasMutaveis.Count;
+ i++
+ )
+ {
+ estado.RuasMutaveis[i].Numero = i + 1;
+ }
+ }
+
+
+ private static string NormalizarNome(
+ string? nome)
+ {
+ return
+ string.IsNullOrWhiteSpace(nome)
+ ? string.Empty
+ : nome.Trim();
+ }
+
+
+ private static double GrausParaRadianos(
+ double graus)
+ {
+ return
+ graus *
+ Math.PI /
+ 180.0;
+ }
+
+
+ private static void ValidarEstado(
+ MapaEstadoCriacao estado)
+ {
+ if (estado == null)
+ {
+ throw new ArgumentNullException(
+ nameof(estado)
+ );
+ }
+ }
+
+
+ private static void ValidarEstadoAtivo(
+ MapaEstadoCriacao estado)
+ {
+ ValidarEstado(estado);
+
+ if (!estado.EmCriacao)
+ {
+ throw new InvalidOperationException(
+ "Não existe uma sessão de criação de mapa ativa."
+ );
+ }
+ }
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaEstadoCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaEstadoCriacao.cs
new file mode 100644
index 000000000..217c99906
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaEstadoCriacao.cs
@@ -0,0 +1,62 @@
+using static AgroBase.Models.Enums;
+
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Estado transitório da criação de mapa.
+ /// Não altera o Workspace até o operador explicitamente concluir.
+ ///
+ public sealed class MapaEstadoCriacao
+ {
+ private readonly List _ruas = new();
+
+ public bool EmCriacao { get; internal set; }
+ public string Nome { get; internal set; } = string.Empty;
+ public TipoMapaOperacao Tipo { get; internal set; } = TipoMapaOperacao.Indefinido;
+ public MapaModoCapturaCriacao ModoCaptura { get; internal set; } = MapaModoCapturaCriacao.Manual;
+ public bool GravacaoAutomaticaAtiva { get; internal set; }
+ public double DistanciaMinimaCapturaAutomaticaMetros { get; internal set; } = 0.50;
+ public int IndiceRuaAtual { get; internal set; }
+
+ public IReadOnlyList Ruas => _ruas;
+ internal List RuasMutaveis => _ruas;
+
+ public int QuantidadeRuas => _ruas.Count;
+ public int QuantidadeRuasValidas => _ruas.Count(x => x != null && x.Valida);
+ public int QuantidadePontosTotal => _ruas.Sum(x => x?.QuantidadePontos ?? 0);
+
+ public MapaRuaCriacao? RuaAtual
+ {
+ get
+ {
+ if (IndiceRuaAtual < 0 || IndiceRuaAtual >= _ruas.Count)
+ return null;
+
+ return _ruas[IndiceRuaAtual];
+ }
+ }
+
+ public bool PodeCriarNovaRua =>
+ EmCriacao &&
+ !GravacaoAutomaticaAtiva &&
+ RuaAtual?.Valida == true;
+
+ public bool PodeSalvar =>
+ EmCriacao &&
+ !GravacaoAutomaticaAtiva &&
+ _ruas.Count > 0 &&
+ _ruas.All(x => x != null && x.Valida);
+
+ internal void Limpar()
+ {
+ _ruas.Clear();
+ EmCriacao = false;
+ Nome = string.Empty;
+ Tipo = TipoMapaOperacao.Indefinido;
+ ModoCaptura = MapaModoCapturaCriacao.Manual;
+ GravacaoAutomaticaAtiva = false;
+ DistanciaMinimaCapturaAutomaticaMetros = 0.50;
+ IndiceRuaAtual = 0;
+ }
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaModoCapturaCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaModoCapturaCriacao.cs
new file mode 100644
index 000000000..dd856b2d6
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaModoCapturaCriacao.cs
@@ -0,0 +1,11 @@
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Forma como os pontos da rua atual são registrados.
+ ///
+ public enum MapaModoCapturaCriacao
+ {
+ Manual = 0,
+ Automatico = 1
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaPontoCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaPontoCriacao.cs
new file mode 100644
index 000000000..9ced6c43d
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaPontoCriacao.cs
@@ -0,0 +1,33 @@
+using static AgroBase.Models.Enums;
+
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Snapshot de um ponto coletado durante a criação de um mapa.
+ /// Latitude/Longitude são sempre WGS84.
+ ///
+ public sealed class MapaPontoCriacao
+ {
+ public double Latitude { get; set; }
+ public double Longitude { get; set; }
+ public double Altitude { get; set; }
+ public double PrecisaoCm { get; set; }
+ public int NumeroSatelites { get; set; }
+ public TiposCorrecaoGPS QualidadeFix { get; set; }
+ public DateTime Momento { get; set; }
+
+ public MapaPontoCriacao Clonar()
+ {
+ return new MapaPontoCriacao
+ {
+ Latitude = Latitude,
+ Longitude = Longitude,
+ Altitude = Altitude,
+ PrecisaoCm = PrecisaoCm,
+ NumeroSatelites = NumeroSatelites,
+ QualidadeFix = QualidadeFix,
+ Momento = Momento
+ };
+ }
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaResumoCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaResumoCriacao.cs
new file mode 100644
index 000000000..7a5f89977
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaResumoCriacao.cs
@@ -0,0 +1,53 @@
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// DTO pronto para alimentar o cockpit de criação.
+ ///
+ /// O bloco GNSS é preenchido pelo Controller, pois o service
+ /// de domínio não conhece GpsService/GPSModel.
+ ///
+ public sealed class MapaResumoCriacao
+ {
+ public bool EmCriacao { get; init; }
+
+ public string Nome { get; init; } =
+ string.Empty;
+
+ public int RuaAtual { get; init; }
+
+ public int QuantidadeRuas { get; init; }
+
+ public int QuantidadeRuasValidas { get; init; }
+
+ public int PontosRuaAtual { get; init; }
+
+ public int PontosTotal { get; init; }
+
+ public double ComprimentoRuaAtualMetros { get; init; }
+
+ public double ComprimentoTotalMetros { get; init; }
+
+ public MapaModoCapturaCriacao ModoCaptura { get; init; }
+
+ public bool GravacaoAutomaticaAtiva { get; init; }
+
+ public double DistanciaMinimaAutomaticaMetros { get; init; }
+
+ public bool PodeNovaRua { get; init; }
+
+ public bool PodeSalvar { get; init; }
+
+ public IReadOnlyList Ruas { get; init; } =
+ Array.Empty();
+
+ /*
+ * Estado do GNSS local para o cockpit.
+ */
+ public bool LeituraGnssValida { get; init; }
+
+ public bool QualidadeAlta { get; init; }
+
+ public string StatusGnss { get; init; } =
+ "GNSS sem leitura.";
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaRuaCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaRuaCriacao.cs
new file mode 100644
index 000000000..5fda62cb3
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaRuaCriacao.cs
@@ -0,0 +1,18 @@
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Uma rua ainda em levantamento.
+ /// O ID canônico definitivo é gerado na conversão para MapaDocumento.
+ ///
+ public sealed class MapaRuaCriacao
+ {
+ private readonly List _pontos = new();
+
+ public int Numero { get; internal set; }
+ public IReadOnlyList Pontos => _pontos;
+ public int QuantidadePontos => _pontos.Count;
+ public bool Valida => _pontos.Count >= 2;
+
+ internal List PontosMutaveis => _pontos;
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/MapaRuaResumoCriacao.cs b/AgroBase/OperationControl/Mapas/Creation/MapaRuaResumoCriacao.cs
new file mode 100644
index 000000000..41f70ecdc
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/MapaRuaResumoCriacao.cs
@@ -0,0 +1,25 @@
+namespace OperationControl.Mapas.Creation
+{
+ ///
+ /// Linha resumida exibida no seletor de ruas do cockpit.
+ ///
+ public sealed class MapaRuaResumoCriacao
+ {
+ public int Numero { get; init; }
+
+ public int QuantidadePontos { get; init; }
+
+ public double ComprimentoMetros { get; init; }
+
+ public bool Valida { get; init; }
+
+ public bool Atual { get; init; }
+
+
+ public string Descricao =>
+ $"Rua {Numero} • " +
+ $"{QuantidadePontos} pts • " +
+ $"{ComprimentoMetros:F1} m" +
+ (Atual ? " • atual" : string.Empty);
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml b/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml
new file mode 100644
index 000000000..655539738
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml.cs b/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml.cs
new file mode 100644
index 000000000..6c2c32dee
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Creation/Views/MapaCriacaoOverlay.xaml.cs
@@ -0,0 +1,178 @@
+using OperationControl.Mapas.Creation;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace OperationControl.Mapas.Creation.Views
+{
+ ///
+ /// Cockpit passivo da criação de mapas.
+ /// Exibe estado e emite intenções. Não altera mapa ou GNSS.
+ ///
+ public partial class MapaCriacaoOverlay : System.Windows.Controls.UserControl
+ {
+ public event Action? NomeMapaAlterado;
+ public event Action? ModoCapturaAlterado;
+ public event Action? RegistrarPontoSolicitado;
+ public event Action? RemoverUltimoPontoSolicitado;
+ public event Action? IniciarCapturaAutomaticaSolicitado;
+ public event Action? FinalizarCapturaAutomaticaSolicitado;
+ public event Action? NovaRuaSolicitada;
+ public event Action? RuaSelecionada;
+ public event Action? ExcluirRuaSolicitada;
+ public event Action? LimparSolicitado;
+ public event Action? SalvarSolicitado;
+ public event Action? CancelarSolicitado;
+
+ private bool _atualizandoEstado;
+ private MapaResumoCriacao? _ultimoResumo;
+
+ public MapaCriacaoOverlay()
+ {
+ InitializeComponent();
+ }
+
+ public void AtualizarEstado(MapaResumoCriacao? resumo)
+ {
+ _atualizandoEstado = true;
+
+ try
+ {
+ _ultimoResumo = resumo;
+
+ bool ativo = resumo?.EmCriacao == true;
+ Visibility = ativo ? Visibility.Visible : Visibility.Collapsed;
+ IsHitTestVisible = ativo;
+
+ if (!ativo)
+ return;
+
+ string nome = resumo!.Nome ?? string.Empty;
+
+ if (!string.Equals(txtNomeMapa.Text, nome, StringComparison.Ordinal))
+ {
+ int caret = txtNomeMapa.CaretIndex;
+ txtNomeMapa.Text = nome;
+ txtNomeMapa.CaretIndex = Math.Min(caret, txtNomeMapa.Text.Length);
+ }
+
+ txtRuaAtual.Text = resumo.RuaAtual > 0
+ ? $"Rua {resumo.RuaAtual}"
+ : "Sem rua";
+
+ txtResumoRua.Text =
+ $"{resumo.PontosRuaAtual} pts • {resumo.ComprimentoRuaAtualMetros:F1} m";
+
+ txtStatusGnss.Text = string.IsNullOrWhiteSpace(resumo.StatusGnss)
+ ? "GNSS sem leitura."
+ : resumo.StatusGnss;
+
+ rbManual.IsChecked = resumo.ModoCaptura == MapaModoCapturaCriacao.Manual;
+ rbAutomatico.IsChecked = resumo.ModoCaptura == MapaModoCapturaCriacao.Automatico;
+
+ cmbRuas.ItemsSource = resumo.Ruas;
+ cmbRuas.SelectedValue = resumo.RuaAtual;
+
+ bool gravando = resumo.GravacaoAutomaticaAtiva;
+
+ btnRegistrarPonto.Visibility =
+ resumo.ModoCaptura == MapaModoCapturaCriacao.Manual
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+
+ btnCapturaAutomatica.Visibility =
+ resumo.ModoCaptura == MapaModoCapturaCriacao.Automatico
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+
+ btnRegistrarPonto.IsEnabled = !gravando && resumo.LeituraGnssValida;
+
+ btnCapturaAutomatica.Content = gravando
+ ? "■ Finalizar captura automática"
+ : "▶ Iniciar captura automática";
+
+ btnCapturaAutomatica.IsEnabled = gravando || resumo.LeituraGnssValida;
+ btnRemoverUltimoPonto.IsEnabled = !gravando && resumo.PontosRuaAtual > 0;
+ btnNovaRua.IsEnabled = resumo.PodeNovaRua;
+ btnExcluirRua.IsEnabled = !gravando && resumo.QuantidadeRuas > 0;
+ btnLimpar.IsEnabled = !gravando && resumo.PontosTotal > 0;
+ cmbRuas.IsEnabled = !gravando && resumo.QuantidadeRuas > 0;
+ rbManual.IsEnabled = !gravando;
+ rbAutomatico.IsEnabled = !gravando;
+ txtNomeMapa.IsEnabled = !gravando;
+ btnSalvar.IsEnabled = resumo.PodeSalvar;
+
+ txtResumoTotal.Text =
+ $"{resumo.QuantidadeRuas} {(resumo.QuantidadeRuas == 1 ? "rua" : "ruas")} • " +
+ $"{resumo.PontosTotal} {(resumo.PontosTotal == 1 ? "ponto" : "pontos")} • " +
+ $"{resumo.ComprimentoTotalMetros:F1} m" +
+ Environment.NewLine +
+ $"{resumo.QuantidadeRuasValidas}/{resumo.QuantidadeRuas} ruas válidas";
+ }
+ finally
+ {
+ _atualizandoEstado = false;
+ }
+ }
+
+ private void txtNomeMapa_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (_atualizandoEstado)
+ return;
+
+ NomeMapaAlterado?.Invoke(txtNomeMapa.Text);
+ }
+
+ private void rbModo_Checked(object sender, RoutedEventArgs e)
+ {
+ if (_atualizandoEstado)
+ return;
+
+ if (rbAutomatico.IsChecked == true)
+ ModoCapturaAlterado?.Invoke(MapaModoCapturaCriacao.Automatico);
+ else if (rbManual.IsChecked == true)
+ ModoCapturaAlterado?.Invoke(MapaModoCapturaCriacao.Manual);
+ }
+
+ private void btnRegistrarPonto_Click(object sender, RoutedEventArgs e) =>
+ RegistrarPontoSolicitado?.Invoke();
+
+ private void btnRemoverUltimoPonto_Click(object sender, RoutedEventArgs e) =>
+ RemoverUltimoPontoSolicitado?.Invoke();
+
+ private void btnCapturaAutomatica_Click(object sender, RoutedEventArgs e)
+ {
+ if (_ultimoResumo?.GravacaoAutomaticaAtiva == true)
+ FinalizarCapturaAutomaticaSolicitado?.Invoke();
+ else
+ IniciarCapturaAutomaticaSolicitado?.Invoke();
+ }
+
+ private void btnNovaRua_Click(object sender, RoutedEventArgs e) =>
+ NovaRuaSolicitada?.Invoke();
+
+ private void cmbRuas_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_atualizandoEstado)
+ return;
+
+ if (cmbRuas.SelectedValue is int numeroRua)
+ RuaSelecionada?.Invoke(numeroRua);
+ }
+
+ private void btnExcluirRua_Click(object sender, RoutedEventArgs e)
+ {
+ int numeroRua = _ultimoResumo?.RuaAtual ?? 0;
+ if (numeroRua > 0)
+ ExcluirRuaSolicitada?.Invoke(numeroRua);
+ }
+
+ private void btnLimpar_Click(object sender, RoutedEventArgs e) =>
+ LimparSolicitado?.Invoke();
+
+ private void btnSalvar_Click(object sender, RoutedEventArgs e) =>
+ SalvarSolicitado?.Invoke();
+
+ private void btnCancelar_Click(object sender, RoutedEventArgs e) =>
+ CancelarSolicitado?.Invoke();
+ }
+}
diff --git a/AgroBase/OperationControl/Mapas/Rendering/MapaCriacaoRenderService.cs b/AgroBase/OperationControl/Mapas/Rendering/MapaCriacaoRenderService.cs
new file mode 100644
index 000000000..a9a103a75
--- /dev/null
+++ b/AgroBase/OperationControl/Mapas/Rendering/MapaCriacaoRenderService.cs
@@ -0,0 +1,412 @@
+using Mapsui;
+using Mapsui.Layers;
+using Mapsui.Nts;
+using Mapsui.Projections;
+using Mapsui.Styles;
+using NetTopologySuite.Geometries;
+using OperationControl.Mapas.Creation;
+using Brush = Mapsui.Styles.Brush;
+using Color = Mapsui.Styles.Color;
+using Font = Mapsui.Styles.Font;
+using Pen = Mapsui.Styles.Pen;
+
+namespace OperationControl.Mapas.Rendering
+{
+ ///
+ /// Preview georreferenciado do mapa ainda em levantamento.
+ ///
+ /// Regras visuais:
+ /// - ruas concluídas: verde;
+ /// - rua atual: ciano e mais espessa;
+ /// - pontos da rua atual: marcadores numerados;
+ /// - primeira posição de cada rua: etiqueta R1, R2...
+ ///
+ /// Nada aqui altera o MapaWorkspace.
+ ///
+ public sealed class MapaCriacaoRenderService
+ {
+ public const string NomeCamada =
+ "MapaCriacaoPreview";
+
+
+ public MemoryLayer CriarCamada(
+ MapaEstadoCriacao estado)
+ {
+ if (estado == null)
+ {
+ throw new ArgumentNullException(
+ nameof(estado)
+ );
+ }
+
+
+ return new MemoryLayer
+ {
+ Name =
+ NomeCamada,
+
+ Features =
+ CriarFeatures(
+ estado
+ )
+ };
+ }
+
+
+ public void AtualizarCamada(
+ MemoryLayer camada,
+ MapaEstadoCriacao estado)
+ {
+ if (camada == null)
+ {
+ throw new ArgumentNullException(
+ nameof(camada)
+ );
+ }
+
+
+ if (estado == null)
+ {
+ throw new ArgumentNullException(
+ nameof(estado)
+ );
+ }
+
+
+ camada.Features =
+ CriarFeatures(
+ estado
+ );
+ }
+
+
+ private static IEnumerable
+ CriarFeatures(
+ MapaEstadoCriacao estado)
+ {
+ var features =
+ new List();
+
+
+ if (!estado.EmCriacao)
+ {
+ return features;
+ }
+
+
+ for (
+ int indiceRua = 0;
+ indiceRua < estado.Ruas.Count;
+ indiceRua++
+ )
+ {
+ MapaRuaCriacao rua =
+ estado.Ruas[indiceRua];
+
+
+ if (
+ rua == null ||
+ rua.Pontos.Count == 0
+ )
+ {
+ continue;
+ }
+
+
+ bool atual =
+ indiceRua ==
+ estado.IndiceRuaAtual;
+
+
+ Color corLinha =
+ atual
+ ? new Color(
+ 0,
+ 210,
+ 255,
+ 255
+ )
+ : new Color(
+ 95,
+ 207,
+ 128,
+ 255
+ );
+
+
+ /*
+ * LINHA DA RUA
+ */
+ if (
+ rua.Pontos.Count >= 2
+ )
+ {
+ Coordinate[] coordenadas =
+ rua
+ .Pontos
+ .Select(
+ ponto =>
+ {
+ var mercator =
+ SphericalMercator
+ .FromLonLat(
+ ponto.Longitude,
+ ponto.Latitude
+ );
+
+
+ return new Coordinate(
+ mercator.x,
+ mercator.y
+ );
+ }
+ )
+ .ToArray();
+
+
+ var featureLinha =
+ new GeometryFeature
+ {
+ Geometry =
+ new LineString(
+ coordenadas
+ )
+ };
+
+
+ featureLinha["Tipo"] =
+ "MapaCriacaoRua";
+
+ featureLinha["Rua"] =
+ rua.Numero;
+
+
+ featureLinha.Styles.Add(
+ new VectorStyle
+ {
+ Line =
+ new Pen(
+ corLinha,
+ atual ? 5 : 3
+ )
+ }
+ );
+
+
+ features.Add(
+ featureLinha
+ );
+ }
+
+
+ /*
+ * MARCADOR DO INÍCIO DA RUA
+ */
+ MapaPontoCriacao primeiro =
+ rua.Pontos[0];
+
+
+ var inicioMercator =
+ SphericalMercator
+ .FromLonLat(
+ primeiro.Longitude,
+ primeiro.Latitude
+ );
+
+
+ var marcadorRua =
+ new PointFeature(
+ inicioMercator.x,
+ inicioMercator.y
+ );
+
+
+ marcadorRua["Tipo"] =
+ "MapaCriacaoInicioRua";
+
+ marcadorRua["label"] =
+ $"R{rua.Numero}";
+
+
+ marcadorRua.Styles.Add(
+ new SymbolStyle
+ {
+ SymbolType =
+ SymbolType.Ellipse,
+
+ SymbolScale =
+ atual
+ ? 1.25
+ : 1.0,
+
+ Fill =
+ new Brush(
+ corLinha
+ ),
+
+ Outline =
+ new Pen(
+ Color.White,
+ 2
+ )
+ }
+ );
+
+
+ marcadorRua.Styles.Add(
+ new LabelStyle
+ {
+ LabelColumn =
+ "label",
+
+ ForeColor =
+ Color.White,
+
+ BackColor =
+ null,
+
+ HorizontalAlignment =
+ LabelStyle
+ .HorizontalAlignmentEnum
+ .Center,
+
+ VerticalAlignment =
+ LabelStyle
+ .VerticalAlignmentEnum
+ .Center,
+
+ Font =
+ new Font
+ {
+ Size = 10
+ }
+ }
+ );
+
+
+ features.Add(
+ marcadorRua
+ );
+
+
+ /*
+ * PONTOS DA RUA ATUAL
+ *
+ * Nas ruas concluídas evitamos centenas de marcadores
+ * para manter o mapa limpo e leve.
+ */
+ if (!atual)
+ {
+ continue;
+ }
+
+
+ for (
+ int indicePonto = 0;
+ indicePonto < rua.Pontos.Count;
+ indicePonto++
+ )
+ {
+ MapaPontoCriacao ponto =
+ rua.Pontos[indicePonto];
+
+
+ var mercator =
+ SphericalMercator
+ .FromLonLat(
+ ponto.Longitude,
+ ponto.Latitude
+ );
+
+
+ var featurePonto =
+ new PointFeature(
+ mercator.x,
+ mercator.y
+ );
+
+
+ featurePonto["Tipo"] =
+ "MapaCriacaoPonto";
+
+ featurePonto["Rua"] =
+ rua.Numero;
+
+ featurePonto["Ordem"] =
+ indicePonto + 1;
+
+ featurePonto["label"] =
+ (indicePonto + 1)
+ .ToString();
+
+
+ featurePonto.Styles.Add(
+ new SymbolStyle
+ {
+ SymbolType =
+ SymbolType.Ellipse,
+
+ SymbolScale =
+ 0.95,
+
+ Fill =
+ new Brush(
+ new Color(
+ 0,
+ 210,
+ 255,
+ 255
+ )
+ ),
+
+ Outline =
+ new Pen(
+ Color.White,
+ 2
+ )
+ }
+ );
+
+
+ featurePonto.Styles.Add(
+ new LabelStyle
+ {
+ LabelColumn =
+ "label",
+
+ ForeColor =
+ Color.White,
+
+ BackColor =
+ null,
+
+ HorizontalAlignment =
+ LabelStyle
+ .HorizontalAlignmentEnum
+ .Center,
+
+ VerticalAlignment =
+ LabelStyle
+ .VerticalAlignmentEnum
+ .Center,
+
+ Font =
+ new Font
+ {
+ Size = 9
+ }
+ }
+ );
+
+
+ features.Add(
+ featurePonto
+ );
+ }
+ }
+
+
+ return features;
+ }
+ }
+}
diff --git a/AgroBase/OperationControl/ViewModels/Views/PreparacaoMapa/PreparacaoMapaBottomViewModel.cs b/AgroBase/OperationControl/ViewModels/Views/PreparacaoMapa/PreparacaoMapaBottomViewModel.cs
index 61acc89e5..5ff9bc301 100644
--- a/AgroBase/OperationControl/ViewModels/Views/PreparacaoMapa/PreparacaoMapaBottomViewModel.cs
+++ b/AgroBase/OperationControl/ViewModels/Views/PreparacaoMapa/PreparacaoMapaBottomViewModel.cs
@@ -22,6 +22,7 @@ namespace OperationControl.ViewModels
public event PropertyChangedEventHandler? PropertyChanged;
public event Action? CarregarMapaSolicitado;
+ public event Action? CriarMapaSolicitado;
public event Action? EditarMapaSolicitado;
// Método, latitude, longitude, altitude, offset frontal cm, offset lateral cm
@@ -63,6 +64,7 @@ namespace OperationControl.ViewModels
};
public ICommand CarregarMapaCommand { get; }
+ public ICommand CriarMapaCommand { get; }
public ICommand EditarMapaCommand { get; }
public ICommand FixarBaseCommand { get; }
public ICommand AplicarOffsetCommand { get; }
@@ -73,6 +75,7 @@ namespace OperationControl.ViewModels
public PreparacaoMapaBottomViewModel()
{
CarregarMapaCommand = new RelayCommand(_ => ExecutarCarregarMapa(), _ => PodeCarregarMapa);
+ CriarMapaCommand = new RelayCommand(_ => CriarMapaSolicitado?.Invoke(), _ => PodeCriarMapa);
EditarMapaCommand = new RelayCommand(_ => EditarMapaSolicitado?.Invoke(), _ => PodeEditarMapa);
FixarBaseCommand = new RelayCommand(_ => ExecutarFixarBase(), _ => PodeFixarBase);
AplicarOffsetCommand = new RelayCommand(_ => ExecutarAplicarOffset(), _ => PodeEditarOffsetBase);
@@ -124,6 +127,7 @@ namespace OperationControl.ViewModels
private bool _mapaCarregado;
private bool _edicaoMapaAtiva;
+ private bool _criacaoMapaAtiva;
///
/// Enquanto o editor estiver aberto nesta etapa:
@@ -137,20 +141,32 @@ namespace OperationControl.ViewModels
public bool EdicaoMapaAtiva =>
_edicaoMapaAtiva;
+ public bool CriacaoMapaAtiva =>
+ _criacaoMapaAtiva;
+
+ public bool PodeCriarMapa =>
+ !_edicaoMapaAtiva &&
+ !_criacaoMapaAtiva &&
+ !FixacaoEmAndamento &&
+ !AlteracaoPapelGnssEmAndamento;
+
public bool PodeEditarMapa =>
_mapaCarregado &&
!_edicaoMapaAtiva &&
+ !_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
public bool PodeContinuar =>
_mapaCarregado &&
!_edicaoMapaAtiva &&
+ !_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
public bool PodeVoltar =>
!_edicaoMapaAtiva &&
+ !_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
@@ -201,6 +217,10 @@ namespace OperationControl.ViewModels
nameof(PodeEditarMapa)
);
+ OnPropertyChanged(
+ nameof(PodeCriarMapa)
+ );
+
OnPropertyChanged(
nameof(PodeCarregarMapa)
);
@@ -214,6 +234,22 @@ namespace OperationControl.ViewModels
);
}
+ public void DefinirCriacaoMapaAtiva(bool ativa)
+ {
+ if (_criacaoMapaAtiva == ativa)
+ return;
+
+ _criacaoMapaAtiva = ativa;
+
+ OnPropertyChanged(nameof(CriacaoMapaAtiva));
+ OnPropertyChanged(nameof(PodeCriarMapa));
+ OnPropertyChanged(nameof(PodeEditarMapa));
+ OnPropertyChanged(nameof(PodeCarregarMapa));
+ OnPropertyChanged(nameof(PodeContinuar));
+ OnPropertyChanged(nameof(PodeVoltar));
+ }
+
+
private GnssPapelEstacao _papelGnssAtual = GnssPapelEstacao.Indefinido;
public GnssPapelEstacao PapelGnssAtual
{
@@ -316,6 +352,10 @@ namespace OperationControl.ViewModels
nameof(PodeEditarMapa)
);
+ OnPropertyChanged(
+ nameof(PodeCriarMapa)
+ );
+
OnPropertyChanged(
nameof(PodeEditarMetodoFixacao)
);
@@ -451,6 +491,7 @@ namespace OperationControl.ViewModels
OnPropertyChanged(nameof(FixacaoEmAndamento));
OnPropertyChanged(nameof(PodeCarregarMapa));
OnPropertyChanged(nameof(PodeEditarMapa));
+ OnPropertyChanged(nameof(PodeCriarMapa));
OnPropertyChanged(nameof(PodeEditarMetodoFixacao));
OnPropertyChanged(nameof(PodeEditarCoordenadasManuais));
OnPropertyChanged(nameof(PodeEditarOffsetBase));
@@ -480,6 +521,7 @@ namespace OperationControl.ViewModels
public bool PodeCarregarMapa =>
!_edicaoMapaAtiva &&
+ !_criacaoMapaAtiva &&
!FixacaoEmAndamento &&
!AlteracaoPapelGnssEmAndamento;
diff --git a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
index f1c5d79f2..34190958d 100644
--- a/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
+++ b/AgroBase/OperationControl/ViewModels/Windows/DockWindowViewModel.cs
@@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq;
using OperationControl.Controls;
using OperationControl.Helpers;
using OperationControl.Mapas;
+using OperationControl.Mapas.Creation;
using OperationControl.Mapas.Editing;
using OperationControl.Mapas.Interaction;
using OperationControl.Mapas.Models;
@@ -58,6 +59,7 @@ namespace OperationControl.ViewModels
_viewPreparacaoMapaBottom = new PreparacaoMapaBottomView();
_viewPreparacaoMapaBottom._vm.CarregarMapaSolicitado += OnCarregarMapaSolicitado;
+ _viewPreparacaoMapaBottom._vm.CriarMapaSolicitado += OnCriarMapaSolicitado;
_viewPreparacaoMapaBottom._vm.EditarMapaSolicitado += OnEditarMapaPreparacaoSolicitado;
_viewPreparacaoMapaBottom._vm.FixarBaseSolicitado += OnFixarBaseSolicitado;
_viewPreparacaoMapaBottom._vm.AplicarOffsetSolicitado += OnAplicarOffsetSolicitado;
@@ -97,6 +99,47 @@ namespace OperationControl.ViewModels
.SalvarMapaEditadoSolicitado +=
OnSalvarMapaEditadoSolicitado;
+ var criacaoOverlay =
+ _viewPreparacaoMapa
+ .MapaPreparacao
+ .CriacaoOverlay;
+
+ criacaoOverlay.NomeMapaAlterado +=
+ nome => _mapas.DefinirNomeCriacaoMapa(nome);
+
+ criacaoOverlay.ModoCapturaAlterado +=
+ modo => _mapas.DefinirModoCapturaCriacao(modo);
+
+ criacaoOverlay.RegistrarPontoSolicitado +=
+ () => _mapas.RegistrarPontoCriacaoManual();
+
+ criacaoOverlay.RemoverUltimoPontoSolicitado +=
+ () => _mapas.RemoverUltimoPontoCriacao();
+
+ criacaoOverlay.IniciarCapturaAutomaticaSolicitado +=
+ () => _mapas.IniciarGravacaoAutomaticaCriacao();
+
+ criacaoOverlay.FinalizarCapturaAutomaticaSolicitado +=
+ () => _mapas.FinalizarGravacaoAutomaticaCriacao();
+
+ criacaoOverlay.NovaRuaSolicitada +=
+ () => _mapas.CriarNovaRuaMapa();
+
+ criacaoOverlay.RuaSelecionada +=
+ numeroRua => _mapas.SelecionarRuaCriacao(numeroRua);
+
+ criacaoOverlay.ExcluirRuaSolicitada +=
+ OnExcluirRuaCriacaoSolicitado;
+
+ criacaoOverlay.LimparSolicitado +=
+ OnLimparCriacaoSolicitado;
+
+ criacaoOverlay.SalvarSolicitado +=
+ OnSalvarMapaCriadoSolicitado;
+
+ criacaoOverlay.CancelarSolicitado +=
+ OnCancelarCriacaoMapaSolicitado;
+
_viewOperacaoLeft = new OperacaoLeftView();
_viewOperacaoRight = new OperacaoRightView();
@@ -118,6 +161,7 @@ namespace OperationControl.ViewModels
_mapas.ValidacaoEdicaoFalhou += OnMapaValidacaoEdicaoFalhou;
_mapas.EstadoMapaAlterado += OnEstadoMapaAlterado;
_mapas.EstadoEdicaoAlterado += OnEstadoMapaAlterado;
+ _mapas.EstadoCriacaoAlterado += OnEstadoCriacaoAlterado;
// Commands
BackCommand = new RelayCommand(_ => GoBack(), _ => CanGoBack);
@@ -240,7 +284,10 @@ namespace OperationControl.ViewModels
CurrentStep != DockStep.ConfigConexao &&
!(
CurrentStep == DockStep.PreparacaoMapa &&
- _mapas.EdicaoAtiva
+ (
+ _mapas.EdicaoAtiva ||
+ _mapas.CriacaoMapaAtiva
+ )
);
public bool CanGoNext
@@ -254,7 +301,8 @@ namespace OperationControl.ViewModels
DockStep.PreparacaoMapa =>
MapaCarregado &&
- !_mapas.EdicaoAtiva,
+ !_mapas.EdicaoAtiva &&
+ !_mapas.CriacaoMapaAtiva,
_ =>
false
@@ -1266,6 +1314,21 @@ namespace OperationControl.ViewModels
}
}
+ /*
+ * Snapshot GNSS compartilhado pelas ferramentas de mapa
+ * que fazem levantamento em campo:
+ *
+ * - borda GNSS do editor;
+ * - criação de mapa por ruas.
+ *
+ * O Controller faz Clone() internamente e aplica a mesma
+ * validação de freshness já usada pela borda.
+ */
+ _mapas.AtualizarGnssLocal(
+ dados
+ );
+
+
_mapas.AtualizarBase(
dados,
mapaAtual
@@ -1675,6 +1738,8 @@ namespace OperationControl.ViewModels
private void OnEstadoMapaAlterado()
{
+ MapaCarregado = _mapas.PossuiMapa;
+
_viewPreparacaoMapaBottom?
._vm?
.DefinirEdicaoMapaAtiva(
@@ -1695,6 +1760,27 @@ namespace OperationControl.ViewModels
AtualizarEstadoMapaParametrizacao();
}
+ private void OnEstadoCriacaoAlterado()
+ {
+ bool ativa = _mapas.CriacaoMapaAtiva;
+
+ _viewPreparacaoMapaBottom?
+ ._vm?
+ .DefinirCriacaoMapaAtiva(ativa);
+
+ _viewPreparacaoMapa?
+ .MapaPreparacao?
+ .CriacaoOverlay?
+ .AtualizarEstado(
+ _mapas.ObterResumoCriacaoMapa()
+ );
+
+ OnPropertyChanged(nameof(CanGoBack));
+ OnPropertyChanged(nameof(CanGoNext));
+ CommandManager.InvalidateRequerySuggested();
+ }
+
+
private void AtualizarEstadoMapaParametrizacao()
{
void Atualizar()
@@ -1858,6 +1944,206 @@ namespace OperationControl.ViewModels
);
}
+ private void OnCriarMapaSolicitado()
+ {
+ try
+ {
+ if (CurrentStep != DockStep.PreparacaoMapa)
+ return;
+
+ if (_mapas.EdicaoAtiva || _mapas.CriacaoMapaAtiva)
+ return;
+
+ var dlgTipo = new OperationControl.Windows.SelecaoTipoMapa
+ {
+ Owner = Models.Variaveis.Dock
+ };
+
+ if (dlgTipo.ShowDialog() != true || dlgTipo.Resultado == null)
+ return;
+
+ MapViewControl? mapa = _viewPreparacaoMapa?.MapaPreparacao;
+ if (mapa == null)
+ return;
+
+ _mapas.IniciarCriacaoMapa(
+ mapa,
+ nome: "NovoMapa",
+ tipo: dlgTipo.Resultado.Value,
+ distanciaMinimaAutomaticaMetros: 0.50
+ );
+
+ _viewPreparacaoMapaBottom?._vm?.DefinirCriacaoMapaAtiva(true);
+
+ if (_viewPreparacaoMapaBottom?._vm != null)
+ {
+ _viewPreparacaoMapaBottom._vm.StatusTexto =
+ "Criação de mapa ativa. Registre os pontos das ruas pelo GNSS.";
+ }
+
+ Models.Variaveis.MostrarLog(
+ "[MAPA][CRIACAO][UI] Cockpit aberto na preparação."
+ );
+ }
+ catch (Exception ex)
+ {
+ Models.Variaveis.MostrarLog(
+ "[MAPA][CRIACAO][UI][ERRO] " + ex
+ );
+
+ MessageBox.Show(
+ "Não foi possível iniciar a criação do mapa." +
+ Environment.NewLine + Environment.NewLine + ex.Message,
+ "Criar mapa",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ }
+
+
+ private void OnExcluirRuaCriacaoSolicitado(int numeroRua)
+ {
+ var resposta = MessageBox.Show(
+ $"Excluir a Rua {numeroRua} do levantamento?",
+ "Excluir rua",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question
+ );
+
+ if (resposta == MessageBoxResult.Yes)
+ _mapas.ExcluirRuaCriacao(numeroRua);
+ }
+
+
+ private void OnLimparCriacaoSolicitado()
+ {
+ var resposta = MessageBox.Show(
+ "Limpar todos os pontos e ruas levantados?",
+ "Limpar levantamento",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question
+ );
+
+ if (resposta == MessageBoxResult.Yes)
+ _mapas.LimparLevantamentoCriacao();
+ }
+
+
+ private void OnCancelarCriacaoMapaSolicitado()
+ {
+ if (!_mapas.CriacaoMapaAtiva)
+ return;
+
+ MapaResumoCriacao resumo = _mapas.ObterResumoCriacaoMapa();
+
+ if (resumo.PontosTotal > 0)
+ {
+ var resposta = MessageBox.Show(
+ "Cancelar a criação do mapa?" +
+ Environment.NewLine + Environment.NewLine +
+ "Os pontos ainda não salvos serão descartados.",
+ "Cancelar criação",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question
+ );
+
+ if (resposta != MessageBoxResult.Yes)
+ return;
+ }
+
+ _mapas.CancelarCriacaoMapa();
+
+ if (_viewPreparacaoMapaBottom?._vm != null)
+ {
+ _viewPreparacaoMapaBottom._vm.StatusTexto =
+ _mapas.PossuiMapa
+ ? "Criação cancelada. O mapa anterior foi restaurado."
+ : "Criação cancelada.";
+ }
+ }
+
+
+ private void OnSalvarMapaCriadoSolicitado()
+ {
+ try
+ {
+ if (!_mapas.CriacaoMapaAtiva)
+ return;
+
+ MapaResumoCriacao resumo = _mapas.ObterResumoCriacaoMapa();
+
+ if (!resumo.PodeSalvar)
+ {
+ MessageBox.Show(
+ "Para salvar o mapa, informe um nome e garanta que " +
+ "todas as ruas possuam pelo menos 2 pontos.",
+ "Mapa incompleto",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning
+ );
+ return;
+ }
+
+ var dialog = new Microsoft.Win32.SaveFileDialog
+ {
+ Title = "Salvar novo mapa operacional",
+ Filter = "GeoJSON (*.geojson)|*.geojson",
+ DefaultExt = ".geojson",
+ AddExtension = true,
+ OverwritePrompt = true,
+ FileName = _mapas.ObterNomeArquivoSugeridoMapaCriado()
+ };
+
+ if (dialog.ShowDialog() != true)
+ return;
+
+ MapaDocumento documento =
+ _mapas.SalvarECarregarMapaCriado(dialog.FileName);
+
+ MapaCarregado = true;
+
+ _mapas.PrepararMapa(
+ _viewPreparacaoMapa?.MapaPreparacao,
+ modoInteracao: MapaModoInteracao.Visualizacao,
+ enquadrarDocumento: true
+ );
+
+ if (_viewPreparacaoMapaBottom?._vm != null)
+ {
+ _viewPreparacaoMapaBottom._vm.StatusTexto =
+ $"Mapa criado e carregado: {System.IO.Path.GetFileName(dialog.FileName)}";
+ }
+
+ MessageBox.Show(
+ "Mapa criado com sucesso." + Environment.NewLine + Environment.NewLine +
+ $"Ruas: {documento.QuantidadeRuas}" + Environment.NewLine +
+ $"Comprimento total: {documento.ComprimentoTotalMetros:F1} m" +
+ Environment.NewLine + Environment.NewLine +
+ dialog.FileName + Environment.NewLine + Environment.NewLine +
+ "O arquivo já está carregado como mapa oficial.",
+ "Mapa criado",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ }
+ catch (Exception ex)
+ {
+ Models.Variaveis.MostrarLog(
+ "[MAPA][CRIACAO][SALVAR][ERRO] " + ex
+ );
+
+ MessageBox.Show(
+ "Não foi possível salvar/carregar o novo mapa." +
+ Environment.NewLine + Environment.NewLine + ex.Message,
+ "Erro ao criar mapa",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ }
+
+
private void OnEditarMapaPreparacaoSolicitado()
{
try
@@ -1884,6 +2170,17 @@ namespace OperationControl.ViewModels
}
+ if (_mapas.CriacaoMapaAtiva)
+ {
+ MessageBox.Show(
+ "Finalize ou cancele a criação do mapa antes de abrir o editor.",
+ "Criação de mapa ativa",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ return;
+ }
+
if (_mapas.EdicaoAtiva)
{
/*
@@ -2106,6 +2403,16 @@ namespace OperationControl.ViewModels
return;
}
+ if (_mapas.CriacaoMapaAtiva)
+ {
+ MessageBox.Show(
+ "Finalize ou cancele a criação atual antes de carregar outro mapa.",
+ "Criação de mapa ativa",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ return;
+ }
_mapas.CarregarMapaArquivo(caminhoArquivo, tipo);
@@ -2132,6 +2439,21 @@ namespace OperationControl.ViewModels
{
try
{
+ if (
+ _mapas.CapturaBordaGnssAutomaticaAtiva ||
+ _mapas.CapturaCriacaoAutomaticaAtiva
+ )
+ {
+ MessageBox.Show(
+ "Finalize o levantamento GNSS automático antes de alterar " +
+ "a configuração física da estação.",
+ "Levantamento em andamento",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning
+ );
+ return;
+ }
+
if (!(Models.Variaveis.GpsService.BaseFix?.FixLiberado ?? false))
{
bool fixar = false;
@@ -2287,13 +2609,13 @@ namespace OperationControl.ViewModels
* durante uma captura automática da borda.
*/
if (
- _mapas.CapturaBordaGnssAutomaticaAtiva
+ _mapas.CapturaBordaGnssAutomaticaAtiva ||
+ _mapas.CapturaCriacaoAutomaticaAtiva
)
{
MessageBox.Show(
- "Existe um levantamento GNSS em andamento.\n\n" +
- "Finalize ou descarte a borda antes de alterar " +
- "o modo do receptor.",
+ "Existe um levantamento GNSS automático em andamento.\n\n" +
+ "Finalize a captura antes de alterar o modo do receptor.",
"Levantamento em andamento",
MessageBoxButton.OK,
MessageBoxImage.Warning
diff --git a/AgroBase/OperationControl/Views/PreparacaoMapa/PreparacaoMapaBottomView.xaml b/AgroBase/OperationControl/Views/PreparacaoMapa/PreparacaoMapaBottomView.xaml
index 93eb51299..e4c3fd9a0 100644
--- a/AgroBase/OperationControl/Views/PreparacaoMapa/PreparacaoMapaBottomView.xaml
+++ b/AgroBase/OperationControl/Views/PreparacaoMapa/PreparacaoMapaBottomView.xaml
@@ -510,6 +510,14 @@
Command="{Binding CarregarMapaCommand}"
IsEnabled="{Binding PodeCarregarMapa}"/>
+
+