using AgroBase.Models; using BruTile; using BruTile.MbTiles; using BruTile.Predefined; using Mapsui; using Mapsui.Extensions; using Mapsui.Layers; using Mapsui.Manipulations; using Mapsui.Projections; using Mapsui.Styles; using Mapsui.Tiling.Layers; using OperationControl.Mapas.Editing; using OperationControl.Mapas.Interaction; using OperationControl.Models; using SQLite; using System.IO; using System.Windows; using System.Windows.Controls; using Brush = Mapsui.Styles.Brush; using Color = Mapsui.Styles.Color; using Font = Mapsui.Styles.Font; using MIFeature = Mapsui.IFeature; using Pen = Mapsui.Styles.Pen; // Aliases para evitar conflito com NetTopologySuite.* (NTS) using UserControl = System.Windows.Controls.UserControl; namespace OperationControl.Controls { public partial class MapViewControl : UserControl { public MapViewControl() { InitializeComponent(); EditorOverlay.FerramentaSelecionada += ferramenta => { MapaEditorFerramentaSelecionada?.Invoke(this, ferramenta); }; EditorOverlay.LimparSelecaoSolicitado += () => { MapaEditorLimparSelecaoSolicitado?.Invoke(this); }; EditorOverlay.DesfazerSolicitado += () => { MapaEditorDesfazerSolicitado?.Invoke(this); }; EditorOverlay.RefazerSolicitado += () => { MapaEditorRefazerSolicitado?.Invoke(this); }; EditorOverlay.ExcluirSelecionadasSolicitado += () => { MapaEditorExcluirSelecionadasSolicitado?.Invoke(this); }; EditorOverlay.AplicarEdicaoSolicitado += () => { MapaEditorAplicarEdicaoSolicitado?.Invoke(this); }; EditorOverlay.CancelarEdicaoSolicitado += () => { MapaEditorCancelarEdicaoSolicitado?.Invoke(this); }; EditorOverlay.EscopoRecorteAlterado += escopo => { MapaEditorEscopoRecorteAlterado?.Invoke(this, escopo); }; EditorOverlay.BordaGnssAcaoSolicitada += acao => { MapaEditorBordaGnssAcaoSolicitada?.Invoke(this, acao); }; EditorOverlay.RestaurarMapaFonteSolicitado += () => { MapaEditorRestaurarFonteSolicitado?.Invoke(this); }; Loaded += OnLoaded; HookHitTesting(); } private Layer? _mapaNovoLayer; private ILayer? _mapaEntidadesDinamicasLayer; private MapaModoInteracao _modoInteracao = MapaModoInteracao.Visualizacao; private MapaFerramentaEdicao _ferramentaEdicaoAtual = MapaFerramentaEdicao.Nenhuma; public MapaModoInteracao ModoInteracao => _modoInteracao; public void DefinirModoInteracao(MapaModoInteracao modo) { if (_modoInteracao == modo) return; _modoInteracao = modo; if (Mapa != null) { Mapa.Cursor = modo == MapaModoInteracao.RetornoManual ? System.Windows.Input.Cursors.Cross : System.Windows.Input.Cursors.Arrow; } } public void DefinirCamadaEntidadesDinamicasNova(ILayer? layer) { _mapaEntidadesDinamicasLayer = layer; } public void DefinirCamadaMapaNovo(Layer? layer) { _mapaNovoLayer = layer; } public event Action? MapaNovaEntidadeClicada; public event Action? MapaNovoRuaClicada; public event Action? MapaNovoPontoRetornoClicado; public event Action? MapaNovoViewportAlterado; public event Action? MapaEditorFerramentaSelecionada; public event Action? MapaEditorLimparSelecaoSolicitado; public event Action? MapaEditorRetanguloSelecionado; public event Action? MapaEditorDesfazerSolicitado; public event Action? MapaEditorRefazerSolicitado; public event Action? MapaEditorExcluirSelecionadasSolicitado; public event Action? MapaEditorAplicarEdicaoSolicitado; public event Action? MapaEditorCancelarEdicaoSolicitado; public event Action? MapaEditorPoligonoRecorteDefinido; public event Action? MapaEditorEscopoRecorteAlterado; public event Action? MapaEditorBordaGnssAcaoSolicitada; public event Action? MapaEditorRestaurarFonteSolicitado; private bool _arrastandoRetanguloEdicao; private System.Windows.Point _inicioRetanguloEdicao; private bool _desenhandoRecorteLivre; private readonly List _pontosRecorteLivre = new(); private readonly List _pontosRecortePorPontos = new(); private const double DistanciaMinimaPontoRecortePixels = 6.0; private const double AreaMinimaRecortePixelsQuadrados = 40.0; private bool EmRecorteLivre => ModoInteracao == MapaModoInteracao.Edicao && _ferramentaEdicaoAtual == MapaFerramentaEdicao.RecorteLivre; private bool EmRecortePorPontos => ModoInteracao == MapaModoInteracao.Edicao && _ferramentaEdicaoAtual == MapaFerramentaEdicao.RecortePorPontos; private bool EmSelecaoRetangulo => ModoInteracao == MapaModoInteracao.Edicao && _ferramentaEdicaoAtual == MapaFerramentaEdicao.SelecionarRetangulo; private static bool CtrlPressionado => System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftCtrl) || System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.RightCtrl); public void EnquadrarAreaWgs84(double minLongitude, double minLatitude, double maxLongitude, double maxLatitude, double margem = 1.15) { if (Mapa?.Map?.Navigator == null) return; if ( !double.IsFinite(minLongitude) || !double.IsFinite(minLatitude) || !double.IsFinite(maxLongitude) || !double.IsFinite(maxLatitude) ) { return; } if ( minLongitude < -180.0 || maxLongitude > 180.0 || minLatitude < -90.0 || maxLatitude > 90.0 ) { return; } try { /* * Documento está em WGS84. * Viewport do Mapsui está em WebMercator. */ var canto1 = SphericalMercator.FromLonLat( minLongitude, minLatitude ); var canto2 = SphericalMercator.FromLonLat( maxLongitude, maxLatitude ); double minX = Math.Min( canto1.x, canto2.x ); double maxX = Math.Max( canto1.x, canto2.x ); double minY = Math.Min( canto1.y, canto2.y ); double maxY = Math.Max( canto1.y, canto2.y ); /* * Espera o WPF terminar o layout. * * Isso é importante principalmente quando * acabamos de trocar a view. */ Dispatcher.BeginInvoke( new Action(() => { if (Mapa?.Map?.Navigator == null) return; double larguraPx = Mapa.ActualWidth; double alturaPx = Mapa.ActualHeight; if ( larguraPx <= 1.0 || alturaPx <= 1.0 ) { return; } double larguraM = maxX - minX; double alturaM = maxY - minY; /* * Proteção para mapas extremamente estreitos * ou até uma única linha horizontal/vertical. */ larguraM = Math.Max( larguraM, 10.0 ); alturaM = Math.Max( alturaM, 10.0 ); /* * metros / pixel necessários para * caber toda a geometria. */ double resolucaoX = larguraM / larguraPx; double resolucaoY = alturaM / alturaPx; double resolution = Math.Max( resolucaoX, resolucaoY ); /* * > 1 = um pouco mais afastado. * * 1.15 dá aproximadamente 15% de folga. */ resolution *= Math.Max( margem, 1.0 ); var centro = new MPoint( (minX + maxX) / 2.0, (minY + maxY) / 2.0 ); Mapa.Map.Navigator .CenterOnAndZoomTo( centro, resolution ); Mapa.Refresh(); }), System.Windows.Threading .DispatcherPriority.Loaded ); } catch (Exception ex) { Models.Variaveis.MostrarLog( "[MAPA][ENQUADRAR][ERRO] " + ex.Message ); } } private IReadOnlyList ObterCamadasInterativas() { if (Mapa?.Map?.Layers == null) return Array.Empty(); /* * A lista de hit-test também depende do modo. * * Isso evita que a camada de Rover/Base, que fica * visualmente acima das ruas, "roube" um clique * destinado à seleção de uma rua. */ return _modoInteracao switch { MapaModoInteracao.SelecaoEntidade when _mapaEntidadesDinamicasLayer != null => new ILayer[] { _mapaEntidadesDinamicasLayer! }, MapaModoInteracao.SelecaoOperacao when _mapaNovoLayer != null => new ILayer[] { _mapaNovoLayer! }, MapaModoInteracao.Edicao when _mapaNovoLayer != null => new ILayer[] { _mapaNovoLayer! }, _ => Array.Empty() }; } public void DefinirEstadoEditor( bool ativo, MapaFerramentaEdicao ferramenta, int quantidadeSelecionadas, bool modificado, bool podeDesfazer, bool podeRefazer, MapaEscopoRecorte escopoRecorte = MapaEscopoRecorte.TodasRuasAtingidas, MapaResumoBordaGnss? bordaGnss = null, bool possuiMapaFonte = false) { /* * A troca de ferramenta nunca pode herdar * um gesto iniciado anteriormente. */ if ( !ativo || ferramenta != _ferramentaEdicaoAtual ) { CancelarGestoEditor(); } _ferramentaEdicaoAtual = ativo ? ferramenta : MapaFerramentaEdicao.Nenhuma; bool ferramentaDesenho = ativo && ( ferramenta == MapaFerramentaEdicao .SelecionarRetangulo || ferramenta == MapaFerramentaEdicao .RecorteLivre ); Mapa.Cursor = ferramentaDesenho ? System.Windows.Input.Cursors.Cross : null; EditorInteractionCanvas.IsHitTestVisible = false; EditorOverlay.DefinirEstado( ativo, ferramenta, quantidadeSelecionadas, modificado, podeDesfazer, podeRefazer, escopoRecorte, bordaGnss, possuiMapaFonte ); if (ativo) { Dispatcher.BeginInvoke( new Action( () => { Root.Focus(); System.Windows.Input.Keyboard.Focus(Root); } ), System.Windows.Threading.DispatcherPriority.Input ); } } private void CancelarGestoEditor() { /* * ===================================================== * RETÂNGULO * ===================================================== */ _arrastandoRetanguloEdicao = false; EditorSelectionRectangle.Visibility = Visibility.Collapsed; EditorSelectionRectangle.Width = 0; EditorSelectionRectangle.Height = 0; /* * ===================================================== * RECORTE LIVRE * ===================================================== */ _desenhandoRecorteLivre = false; _pontosRecorteLivre .Clear(); _pontosRecortePorPontos .Clear(); EditorCutPolygonFill .Points .Clear(); EditorCutVerticesCanvas .Children .Clear(); EditorCutPolyline .Points .Clear(); EditorCutPolygonFill.Visibility = Visibility.Collapsed; EditorCutPolyline.Visibility = Visibility.Collapsed; EditorCutClosingLine.Visibility = Visibility.Collapsed; /* * Garantia de que o Mapsui jamais * permaneça sequestrado. */ Mapa.IsHitTestVisible = true; if (Root.IsMouseCaptured) { Root.ReleaseMouseCapture(); } } public static readonly DependencyProperty LatitudeProperty = DependencyProperty.Register(nameof(Latitude), typeof(double), typeof(MapViewControl), new PropertyMetadata(0.0, OnViewPropertyChanged)); public static readonly DependencyProperty LongitudeProperty = DependencyProperty.Register(nameof(Longitude), typeof(double), typeof(MapViewControl), new PropertyMetadata(0.0, OnViewPropertyChanged)); public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register(nameof(Scale), typeof(double), typeof(MapViewControl), new PropertyMetadata(5000.0, OnViewPropertyChanged)); public static readonly DependencyProperty MbTilesPathProperty = DependencyProperty.Register(nameof(MbTilesPath), typeof(string), typeof(MapViewControl), new PropertyMetadata(string.Empty, OnMbTilesPathChanged)); public double Latitude { get => (double)GetValue(LatitudeProperty); set => SetValue(LatitudeProperty, value); } public double Longitude { get => (double)GetValue(LongitudeProperty); set => SetValue(LongitudeProperty, value); } public double Scale { get => (double)GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } #region BASE LAYER private TileLayer? _baseLayer; private List _baseOptions; public string MbTilesPath { get => (string)GetValue(MbTilesPathProperty); set => SetValue(MbTilesPathProperty, value); } private class BaseLayerOption { public string Name { get; set; } = ""; public Func CreateLayer { get; set; } = default!; public override string ToString() => Name; } private void InitBaseMapOptions() { _baseOptions = new List { new BaseLayerOption { Name = "OpenStreetMap", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.OpenStreetMap); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "OpenCycleMap", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.OpenCycleMap); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "OpenCycleMapTransport", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.OpenCycleMapTransport); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldBoundariesAndPlaces", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldBoundariesAndPlaces); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldDarkGrayBase", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldDarkGrayBase); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldPhysical", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldPhysical); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldReferenceOverlay", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldReferenceOverlay); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldShadedRelief", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldShadedRelief); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldTopo", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldTopo); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "EsriWorldTransportation", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.EsriWorldTransportation); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "BKGTopPlusColor", CreateLayer = () => { var src = KnownTileSources.Create(KnownTileSource.BKGTopPlusColor); var layer = new TileLayer(src) { Name = "_BaseTile" }; return layer; } }, new BaseLayerOption { Name = "Deep Zoom (Empty)", CreateLayer = () => { // Sem tile de fundo: devolve null e tratamos na troca return null!; } } // Se quiser Bing, precisa de API key: // new BaseLayerOption // { // Name = "Bing Aerial (requer key)", // CreateLayer = () => { // var key = "SUA_BING_MAPS_KEY"; // var src = KnownTileSources.Create(KnownTileSource.BingAerial, key); // return new TileLayer(src){ Name = "_BaseTile" }; // } // }, }; CmbBaseMap.ItemsSource = _baseOptions; CmbBaseMap.SelectedIndex = 0; } private void ReplaceBaseLayer(BaseLayerOption option) { if (Mapa?.Map == null) return; // 1) guardar o centro aproximado (compatível entre versões) var center = GetCenterWorldSafe(); // 2) remover base antiga (se houver) if (_baseLayer != null && Mapa.Map.Layers.Contains(_baseLayer)) Mapa.Map.Layers.Remove(_baseLayer); // 3) criar nova base e inserir no começo da lista (fica no fundo) var newBase = option.CreateLayer() as TileLayer; if (newBase != null) { Mapa.Map.Layers.Insert(0, newBase); _baseLayer = newBase; HideOverlay(); } // 4) recentrar onde estava (se conseguir ler o centro) if (center != null) { try { Mapa.Map.Navigator?.CenterOn(center); } catch { /* versões antigas */ } } Mapa.Refresh(); } private Mapsui.MPoint? GetCenterWorldSafe() { try { // tentativas em versões diferentes dynamic nav = Mapa?.Map?.Navigator; if (nav != null) { try { var c = nav.Center; if (c != null) return (Mapsui.MPoint)c; } catch { } try { var vp = nav.Viewport; if (vp != null) { var cx = (double)vp.CenterX; var cy = (double)vp.CenterY; return new Mapsui.MPoint(cx, cy); } } catch { } } dynamic vp2 = Mapa?.Map?.Navigator?.Viewport; if (vp2 != null) { var cx = (double)vp2.CenterX; var cy = (double)vp2.CenterY; return new Mapsui.MPoint(cx, cy); } } catch { } return null; } private void OnBaseMapChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { if (CmbBaseMap.SelectedItem is BaseLayerOption opt) ReplaceBaseLayer(opt); } #endregion private void OnLoaded(object sender, RoutedEventArgs e) { if (Mapa.Map == null) { Mapa.Map = new Mapsui.Map(); Mapa.Map.BackColor = Color.FromString("#111111"); } Mapa.Map.Widgets.Clear(); Mapa.MouseWheel += (s, e) => { /* * O Mapsui ainda processará o zoom. * Agendamos nossa atualização para depois * da alteração do viewport. */ Dispatcher.BeginInvoke( new Action( () => { MapaNovoViewportAlterado?.Invoke(this); } ), System.Windows.Threading.DispatcherPriority.Background ); }; InitBaseMapOptions(); EnsureBaseLayer(); NavigateToView(); } private static void OnViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is MapViewControl ctrl && ctrl.IsLoaded) { ctrl.NavigateToView(); } } private static void OnMbTilesPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is MapViewControl ctrl && ctrl.IsLoaded) { ctrl.EnsureBaseLayer(); } } private void HookHitTesting() { Mapa.PreviewMouseLeftButtonDown += (s, e) => { GarantirFocoEditor(); }; Mapa.MouseLeftButtonUp += (s, e) => { try { if ( _modoInteracao == MapaModoInteracao.Edicao && _ferramentaEdicaoAtual != MapaFerramentaEdicao.Selecionar ) { return; } var pos = e.GetPosition( Mapa ); var sp = new ScreenPosition( pos.X, pos.Y ); /* * Retorno manual não depende de uma feature. * O clique representa diretamente uma * coordenada WGS84 do mapa. */ if ( _modoInteracao == MapaModoInteracao.RetornoManual ) { if ( TentarObterCoordenadaWgs84( sp, out double latitude, out double longitude ) ) { MapaNovoPontoRetornoClicado? .Invoke( latitude, longitude ); } return; } var camadas = ObterCamadasInterativas(); if (camadas.Count == 0) return; var info = Mapa.GetMapInfo( sp, camadas ); var feat = info?.Feature; /* * Seleção de Base/Rover. */ if ( _modoInteracao == MapaModoInteracao.SelecaoEntidade && _mapaEntidadesDinamicasLayer != null && ReferenceEquals( info?.Layer, _mapaEntidadesDinamicasLayer ) && feat is MIFeature entidadeFeature ) { string? entidadeId = GetAttr( entidadeFeature, "EntidadeId" ); if ( !string.IsNullOrWhiteSpace( entidadeId ) ) { MapaNovaEntidadeClicada? .Invoke( entidadeId.Trim() ); } return; } /* * Rua operacional ou rua em edição. * * O MapViewControl não precisa saber qual * serviço irá tratar o ID. Ele apenas informa * a rua clicada. */ if ( ( _modoInteracao == MapaModoInteracao.SelecaoOperacao || _modoInteracao == MapaModoInteracao.Edicao ) && _mapaNovoLayer != null && ReferenceEquals( info?.Layer, _mapaNovoLayer ) && feat is MIFeature mapaFeature ) { string? ruaId = GetAttr( mapaFeature, "RuaId" ); if ( string.IsNullOrWhiteSpace( ruaId ) ) { ruaId = GetAttr( mapaFeature, "Id" ); } if ( !string.IsNullOrWhiteSpace( ruaId ) ) { MapaNovoRuaClicada? .Invoke( ruaId.Trim() ); } } } catch { /* * Hit-test não deve derrubar a interface. */ } }; Mapa.MouseMove += (s, e) => { try { if ( EmSelecaoRetangulo || EmRecorteLivre || EmRecortePorPontos ) { Mapa.Cursor = CtrlPressionado ? System.Windows.Input.Cursors.SizeAll : System.Windows.Input.Cursors.Cross; return; } if ( _modoInteracao == MapaModoInteracao.RetornoManual ) { Mapa.Cursor = System.Windows.Input.Cursors.Cross; return; } var camadas = ObterCamadasInterativas(); if (camadas.Count == 0) { Mapa.Cursor = System.Windows.Input.Cursors.Arrow; return; } var pos = e.GetPosition( Mapa ); var sp = new ScreenPosition( pos.X, pos.Y ); var info = Mapa.GetMapInfo( sp, camadas ); Mapa.Cursor = info?.Feature != null ? System.Windows.Input.Cursors.Hand : System.Windows.Input.Cursors.Arrow; } catch { Mapa.Cursor = System.Windows.Input.Cursors.Arrow; } }; } private bool TentarObterCoordenadaWgs84(ScreenPosition sp, out double latitude, out double longitude) { latitude = 0.0; longitude = 0.0; try { var world = Mapa.Map.Navigator.Viewport .ScreenToWorld( sp.X, sp.Y ); var lonLat = Mapsui.Projections .SphericalMercator .ToLonLat( world.X, world.Y ); latitude = lonLat.lat; longitude = lonLat.lon; if ( double.IsNaN(latitude) || double.IsInfinity(latitude) || latitude < -90.0 || latitude > 90.0 ) { return false; } if ( double.IsNaN(longitude) || double.IsInfinity(longitude) || longitude < -180.0 || longitude > 180.0 ) { return false; } return true; } catch { return false; } } #region RETORNO A BASE MANUAL private MemoryLayer _layerPontosSelecionados; public List _pontosSelecionados = new List(); private void RedesenharPontosSelecionados() { var features = new List(); for (int i = 0; i < _pontosSelecionados.Count; i++) { var p = _pontosSelecionados[i]; var mercator = Mapsui.Projections.SphericalMercator.FromLonLat(p.Longitude, p.Latitude); var feature = new PointFeature(mercator.x, mercator.y); // número do ponto feature["label"] = (i + 1).ToString(); // círculo feature.Styles.Add(new SymbolStyle { SymbolType = SymbolType.Ellipse, SymbolScale = 1.2, Fill = new Brush(Color.Red), Outline = new Pen(Color.White, 2) }); // texto (número) feature.Styles.Add(new LabelStyle { LabelColumn = "label", ForeColor = Color.White, BackColor = null, HorizontalAlignment = LabelStyle.HorizontalAlignmentEnum.Center, VerticalAlignment = LabelStyle.VerticalAlignmentEnum.Center, Offset = new Offset(0, 0), Font = new Font { Size = 12 } }); features.Add(feature); } if (_layerPontosSelecionados == null) { _layerPontosSelecionados = new MemoryLayer { Name = "PontosSelecionados" }; Mapa.Map.Layers.Add(_layerPontosSelecionados); } _layerPontosSelecionados.Features = features; Mapa.Refresh(); } public void LimparPontosSelecionados() { _pontosSelecionados = new List(); RedesenharPontosSelecionados(); VariaveisControleOperacao.RoverEmFoco.PontosRetorno = null; } #endregion private void AtualizarPreviewRetangulo(System.Windows.Point inicio, System.Windows.Point atual) { double x = Math.Min( inicio.X, atual.X ); double y = Math.Min( inicio.Y, atual.Y ); double largura = Math.Abs( atual.X - inicio.X ); double altura = Math.Abs( atual.Y - inicio.Y ); Canvas.SetLeft( EditorSelectionRectangle, x ); Canvas.SetTop( EditorSelectionRectangle, y ); EditorSelectionRectangle.Width = largura; EditorSelectionRectangle.Height = altura; } private void FinalizarRetanguloEdicao(System.Windows.Point fim) { try { if (!_arrastandoRetanguloEdicao) return; _arrastandoRetanguloEdicao = false; Root.ReleaseMouseCapture(); EditorSelectionRectangle .Visibility = Visibility.Collapsed; /* * Evita transformar um clique comum * em um retângulo microscópico. */ double largura = Math.Abs( fim.X - _inicioRetanguloEdicao.X ); double altura = Math.Abs( fim.Y - _inicioRetanguloEdicao.Y ); if ( largura < 5 || altura < 5 ) { return; } double minX = Math.Min( _inicioRetanguloEdicao.X, fim.X ); double minY = Math.Min( _inicioRetanguloEdicao.Y, fim.Y ); double maxX = Math.Max( _inicioRetanguloEdicao.X, fim.X ); double maxY = Math.Max( _inicioRetanguloEdicao.Y, fim.Y ); /* * Pegamos dois cantos opostos. * * Depois normalizamos usando min/max * de latitude e longitude. */ var canto1 = new ScreenPosition( minX, minY ); var canto2 = new ScreenPosition( maxX, maxY ); if ( !TentarObterCoordenadaWgs84( canto1, out double lat1, out double lon1 ) || !TentarObterCoordenadaWgs84( canto2, out double lat2, out double lon2 ) ) { return; } var retangulo = new MapaRetanguloSelecao( lat1, lon1, lat2, lon2 ); MapaEditorRetanguloSelecionado? .Invoke( this, retangulo ); } finally { CancelarGestoEditor(); } } private bool TentarEmitirPoligonoRecorte(IReadOnlyList pontosTela) { if ( pontosTela == null || pontosTela.Count < 3 ) { return false; } double areaTela = CalcularAreaPoligonoTela( pontosTela ); if ( areaTela < AreaMinimaRecortePixelsQuadrados ) { return false; } var pontosWgs84 = new List(); foreach ( var pontoTela in pontosTela) { var screenPosition = new ScreenPosition( pontoTela.X, pontoTela.Y ); if ( !TentarObterCoordenadaWgs84( screenPosition, out double latitude, out double longitude ) ) { return false; } pontosWgs84.Add( new MapaPontoWgs84( latitude, longitude ) ); } var recorte = new MapaPoligonoRecorte( pontosWgs84 ); MapaEditorPoligonoRecorteDefinido? .Invoke( this, recorte ); return true; } private void AtualizarPreviewRecorteLivre() { if ( _pontosRecorteLivre.Count == 0 ) { return; } var pontosPolyline = new System.Windows.Media .PointCollection(); var pontosPolygon = new System.Windows.Media .PointCollection(); foreach ( var ponto in _pontosRecorteLivre) { pontosPolyline.Add( ponto ); pontosPolygon.Add( ponto ); } EditorCutPolyline.Points = pontosPolyline; EditorCutPolygonFill.Points = pontosPolygon; EditorCutPolyline.Visibility = Visibility.Visible; if ( _pontosRecorteLivre.Count >= 3 ) { EditorCutPolygonFill.Visibility = Visibility.Visible; } if ( _pontosRecorteLivre.Count >= 2 ) { var primeiro = _pontosRecorteLivre[0]; var ultimo = _pontosRecorteLivre[ _pontosRecorteLivre.Count - 1 ]; EditorCutClosingLine.X1 = ultimo.X; EditorCutClosingLine.Y1 = ultimo.Y; EditorCutClosingLine.X2 = primeiro.X; EditorCutClosingLine.Y2 = primeiro.Y; EditorCutClosingLine.Visibility = Visibility.Visible; } } private void AdicionarPontoRecorteLivre(System.Windows.Point ponto, bool forcar = false) { if ( _pontosRecorteLivre.Count > 0 ) { var ultimo = _pontosRecorteLivre[ _pontosRecorteLivre.Count - 1 ]; double dx = ponto.X - ultimo.X; double dy = ponto.Y - ultimo.Y; double distanciaQuadrada = dx * dx + dy * dy; double minimo = forcar ? 1.0 : DistanciaMinimaPontoRecortePixels; if ( distanciaQuadrada < minimo * minimo ) { return; } } _pontosRecorteLivre.Add( ponto ); AtualizarPreviewRecorteLivre(); } private static double CalcularAreaPoligonoTela(IReadOnlyList pontos) { if ( pontos == null || pontos.Count < 3 ) { return 0; } double soma = 0; for ( int i = 0; i < pontos.Count; i++) { var atual = pontos[i]; var proximo = pontos[ (i + 1) % pontos.Count ]; soma += atual.X * proximo.Y - proximo.X * atual.Y; } return Math.Abs( soma ) / 2.0; } private void FinalizarRecorteLivre(System.Windows.Point fim) { try { if (!_desenhandoRecorteLivre) return; /* * Marcamos como encerrado ANTES de * liberar a captura, evitando que * LostMouseCapture interprete isso * como cancelamento inesperado. */ _desenhandoRecorteLivre = false; if ( Root.IsMouseCaptured ) { Root.ReleaseMouseCapture(); } /* * Tenta aproveitar a posição final. */ AdicionarPontoRecorteLivre( fim, forcar: true ); TentarEmitirPoligonoRecorte(_pontosRecorteLivre); } finally { /* * Preview nunca fica pendurado, * mesmo se o Controller lançar erro. */ CancelarGestoEditor(); } } private void AtualizarPreviewRecortePorPontos(System.Windows.Point? ponteiroAtual = null) { var pontosVisuais = new List( _pontosRecortePorPontos ); /* * Durante o movimento do mouse mostramos * para onde iria o próximo segmento. */ if ( ponteiroAtual.HasValue && _pontosRecortePorPontos.Count > 0 ) { pontosVisuais.Add( ponteiroAtual.Value ); } var pontosLinha = new System.Windows.Media .PointCollection(); var pontosArea = new System.Windows.Media .PointCollection(); foreach ( var ponto in pontosVisuais) { pontosLinha.Add( ponto ); pontosArea.Add( ponto ); } EditorCutPolyline.Points = pontosLinha; EditorCutPolygonFill.Points = pontosArea; EditorCutPolyline.Visibility = pontosVisuais.Count > 0 ? Visibility.Visible : Visibility.Collapsed; EditorCutPolygonFill.Visibility = pontosVisuais.Count >= 3 ? Visibility.Visible : Visibility.Collapsed; /* * Fechamento virtual: * * último ponto atual → primeiro ponto. */ if ( pontosVisuais.Count >= 2 ) { var primeiro = pontosVisuais[0]; var ultimo = pontosVisuais[ pontosVisuais.Count - 1 ]; EditorCutClosingLine.X1 = ultimo.X; EditorCutClosingLine.Y1 = ultimo.Y; EditorCutClosingLine.X2 = primeiro.X; EditorCutClosingLine.Y2 = primeiro.Y; EditorCutClosingLine.Visibility = Visibility.Visible; } else { EditorCutClosingLine.Visibility = Visibility.Collapsed; } AtualizarMarcadoresRecortePorPontos(); } private void AtualizarMarcadoresRecortePorPontos() { EditorCutVerticesCanvas .Children .Clear(); foreach ( var ponto in _pontosRecortePorPontos) { var marcador = new System.Windows.Shapes .Ellipse { Width = 9, Height = 9, Fill = new System.Windows.Media .SolidColorBrush( System.Windows.Media .Color .FromRgb( 255, 103, 92 ) ), Stroke = System.Windows.Media .Brushes.White, StrokeThickness = 1.5, IsHitTestVisible = false }; Canvas.SetLeft( marcador, ponto.X - 4.5 ); Canvas.SetTop( marcador, ponto.Y - 4.5 ); EditorCutVerticesCanvas .Children .Add( marcador ); } } private void AdicionarPontoRecortePorPontos(System.Windows.Point ponto) { if ( _pontosRecortePorPontos.Count > 0 ) { var ultimo = _pontosRecortePorPontos[ _pontosRecortePorPontos.Count - 1 ]; double dx = ponto.X - ultimo.X; double dy = ponto.Y - ultimo.Y; /* * Evita vértices praticamente duplicados. */ if ( dx * dx + dy * dy < 4.0 ) { return; } } _pontosRecortePorPontos .Add( ponto ); AtualizarPreviewRecortePorPontos(); } private void FinalizarRecortePorPontos() { if ( _pontosRecortePorPontos.Count < 3 ) { return; } /* * Copiamos porque CancelarGestoEditor() * vai limpar a coleção. */ var pontos = _pontosRecortePorPontos .ToArray(); try { TentarEmitirPoligonoRecorte(pontos); } finally { CancelarGestoEditor(); } } private void Root_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { if ( ModoInteracao == MapaModoInteracao.Edicao ) { Root.Focus(); System.Windows.Input.Keyboard .Focus( Root ); } bool retangulo = EmSelecaoRetangulo; bool recorteLivre = EmRecorteLivre; bool recortePorPontos = EmRecortePorPontos; if ( !retangulo && !recorteLivre && !recortePorPontos ) { return; } /* * Ctrl + arraste pertence ao Mapsui. */ if (CtrlPressionado) { CancelarGestoEditor(); return; } /* * Gesto só nasce realmente sobre * o MapControl. */ if ( e.OriginalSource is not DependencyObject source || !EhDescendenteDe( source, Mapa ) ) { return; } Root.Focus(); System.Windows.Input.Keyboard .Focus( Root ); if (recortePorPontos) { /* * O segundo clique de um double-click * encerra o polígono. * * O primeiro clique já adicionou * o último vértice. */ if ( e.ClickCount >= 2 && _pontosRecortePorPontos.Count >= 3 ) { FinalizarRecortePorPontos(); e.Handled = true; return; } var ponto = e.GetPosition( EditorInteractionCanvas ); AdicionarPontoRecortePorPontos( ponto ); e.Handled = true; return; } /* * Mata qualquer gesto antigo. */ CancelarGestoEditor(); var inicio = e.GetPosition( EditorInteractionCanvas ); if (retangulo) { _inicioRetanguloEdicao = inicio; _arrastandoRetanguloEdicao = true; Canvas.SetLeft( EditorSelectionRectangle, inicio.X ); Canvas.SetTop( EditorSelectionRectangle, inicio.Y ); EditorSelectionRectangle.Width = 0; EditorSelectionRectangle.Height = 0; EditorSelectionRectangle.Visibility = Visibility.Visible; } else { /* * RECORTE LIVRE */ _desenhandoRecorteLivre = true; _pontosRecorteLivre .Clear(); AdicionarPontoRecorteLivre( inicio, forcar: true ); } /* * A partir daqui o gesto é nosso. */ Root.CaptureMouse(); Mapa.IsHitTestVisible = false; e.Handled = true; } private void Root_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) { System.Windows.Point atual; if ( EmRecortePorPontos && _pontosRecortePorPontos.Count > 0 && !CtrlPressionado ) { if ( e.OriginalSource is DependencyObject source && EhDescendenteDe( source, Mapa ) ) { atual = e.GetPosition( EditorInteractionCanvas ); AtualizarPreviewRecortePorPontos( atual ); } } bool existeGesto = _arrastandoRetanguloEdicao || _desenhandoRecorteLivre; if (!existeGesto) return; /* * Nunca existe drag sem o botão * fisicamente pressionado. */ if ( e.LeftButton != System.Windows.Input .MouseButtonState.Pressed ) { CancelarGestoEditor(); return; } atual = e.GetPosition( EditorInteractionCanvas ); if ( _arrastandoRetanguloEdicao ) { if (!EmSelecaoRetangulo) { CancelarGestoEditor(); return; } AtualizarPreviewRetangulo( _inicioRetanguloEdicao, atual ); e.Handled = true; return; } if ( _desenhandoRecorteLivre ) { if (!EmRecorteLivre) { CancelarGestoEditor(); return; } AdicionarPontoRecorteLivre( atual ); e.Handled = true; } } private void Root_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { if ( _arrastandoRetanguloEdicao ) { if (!EmSelecaoRetangulo) { CancelarGestoEditor(); return; } var fim = e.GetPosition( EditorInteractionCanvas ); FinalizarRetanguloEdicao( fim ); e.Handled = true; return; } if ( _desenhandoRecorteLivre ) { if (!EmRecorteLivre) { CancelarGestoEditor(); return; } var fim = e.GetPosition( EditorInteractionCanvas ); FinalizarRecorteLivre( fim ); e.Handled = true; } } private void Root_LostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e) { if ( _arrastandoRetanguloEdicao || _desenhandoRecorteLivre ) { CancelarGestoEditor(); } } private void Root_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if ( ModoInteracao != MapaModoInteracao.Edicao ) { return; } if (EmRecortePorPontos) { /* * ESC * * Descarta somente o polígono em construção. * NÃO cancela a sessão de edição. */ if ( e.Key == System.Windows.Input .Key.Escape && _pontosRecortePorPontos.Count > 0 ) { CancelarGestoEditor(); e.Handled = true; return; } /* * BACKSPACE * * Remove o último vértice. */ if ( e.Key == System.Windows.Input .Key.Back && _pontosRecortePorPontos.Count > 0 ) { _pontosRecortePorPontos .RemoveAt( _pontosRecortePorPontos.Count - 1 ); AtualizarPreviewRecortePorPontos(); e.Handled = true; return; } bool ctrlPontos = ( System.Windows.Input .Keyboard.Modifiers & System.Windows.Input .ModifierKeys.Control ) != 0; /* * ENTER simples conclui. * * Ctrl+Enter continua reservado * para "Aplicar alterações". */ if ( !ctrlPontos && ( e.Key == System.Windows.Input.Key.Enter || e.Key == System.Windows.Input.Key.Return ) && _pontosRecortePorPontos.Count >= 3 ) { FinalizarRecortePorPontos(); e.Handled = true; return; } } /* * DELETE * * Exclusão das ruas atualmente * selecionadas. */ if ( e.Key == System.Windows.Input.Key.Delete ) { MapaEditorExcluirSelecionadasSolicitado? .Invoke( this ); e.Handled = true; return; } bool ctrl = ( System.Windows.Input .Keyboard.Modifiers & System.Windows.Input .ModifierKeys.Control ) != 0; if (!ctrl) return; if ( ctrl && ( e.Key == System.Windows.Input.Key.Enter || e.Key == System.Windows.Input.Key.Return ) ) { MapaEditorAplicarEdicaoSolicitado? .Invoke( this ); e.Handled = true; return; } /* * CTRL + Z */ if ( e.Key == System.Windows.Input.Key.Z ) { MapaEditorDesfazerSolicitado? .Invoke( this ); e.Handled = true; return; } /* * CTRL + Y */ if ( e.Key == System.Windows.Input.Key.Y ) { MapaEditorRefazerSolicitado? .Invoke( this ); e.Handled = true; } } private static bool EhDescendenteDe(DependencyObject? elemento, DependencyObject ancestral) { while (elemento != null) { if ( ReferenceEquals( elemento, ancestral ) ) { return true; } elemento = System.Windows.Media .VisualTreeHelper .GetParent( elemento ); } return false; } private void GarantirFocoEditor() { if (ModoInteracao != MapaModoInteracao.Edicao) return; Root.Focus(); System.Windows.Input.Keyboard.Focus(Root); } private void EnsureBaseLayer() { try { // Remove camada base anterior (se houver) if (_baseLayer != null) { Mapa.Map.Layers.Remove(_baseLayer); _baseLayer = null; } // Tenta carregar MBTiles se existir if (!string.IsNullOrWhiteSpace(MbTilesPath) && File.Exists(MbTilesPath)) { var connStr = new SQLiteConnectionString(MbTilesPath, false); var schema = new GlobalSphericalMercator(YAxis.TMS, 0, 18); // ajuste de níveis se necessário var mb = new MbTilesTileSource(connStr, schema); _baseLayer = new TileLayer(mb) { Name = "Basemap" }; Mapa.Map.Layers.Insert(0, _baseLayer); HideOverlay(); } else { // Sem MBTiles: mostra aviso amigável //ShowOverlay("Arquivo MBTiles não encontrado. Defina 'MbTilesPath' para habilitar o mapa offline."); // Fallback online (OpenStreetMap) var osm = KnownTileSources.Create(KnownTileSource.OpenStreetMap); _baseLayer = new TileLayer(osm) { Name = "OSM (online)" }; Mapa.Map.Layers.Insert(0, _baseLayer); HideOverlay(); // some com a mensagem } } catch (Exception ex) { ShowOverlay($"Falha ao carregar MBTiles: {ex.Message}"); } } private void NavigateToView() { if (Mapa?.Map is null) return; // Converte WGS84 (lon/lat) para WebMercator var (x, y) = SphericalMercator.FromLonLat(Longitude, Latitude); // Evita navegar para valores inválidos if (double.IsNaN(x) || double.IsNaN(y)) return; var center = new MPoint(x, y); Mapa.Map?.Navigator?.CenterOnAndZoomTo(center, resolution: Scale > 0 ? Scale : 5000); } private void ShowOverlay(string message) { OverlayText.Text = message; OverlayMessage.Visibility = Visibility.Visible; } private void HideOverlay() { OverlayMessage.Visibility = Visibility.Collapsed; } /// /// API pública para ajustar a câmera via código. /// public void SetView(double? latitude, double? longitude, double? scale = null) { if (latitude.HasValue) Latitude = latitude.Value; if (longitude.HasValue) Longitude = longitude.Value; if (scale.HasValue) Scale = scale.Value; if (latitude.HasValue || longitude.HasValue) NavigateToView(); } private string? GetAttr(MIFeature? feature, string fieldName) { if (feature == null || string.IsNullOrWhiteSpace(fieldName)) return null; try { var value = feature[fieldName]; if (value == null || value is DBNull) return null; var text = Convert.ToString( value, System.Globalization.CultureInfo.InvariantCulture ); return string.IsNullOrWhiteSpace(text) ? null : text.Trim(); } catch { return null; } } } }