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