agrobot_base/AgroBase/OperationControl/Controls/MapViewControl.xaml.cs

2682 lines
72 KiB
C#

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<string>? MapaNovaEntidadeClicada;
public event Action<string>? MapaNovoRuaClicada;
public event Action<double, double>? MapaNovoPontoRetornoClicado;
public event Action<MapViewControl>? MapaNovoViewportAlterado;
public event Action<MapViewControl, MapaFerramentaEdicao>? MapaEditorFerramentaSelecionada;
public event Action<MapViewControl>? MapaEditorLimparSelecaoSolicitado;
public event Action<MapViewControl, MapaRetanguloSelecao>? MapaEditorRetanguloSelecionado;
public event Action<MapViewControl>? MapaEditorDesfazerSolicitado;
public event Action<MapViewControl>? MapaEditorRefazerSolicitado;
public event Action<MapViewControl>? MapaEditorExcluirSelecionadasSolicitado;
public event Action<MapViewControl>? MapaEditorAplicarEdicaoSolicitado;
public event Action<MapViewControl>? MapaEditorCancelarEdicaoSolicitado;
public event Action<MapViewControl, MapaPoligonoRecorte>? MapaEditorPoligonoRecorteDefinido;
public event Action<MapViewControl, MapaEscopoRecorte>? MapaEditorEscopoRecorteAlterado;
public event Action<MapViewControl, MapaAcaoBordaGnss>? MapaEditorBordaGnssAcaoSolicitada;
public event Action<MapViewControl>? MapaEditorRestaurarFonteSolicitado;
private bool _arrastandoRetanguloEdicao;
private System.Windows.Point _inicioRetanguloEdicao;
private bool _desenhandoRecorteLivre;
private readonly List<System.Windows.Point> _pontosRecorteLivre = new();
private readonly List<System.Windows.Point> _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<ILayer> ObterCamadasInterativas()
{
if (Mapa?.Map?.Layers == null)
return Array.Empty<ILayer>();
/*
* 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<ILayer>()
};
}
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<BaseLayerOption> _baseOptions;
public string MbTilesPath
{
get => (string)GetValue(MbTilesPathProperty);
set => SetValue(MbTilesPathProperty, value);
}
private class BaseLayerOption
{
public string Name { get; set; } = "";
public Func<ILayer> CreateLayer { get; set; } = default!;
public override string ToString() => Name;
}
private void InitBaseMapOptions()
{
_baseOptions = new List<BaseLayerOption>
{
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<GPSModel> _pontosSelecionados = new List<GPSModel>();
private void RedesenharPontosSelecionados()
{
var features = new List<Mapsui.IFeature>();
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<GPSModel>();
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<System.Windows.Point> pontosTela)
{
if (
pontosTela == null ||
pontosTela.Count < 3
)
{
return false;
}
double areaTela =
CalcularAreaPoligonoTela(
pontosTela
);
if (
areaTela <
AreaMinimaRecortePixelsQuadrados
)
{
return false;
}
var pontosWgs84 =
new List<MapaPontoWgs84>();
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<System.Windows.Point> 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<System.Windows.Point>(
_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;
}
/// <summary>
/// API pública para ajustar a câmera via código.
/// </summary>
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;
}
}
}
}