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." ); } } } }