2682 lines
72 KiB
C#
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;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|