using AgroBase.Models; using Newtonsoft.Json; using OperationControl.Mapas.Models; using System.IO; using System.Text; namespace OperationControl.Mapas.Services { public class MapaExportService { private readonly MapaValidationService _validationService; public MapaExportService( MapaValidationService? validationService = null) { _validationService = validationService ?? new MapaValidationService(); } /// /// Converte o documento canônico do Operation Control /// para o contrato atualmente consumido pelo rover. /// /// Nenhuma referência geométrica do documento é compartilhada /// com o objeto exportado. /// public MapaFeatureCollectionModel CriarMapaOperacional( MapaDocumento documento) { MapaResultadoValidacao validacao = _validationService.Validar( documento ); if (!validacao.Valido) { string erros = string.Join( Environment.NewLine, validacao.Erros ); throw new InvalidOperationException( "O mapa não pode ser exportado porque " + "possui erros de validação:" + Environment.NewLine + erros ); } var mapa = new MapaFeatureCollectionModel { type = "FeatureCollection", features = new List() }; foreach ( MapaRua rua in documento.Ruas) { var coordenadas = new List>(); foreach ( var coordenada in rua.Geometria.Coordinates) { /* * Contrato GeoJSON / rover: * * [longitude, latitude] */ coordenadas.Add( new List { coordenada.X, coordenada.Y } ); } /* * Esta igualdade é proposital. * * A partir da arquitetura nova existe apenas * UM ID canônico para a rua. */ string id = rua.Id; mapa.features.Add( new MapaFeatureModel { type = "Feature", properties = new MapaFeaturePropertiesModel { Id = id, Name = string.IsNullOrWhiteSpace( rua.Nome ) ? $"Rua {id}" : rua.Nome, Length = rua.ComprimentoMetros, Dist1 = rua.Dist1, Dist2 = rua.Dist2 }, geometry = new MapaFeatureGeometryModel { id = id, type = "LineString", coordinates = coordenadas } } ); } return mapa; } /// /// Persiste o documento canônico como GeoJSON operacional. /// /// O arquivo contém somente o mapa consolidado: /// ruas, IDs, atributos e geometrias já editadas. /// /// Estado de UI, seleção da missão, Undo/Redo, borda GNSS /// e rota de retorno não fazem parte deste arquivo. /// public void SalvarGeoJson( MapaDocumento documento, string caminhoArquivo) { if (documento == null) { throw new ArgumentNullException( nameof(documento) ); } if (string.IsNullOrWhiteSpace( caminhoArquivo)) { throw new ArgumentException( "O caminho do arquivo GeoJSON não foi informado.", nameof(caminhoArquivo) ); } string caminhoCompleto = Path.GetFullPath( caminhoArquivo ); string extensao = Path.GetExtension( caminhoCompleto ); if ( !string.Equals( extensao, ".geojson", StringComparison.OrdinalIgnoreCase ) && !string.Equals( extensao, ".json", StringComparison.OrdinalIgnoreCase ) ) { throw new InvalidOperationException( "O mapa operacional deve ser salvo como .geojson ou .json." ); } string? diretorio = Path.GetDirectoryName( caminhoCompleto ); if ( string.IsNullOrWhiteSpace( diretorio ) || !Directory.Exists( diretorio ) ) { throw new DirectoryNotFoundException( "A pasta escolhida para salvar o mapa não existe." ); } /* * CriarMapaOperacional valida novamente o documento * e garante o contrato canônico usado pelo rover: * * properties.Id == geometry.id * coordinates = [longitude, latitude] */ MapaFeatureCollectionModel mapa = CriarMapaOperacional( documento ); string json = JsonConvert.SerializeObject( mapa, Formatting.Indented ); /* * Escrita transacional simples: * grava primeiro um temporário na mesma pasta e somente * depois cria/substitui o arquivo final. */ string caminhoTemporario = Path.Combine( diretorio, "." + Path.GetFileName( caminhoCompleto ) + "." + Guid.NewGuid().ToString("N") + ".tmp" ); try { File.WriteAllText( caminhoTemporario, json, new UTF8Encoding( encoderShouldEmitUTF8Identifier: false ) ); File.Move( caminhoTemporario, caminhoCompleto, overwrite: true ); } finally { if (File.Exists( caminhoTemporario)) { try { File.Delete( caminhoTemporario ); } catch { // O arquivo final já salvo continua válido. } } } } } }