using AgroBase.Models; using Mapsui.Layers; using NetTopologySuite.Geometries; using OperationControl.Controls; using OperationControl.Mapas.Editing; using OperationControl.Mapas.Rendering; using OperationControl.Mapas.Services; using System.Diagnostics; using static AgroBase.Models.Enums; namespace OperationControl.Mapas { public sealed partial class MapaOperacaoController { private const double IdadeMaximaPosicaoBordaGnssSegundos = 2.0; private const double DistanciaMinimaPontoManualMetros = 0.02; private int _capturaAutomaticaUiAgendada; private readonly object _snapshotGnssLocalLock = new(); private GPSModel? _ultimaLeituraGnssLocal; private readonly MapaBordaGnssRenderService _bordaGnssRenderService = new(); private readonly Dictionary< MapViewControl, MemoryLayer> _layersBordaGnss = new(); /* * ========================================================= * SNAPSHOT DO GNSS LOCAL * ========================================================= */ private void AtualizarSnapshotGnssLocal(GPSModel dados) { if (dados == null) return; GPSModel copia = dados.Clone(); lock (_snapshotGnssLocalLock) { _ultimaLeituraGnssLocal = copia; } AgendarCapturaAutomaticaSeNecessario(); } private GPSModel? ObterSnapshotGnssLocal() { lock (_snapshotGnssLocalLock) { return _ultimaLeituraGnssLocal? .Clone(); } } /* * ========================================================= * RESUMO PARA O COCKPIT * ========================================================= */ private MapaResumoBordaGnss CriarResumoBordaGnss() { GPSModel? leitura = ObterSnapshotGnssLocal(); bool leituraValida = TentarValidarLeituraGnss( leitura, out double idadeSegundos, out string motivo ); bool qualidadeAlta = leitura != null && ( leitura.QualidadeFix == TiposCorrecaoGPS.RTKFixo || leitura.QualidadeFix == TiposCorrecaoGPS.BaseFix ); string status; if (!leituraValida) { status = motivo; } else { status = $"{DescreverFix(leitura!.QualidadeFix)}" + $" • {leitura.NumeroSatelites} sat" + $" • {idadeSegundos:F1} s"; if ( double.IsFinite( leitura.PrecisaoCm ) ) { status += $" • ~{leitura.PrecisaoCm:F1} cm"; } } return new MapaResumoBordaGnss { Modo = _estadoRecorte.ModoCapturaGnss, QuantidadePontos = _estadoRecorte.QuantidadePontos, ComprimentoMetros = CalcularComprimentoBordaGnss(), CapturaAtiva = _estadoRecorte.CapturaAtiva, Finalizada = _estadoRecorte.BordaGnssFinalizada, LeituraGnssValida = leituraValida, QualidadeAlta = qualidadeAlta, StatusGnss = status, GravacaoAutomaticaAtiva = _estadoRecorte.GravacaoAutomaticaAtiva, DistanciaMinimaAutomaticaMetros = _estadoRecorte.DistanciaMinimaCapturaAutomaticaMetros, }; } private bool TentarValidarLeituraGnss(GPSModel? leitura, out double idadeSegundos, out string motivo) { idadeSegundos = double.PositiveInfinity; if (leitura == null) { motivo = "GNSS sem leitura."; return false; } if (!leitura.Inicializado) { motivo = "GNSS ainda não inicializado."; return false; } if ( !double.IsFinite( leitura.Latitude ) || !double.IsFinite( leitura.Longitude ) || leitura.Latitude < -90.0 || leitura.Latitude > 90.0 || leitura.Longitude < -180.0 || leitura.Longitude > 180.0 || ( Math.Abs( leitura.Latitude ) < 1e-12 && Math.Abs( leitura.Longitude ) < 1e-12 ) ) { motivo = "GNSS sem posição válida."; return false; } double agoraMono = Stopwatch.GetTimestamp() / (double) Stopwatch.Frequency; double timestampPos = leitura .TimestampPos? .valor ?? 0; if (timestampPos > 0) { idadeSegundos = Math.Max( 0, agoraMono - timestampPos ); } else if ( leitura.Momento != default ) { idadeSegundos = Math.Max( 0, ( DateTime.Now - leitura.Momento ) .TotalSeconds ); } if ( !double.IsFinite( idadeSegundos ) || idadeSegundos > IdadeMaximaPosicaoBordaGnssSegundos ) { motivo = double.IsFinite( idadeSegundos ) ? $"Posição GNSS desatualizada ({idadeSegundos:F1} s)." : "Não foi possível determinar a idade da posição."; return false; } motivo = string.Empty; return true; } private static string DescreverFix(TiposCorrecaoGPS fix) { return fix switch { TiposCorrecaoGPS.RTKFixo => "RTK FIX", TiposCorrecaoGPS.RTKFlutuante => "RTK FLOAT", TiposCorrecaoGPS.DGPS => "DGPS", TiposCorrecaoGPS.BaseFix => "BASE FIX", TiposCorrecaoGPS.Autonomo => "AUTÔNOMO", TiposCorrecaoGPS.SemCorrecao => "SEM CORREÇÃO", _ => fix.ToString() }; } /* * ========================================================= * CAPTURA * ========================================================= */ private void RegistrarPontoBordaGnssManual() { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss || !_estadoRecorte.CapturaAtiva ) { return; } GPSModel? leitura = ObterSnapshotGnssLocal(); if ( !TentarValidarLeituraGnss( leitura, out double idade, out string motivo ) ) { Log( "[MAPA][EDICAO][BORDA-GNSS][PONTO][IGNORADO] " + motivo ); SincronizarCockpitEdicao(); return; } var ponto = new Coordinate( leitura!.Longitude, leitura.Latitude ); /* * Manual respeita explicitamente * a vontade do operador. * * Só bloqueamos duplicata praticamente * idêntica. */ if ( _estadoRecorte.Pontos.Count > 0 ) { Coordinate ultimo = _estadoRecorte .Pontos[ _estadoRecorte .Pontos .Count - 1 ]; var linhaTeste = new LineString( new[] { new Coordinate( ultimo.X, ultimo.Y ), new Coordinate( ponto.X, ponto.Y ) } ); double distancia = MapaRecorteService .CalcularComprimentoMetros( linhaTeste ); if ( distancia < DistanciaMinimaPontoManualMetros ) { Log( "[MAPA][EDICAO][BORDA-GNSS][PONTO][IGNORADO] " + $"Duplicata | Dist={distancia:F3} m" ); return; } } _estadoRecorte .AdicionarPontoBordaGnss( ponto ); AtualizarVisualBordaGnss(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS][PONTO] " + $"N={_estadoRecorte.QuantidadePontos} | " + $"Lat={leitura.Latitude:F9} | " + $"Lon={leitura.Longitude:F9} | " + $"Fix={leitura.QualidadeFix} | " + $"Idade={idade:F2}s" ); } private void RemoverUltimoPontoBordaGnss() { if ( !_estadoRecorte .RemoverUltimoPontoBordaGnss() ) { return; } AtualizarVisualBordaGnss(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS] " + "Último ponto removido." ); } private void FinalizarCapturaBordaGnss() { if ( !_estadoRecorte .FinalizarBordaGnss() ) { return; } AtualizarVisualBordaGnss(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS] " + $"Captura finalizada | " + $"Pontos={_estadoRecorte.QuantidadePontos} | " + $"Comprimento={CalcularComprimentoBordaGnss():F2} m" ); } private void ContinuarCapturaBordaGnss() { if ( !_estadoRecorte .ContinuarBordaGnss() ) { return; } SincronizarCockpitEdicao(); Log( "[MAPA][EDICAO][BORDA-GNSS] " + "Captura reaberta." ); } private void DescartarCapturaBordaGnss() { _estadoRecorte .DescartarBordaGnss(); AtualizarVisualBordaGnss(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS] " + "Captura descartada." ); } private double CalcularComprimentoBordaGnss() { if ( _estadoRecorte .Pontos .Count < 2 ) { return 0; } var linha = new LineString( _estadoRecorte .Pontos .Select( p => new Coordinate( p.X, p.Y ) ) .ToArray() ); return MapaRecorteService .CalcularComprimentoMetros( linha ); } /* * ========================================================= * RENDER * ========================================================= */ private void AtualizarVisualBordaGnss() { foreach ( MapViewControl mapa in _mapasEmEdicao .ToArray()) { RenderizarBordaGnssNoMapa( mapa ); mapa .Mapa .Refresh(); } } private void AplicarBordaGnss() { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss ) { return; } /* * A captura precisa estar finalizada. * * Nada de cortar enquanto o operador * ainda estiver levantando a borda. */ if ( !_estadoRecorte .BordaGnssFinalizada || !_estadoRecorte .PodeConcluirLinha ) { Log( "[MAPA][EDICAO][BORDA-GNSS][APLICAR][IGNORADO] " + "A borda ainda não foi finalizada." ); return; } /* * Escopo cirúrgico: * * Somente selecionadas exige pelo menos * uma rua selecionada. */ if ( _estadoRecorte .SomenteSelecionadas && !_estadoEdicao .PossuiSelecao ) { Log( "[MAPA][EDICAO][BORDA-GNSS][APLICAR][IGNORADO] " + "Escopo=SomenteSelecionadas | " + "Nenhuma rua selecionada." ); SincronizarCockpitEdicao(); return; } /* * Copiamos a geometria. * * O service não recebe a coleção interna * mutável do estado. */ Coordinate[] pontos = _estadoRecorte .Pontos .Select( p => new Coordinate( p.X, p.Y ) ) .ToArray(); try { MapaResultadoRecorte resultado = _edicaoService .ApararPorLinha( _estadoEdicao, pontos, somenteSelecionadas: _estadoRecorte .SomenteSelecionadas ); /* * Nenhuma alteração: * * NÃO descartamos a borda. * * Isso é muito importante em campo. * O operador ainda pode olhar a linha, * clicar em Continuar e corrigir/adicionar * novos pontos. */ if (!resultado.HouveAlteracao) { Log( "[MAPA][EDICAO][BORDA-GNSS][APLICAR] " + $"Escopo={_estadoRecorte.Escopo} | " + resultado ); SincronizarCockpitEdicao(); return; } /* * Deu certo. * * A operação inteira já entrou no histórico * como UMA única transação pelo * MapaEdicaoService. * * Consumimos agora a borda utilizada e * deixamos a ferramenta pronta para um * novo levantamento. */ _estadoRecorte .DescartarBordaGnss(); /* * Houve alteração geométrica. * * Precisamos reconstruir o render * do DocumentoTrabalho. */ RenderizarDocumentoEdicaoAtual(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS][APLICAR] " + $"Escopo={_estadoRecorte.Escopo} | " + resultado + $" | Undo={PodeDesfazerEdicao} | " + $"Redo={PodeRefazerEdicao}" ); } catch (Exception ex) { /* * MapaEdicaoService é transacional. * * Em erro a geometria volta para o * snapshot anterior. * * A borda GNSS permanece na tela para * o operador não perder o levantamento. */ Log( "[MAPA][EDICAO][BORDA-GNSS][APLICAR][ERRO] " + ex.Message ); SincronizarCockpitEdicao(); } } private void RenderizarBordaGnssNoMapa(MapViewControl mapaControl) { if ( mapaControl?.Mapa?.Map == null ) { return; } bool exibir = _estadoEdicao.EmEdicao && _mapasEmEdicao.Contains( mapaControl ) && _ferramentaEdicaoAtual == MapaFerramentaEdicao .RecorteBordaGnss && _estadoRecorte.Tipo == MapaTipoRecorte .BordaGnss; if ( !_layersBordaGnss .TryGetValue( mapaControl, out MemoryLayer? camada ) ) { camada = _bordaGnssRenderService .CriarCamada( _estadoRecorte ); mapaControl .Mapa .Map .Layers .Add( camada ); _layersBordaGnss[ mapaControl ] = camada; } if (exibir) { _bordaGnssRenderService .AtualizarCamada( camada, _estadoRecorte ); } else { camada.Features = Array.Empty< Mapsui.IFeature>(); } /* * Preview fica sobre o mapa, * mas Rover/Base continuam no topo. */ var layers = mapaControl .Mapa .Map .Layers; if ( layers.Contains( camada ) ) { layers.Remove( camada ); layers.Add( camada ); } GarantirEntidadesNoTopo( mapaControl ); } private void DefinirModoCapturaBordaGnss(MapaModoCapturaGnss modo) { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss ) { return; } if ( !_estadoRecorte .DefinirModoCapturaGnss( modo ) ) { return; } SincronizarCockpitEdicao(); Log( "[MAPA][EDICAO][BORDA-GNSS][MODO] " + $"Atual={modo}" ); } private void IniciarGravacaoBordaGnssAutomatica() { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss ) { return; } GPSModel? leitura = ObterSnapshotGnssLocal(); if ( !TentarValidarLeituraGnss( leitura, out double idade, out string motivo ) ) { Log( "[MAPA][EDICAO][BORDA-GNSS][AUTO][INICIO][IGNORADO] " + motivo ); SincronizarCockpitEdicao(); return; } if ( !_estadoRecorte .IniciarGravacaoAutomatica() ) { return; } /* * Registra imediatamente a posição * onde o operador apertou INICIAR. */ ProcessarPontoAutomatico( leitura!, forcarPrimeiroPonto: true ); SincronizarCockpitEdicao(); Log( "[MAPA][EDICAO][BORDA-GNSS][AUTO] " + "Gravação iniciada | " + $"DistMin=" + $"{_estadoRecorte.DistanciaMinimaCapturaAutomaticaMetros:F2} m | " + $"Fix={leitura!.QualidadeFix} | " + $"Idade={idade:F2}s" ); } private void ProcessarPontoAutomatico(GPSModel leitura, bool forcarPrimeiroPonto = false) { if ( leitura == null || !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss || !_estadoRecorte .GravacaoAutomaticaAtiva ) { return; } if ( !TentarValidarLeituraGnss( leitura, out double idade, out _ ) ) { return; } var ponto = new Coordinate( leitura.Longitude, leitura.Latitude ); bool adicionar = _estadoRecorte .QuantidadePontos == 0; double distancia = 0; if ( !adicionar && _estadoRecorte .QuantidadePontos > 0 ) { Coordinate ultimo = _estadoRecorte .Pontos[ _estadoRecorte .QuantidadePontos - 1 ]; var trecho = new LineString( new[] { new Coordinate( ultimo.X, ultimo.Y ), new Coordinate( ponto.X, ponto.Y ) } ); distancia = MapaRecorteService .CalcularComprimentoMetros( trecho ); adicionar = distancia >= _estadoRecorte .DistanciaMinimaCapturaAutomaticaMetros; } if ( forcarPrimeiroPonto && _estadoRecorte.QuantidadePontos == 0 ) { adicionar = true; } if (!adicionar) return; _estadoRecorte .AdicionarPontoBordaGnss( ponto ); AtualizarVisualBordaGnss(); SincronizarCockpitEdicao(); EstadoEdicaoAlterado? .Invoke(); Log( "[MAPA][EDICAO][BORDA-GNSS][AUTO][PONTO] " + $"N={_estadoRecorte.QuantidadePontos} | " + $"Dist={distancia:F2} m | " + $"Lat={leitura.Latitude:F9} | " + $"Lon={leitura.Longitude:F9} | " + $"Fix={leitura.QualidadeFix} | " + $"Idade={idade:F2}s" ); } private void AgendarCapturaAutomaticaSeNecessario() { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss || !_estadoRecorte .GravacaoAutomaticaAtiva ) { return; } /* * Evita colocar 300 delegates na fila * caso o GNSS esteja publicando rápido. */ if ( System.Threading .Interlocked .Exchange( ref _capturaAutomaticaUiAgendada, 1 ) != 0 ) { return; } var dispatcher = System.Windows .Application .Current? .Dispatcher; if (dispatcher == null) { System.Threading .Interlocked .Exchange( ref _capturaAutomaticaUiAgendada, 0 ); return; } dispatcher.BeginInvoke( new Action( () => { try { if ( !_estadoEdicao.EmEdicao || _ferramentaEdicaoAtual != MapaFerramentaEdicao .RecorteBordaGnss || !_estadoRecorte .GravacaoAutomaticaAtiva ) { return; } GPSModel? leitura = ObterSnapshotGnssLocal(); if (leitura == null) return; ProcessarPontoAutomatico( leitura ); } finally { System.Threading .Interlocked .Exchange( ref _capturaAutomaticaUiAgendada, 0 ); } } ), System.Windows.Threading .DispatcherPriority.Background ); } } }