using Mapsui.Layers; using Mapsui.Nts; using Mapsui.Providers; using Mapsui.Styles; using OperationControl.Mapas.Models; using Brush = Mapsui.Styles.Brush; using Color = Mapsui.Styles.Color; using Pen = Mapsui.Styles.Pen; namespace OperationControl.Mapas.Rendering { /// /// Converte o modelo canônico de mapa em uma representação /// visual compatível com Mapsui. /// /// Este serviço NÃO altera o MapaDocumento. /// Este serviço NÃO possui estado operacional. /// Este serviço NÃO conhece telas WPF. /// public class MapaRenderService { public const string NomeCamadaPadrao = "MapaOperacionalNovo"; /// /// Cria uma nova camada visual a partir do documento. /// /// O documento permanece sendo a fonte de verdade. /// public MapaRenderResult CriarCamada( MapaDocumento documento, string? nomeCamada = null) { if (documento == null) { throw new ArgumentNullException( nameof(documento) ); } if (documento.Ruas == null || documento.Ruas.Count == 0) { throw new InvalidOperationException( "O documento não possui ruas para renderização." ); } var features = new List( documento.Ruas.Count ); var featuresPorRuaId = new Dictionary( StringComparer.OrdinalIgnoreCase ); foreach ( MapaRua rua in documento.Ruas) { if (rua == null) continue; if (string.IsNullOrWhiteSpace( rua.Id)) { throw new InvalidOperationException( "Foi encontrada uma rua sem ID durante a renderização." ); } if (rua.Geometria == null || rua.Geometria.IsEmpty) { throw new InvalidOperationException( $"A rua '{rua.Id}' não possui geometria válida." ); } /* * IMPORTANTÍSSIMO: * * A GeometryFeature recebe uma cópia da geometria. * * Assim nenhuma eventual manipulação realizada * pelo componente visual poderá alterar acidentalmente * a geometria canônica do MapaDocumento. */ var geometriaVisual = rua.Geometria.Copy(); var feature = new GeometryFeature { Geometry = geometriaVisual }; /* * Esses atributos existem SOMENTE para facilitar: * * - identificação visual; * - hit testing; * - depuração; * - futura interação. * * Eles NÃO são fonte da verdade. */ feature["RuaId"] = rua.Id; feature["Id"] = rua.Id; feature["Nome"] = rua.Nome; feature["ComprimentoMetros"] = rua.ComprimentoMetros; /* * Toda feature nasce visualmente como rua normal. * * O estado visual não é armazenado na feature. * O estilo é apenas uma projeção do estado real. */ feature.Styles.Add( CriarEstiloNormal() ); if (!featuresPorRuaId.TryAdd( rua.Id, feature)) { throw new InvalidOperationException( $"O ID de rua '{rua.Id}' está duplicado " + "durante a renderização." ); } features.Add( feature ); } if (features.Count == 0) { throw new InvalidOperationException( "Nenhuma feature visual pôde ser criada." ); } /* * As geometrias do nosso documento são sempre: * * EPSG:4326 * X = longitude * Y = latitude */ var providerWgs84 = new MemoryProvider( features ) { CRS = "EPSG:4326" }; /* * O Mapsui trabalha visualmente em WebMercator. * * A reprojeção fica SOMENTE nesta camada visual. */ var providerWebMercator = new ProjectingProvider( providerWgs84 ) { CRS = "EPSG:3857" }; string nome = string.IsNullOrWhiteSpace( nomeCamada) ? NomeCamadaPadrao : nomeCamada.Trim(); var layer = new Layer( nome ) { DataSource = providerWebMercator, /* * Os estilos pertencem às features visuais. * * Isso permitirá que cada rua tenha seu próprio * estado visual sem duplicar camadas. */ Style = null }; return new MapaRenderResult( layer, featuresPorRuaId ); } /// /// Atualiza a representação visual das ruas de acordo /// com a seleção operacional atual. /// /// IMPORTANTE: /// este método não altera MapaDocumento nem MapaSelecaoOperacao. /// /// Ele apenas faz o Mapsui refletir visualmente o estado real. /// public void AplicarSelecao( MapaRenderResult render, MapaSelecaoOperacao selecao) { if (render == null) { throw new ArgumentNullException( nameof(render) ); } if (selecao == null) { throw new ArgumentNullException( nameof(selecao) ); } var selecionadas = new HashSet( selecao.RuasOrdenadas, StringComparer.OrdinalIgnoreCase ); foreach ( var item in render.FeaturesPorRuaId) { string ruaId = item.Key; GeometryFeature feature = item.Value; bool selecionada = selecionadas.Contains( ruaId ); feature.Styles.Clear(); feature.Styles.Add( selecionada ? CriarEstiloSelecionado() : CriarEstiloNormal() ); } } /// /// Atualiza visualmente apenas uma rua. /// /// Útil para interações pontuais, evitando percorrer /// o mapa inteiro a cada clique. /// public void AtualizarEstiloRua( MapaRenderResult render, MapaSelecaoOperacao selecao, string ruaId) { if (render == null) { throw new ArgumentNullException( nameof(render) ); } if (selecao == null) { throw new ArgumentNullException( nameof(selecao) ); } if (string.IsNullOrWhiteSpace( ruaId)) { throw new ArgumentException( "O ID da rua não foi informado.", nameof(ruaId) ); } string id = ruaId.Trim(); GeometryFeature? feature = render.ObterFeature( id ); if (feature == null) { throw new InvalidOperationException( $"A rua '{id}' não possui representação visual." ); } bool selecionada = selecao.RuasOrdenadas.Contains( id, StringComparer.OrdinalIgnoreCase ); feature.Styles.Clear(); feature.Styles.Add( selecionada ? CriarEstiloSelecionado() : CriarEstiloNormal() ); } private static VectorStyle CriarEstiloNormal() { return new VectorStyle { Line = new Pen( Color.FromArgb( 255, 46, 204, 113 ), 2 ), Fill = new Brush( Color.FromArgb( 60, 46, 204, 113 ) ) }; } private static VectorStyle CriarEstiloSelecionado() { return new VectorStyle { Line = new Pen( Color.FromArgb( 255, 241, 196, 15 ), 4 ), Fill = null }; } } }