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(); } } }