2089 lines
74 KiB
C#
2089 lines
74 KiB
C#
using System.IO;
|
|
using System.Windows;
|
|
using BruTile;
|
|
using BruTile.MbTiles;
|
|
using BruTile.Predefined;
|
|
using SQLite;
|
|
|
|
using NetTopologySuite.IO;
|
|
using NetTopologySuite.Features;
|
|
using NetTopologySuite.Geometries;
|
|
|
|
using Mapsui;
|
|
using Mapsui.Layers;
|
|
using Mapsui.Providers;
|
|
using Mapsui.Styles;
|
|
using Mapsui.Projections;
|
|
using Mapsui.Tiling.Layers;
|
|
using Mapsui.Nts;
|
|
using Mapsui.UI.Wpf;
|
|
|
|
// Aliases para evitar conflito com NetTopologySuite.* (NTS)
|
|
using UserControl = System.Windows.Controls.UserControl;
|
|
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
|
|
using MIFeature = Mapsui.IFeature;
|
|
using Brush = Mapsui.Styles.Brush;
|
|
using Pen = Mapsui.Styles.Pen;
|
|
using Color = Mapsui.Styles.Color;
|
|
using Font = Mapsui.Styles.Font;
|
|
using NFeature = NetTopologySuite.Features.Feature;
|
|
using Mapsui.Manipulations;
|
|
using Mapsui.Extensions;
|
|
using OperationControl.Models;
|
|
using static AgroBase.Models.Enums;
|
|
using System.Windows.Input;
|
|
using AgroBase.Models;
|
|
|
|
|
|
namespace OperationControl.Controls
|
|
{
|
|
public partial class MapViewControl : UserControl
|
|
{
|
|
public MapViewControl()
|
|
{
|
|
InitializeComponent();
|
|
Loaded += OnLoaded;
|
|
markers = new MapMarkerManager(Mapa);
|
|
RuasMapaCarregado = new List<StreetMapDictionaryModel>();
|
|
HookHitTesting();
|
|
}
|
|
|
|
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 TipoMapaOperacao TipoMapaSelecionado;
|
|
public string PathMapaSelecionado;
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
public static readonly DependencyProperty MarkerClickedCommandProperty =
|
|
DependencyProperty.Register(
|
|
nameof(MarkerClickedCommand),
|
|
typeof(ICommand),
|
|
typeof(MapViewControl),
|
|
new PropertyMetadata(null));
|
|
|
|
public ICommand MarkerClickedCommand
|
|
{
|
|
get => (ICommand)GetValue(MarkerClickedCommandProperty);
|
|
set => SetValue(MarkerClickedCommandProperty, value);
|
|
}
|
|
|
|
public static readonly DependencyProperty StreetMapClickedCommandProperty =
|
|
DependencyProperty.Register(
|
|
nameof(StreetMapClickedCommand),
|
|
typeof(ICommand),
|
|
typeof(MapViewControl),
|
|
new PropertyMetadata(null));
|
|
|
|
public ICommand StreetMapClickedCommand
|
|
{
|
|
get => (ICommand)GetValue(StreetMapClickedCommandProperty);
|
|
set => SetValue(StreetMapClickedCommandProperty, value);
|
|
}
|
|
|
|
|
|
#region MARCADORES
|
|
|
|
public MapMarkerManager markers;
|
|
public event EventHandler<MapMarkerManager.MarkerClickedEventArgs>? MarkerClicked;
|
|
private bool SemRoverFocado => VariaveisControleOperacao.RoverEmFoco == null;
|
|
protected virtual void OnMarkerClicked(string markerId)
|
|
{
|
|
if (markerId == null) markers?.ClearMarkerFocused();
|
|
else markers?.SetMarkerFocused(markerId, true);
|
|
MarkerClicked?.Invoke(this, new MapMarkerManager.MarkerClickedEventArgs(markerId));
|
|
}
|
|
|
|
private void OnMarkerClickedInternal(MapMarkerManager.MarkerClickedEventArgs e)
|
|
{
|
|
MarkerClicked?.Invoke(this, e);
|
|
|
|
if (MarkerClickedCommand?.CanExecute(e) == true)
|
|
MarkerClickedCommand.Execute(e);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RUAS PLANTACAO
|
|
|
|
private Layer MapaPlantacao_Layer;
|
|
public List<StreetMapDictionaryModel> RuasMapaCarregado;
|
|
|
|
public event EventHandler<StreetMapClickedEventArgs>? StreetMapClicked;
|
|
private bool ParametrizandoOperacao =>
|
|
(
|
|
(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Modo ?? ModoOperacao.NaoDefinido) == ModoOperacao.MapaGPS ||
|
|
(VariaveisControleOperacao.RoverEmFoco?.Modo ?? ModoOperacao.NaoDefinido) == ModoOperacao.MapaGPS
|
|
) &&
|
|
!(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Iniciada ?? false) &&
|
|
(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Status ?? StatusOperacao.NaoIniciado) == StatusOperacao.Parametrizando &&
|
|
((App)System.Windows.Application.Current).Shell.Dock?._vm?._viewOperacaoRight?._vm?.SecaoAtual == ViewModels.Views.Operacao.SecaoRightBar.Parametrizacao;
|
|
protected virtual void OnStreetMapClicked(string streetId)
|
|
{
|
|
StreetMapClicked?.Invoke(this, new StreetMapClickedEventArgs(streetId, RuasMapaCarregado.Where(x => x.Selected).Select(x => x.Id).ToList()));
|
|
}
|
|
|
|
private void OnStreetMapClickedInternal(StreetMapClickedEventArgs e)
|
|
{
|
|
StreetMapClicked?.Invoke(this, e);
|
|
|
|
if (StreetMapClickedCommand?.CanExecute(e) == true)
|
|
StreetMapClickedCommand.Execute(e);
|
|
}
|
|
|
|
private void HighlightRoad(MIFeature? feature = null, string? id = null)
|
|
{
|
|
if (string.IsNullOrEmpty(id))
|
|
id = GetAttr(feature, "Id");
|
|
|
|
var rua = RuasMapaCarregado.FirstOrDefault(x => x.Id == id);
|
|
if (rua == null)
|
|
return;
|
|
|
|
if (!rua.Selected)
|
|
{
|
|
// Seleciona no final da fila
|
|
int proximaOrdem = RuasMapaCarregado
|
|
.Where(x => x.Selected && x.OrderSelection.HasValue)
|
|
.Select(x => x.OrderSelection!.Value)
|
|
.DefaultIfEmpty(0)
|
|
.Max() + 1;
|
|
|
|
rua.Selected = true;
|
|
rua.Status = StreetMapStyle.Selected;
|
|
rua.OrderSelection = proximaOrdem;
|
|
}
|
|
else
|
|
{
|
|
// Guarda a ordem antiga antes de remover
|
|
int ordemRemovida = rua.OrderSelection ?? 0;
|
|
|
|
rua.Selected = false;
|
|
rua.Status = StreetMapStyle.Normal;
|
|
rua.OrderSelection = null;
|
|
|
|
// Fecha o buraco: todo mundo depois sobe uma posição
|
|
foreach (var r in RuasMapaCarregado
|
|
.Where(x => x.Selected && x.OrderSelection.HasValue && x.OrderSelection.Value > ordemRemovida))
|
|
{
|
|
r.OrderSelection--;
|
|
}
|
|
}
|
|
|
|
rua.Feature.Styles.Clear();
|
|
rua.Feature.Styles.Add(CreateStreetStyle(rua.Status));
|
|
|
|
Mapa.Refresh();
|
|
|
|
OnStreetMapClicked(id);
|
|
}
|
|
|
|
private void AtualizarStylesRuas()
|
|
{
|
|
foreach (var rua in RuasMapaCarregado)
|
|
{
|
|
rua.Status = (rua.Selected) ? StreetMapStyle.Selected : StreetMapStyle.Normal;
|
|
rua.Feature.Styles.Clear();
|
|
rua.Feature.Styles.Add(CreateStreetStyle(rua.Status));
|
|
}
|
|
|
|
Mapa.Refresh();
|
|
}
|
|
|
|
public enum StreetMapStyle
|
|
{
|
|
Normal,
|
|
Selected,
|
|
Running,
|
|
Done
|
|
}
|
|
|
|
public class StreetMapDictionaryModel
|
|
{
|
|
public string Id { get; set; }
|
|
public MIFeature Feature { get; set; }
|
|
public bool Selected { get; set; }
|
|
public StreetMapStyle Status { get; set; }
|
|
public int? OrderSelection { get; set; }
|
|
}
|
|
|
|
public class StreetMapClickedEventArgs : EventArgs
|
|
{
|
|
public string StreetId { get; }
|
|
public List<string> SelectedStreetsIds { get; }
|
|
|
|
public StreetMapClickedEventArgs(string streetId, List<string> selectedStreets)
|
|
{
|
|
StreetId = streetId;
|
|
SelectedStreetsIds = new List<string>(selectedStreets);
|
|
}
|
|
}
|
|
|
|
private static VectorStyle CreateStreetStyle(StreetMapStyle style)
|
|
{
|
|
switch (style)
|
|
{
|
|
case StreetMapStyle.Selected:
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Color.FromArgb(255, 241, 196, 15), 4), // amarelo mais grosso
|
|
Fill = null
|
|
};
|
|
|
|
case StreetMapStyle.Running:
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Color.FromArgb(255, 52, 152, 219), 4), // azul
|
|
Fill = null
|
|
};
|
|
|
|
case StreetMapStyle.Done:
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Color.FromArgb(255, 39, 174, 96), 3), // verde
|
|
Fill = null
|
|
};
|
|
|
|
case StreetMapStyle.Normal:
|
|
default:
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Color.FromArgb(255, 46, 204, 113), 2), // seu padrão do GeoJson
|
|
Fill = new Brush(Color.FromArgb(60, 46, 204, 113))
|
|
};
|
|
}
|
|
}
|
|
|
|
public void AtualizarStatusRua(string id, StreetMapStyle status)
|
|
{
|
|
var rua = RuasMapaCarregado.FirstOrDefault(x => x.Id == id);
|
|
if (rua == null) return;
|
|
|
|
rua.Status = status;
|
|
ApplyStreetStyles();
|
|
}
|
|
|
|
private void ApplyStreetStyles()
|
|
{
|
|
if (RuasMapaCarregado == null) return;
|
|
|
|
foreach (var rua in RuasMapaCarregado)
|
|
{
|
|
rua.Feature.Styles.Clear();
|
|
rua.Feature.Styles.Add(CreateStreetStyle(rua.Status));
|
|
}
|
|
|
|
Mapa.Refresh();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#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 */ }
|
|
}
|
|
|
|
// 5) garantir marcadores por cima
|
|
try { markers?.BringMarkersToFront(); } catch { }
|
|
|
|
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();
|
|
|
|
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()
|
|
{
|
|
// Clique: seleciona (foco)
|
|
Mapa.MouseLeftButtonUp += (s, e) =>
|
|
{
|
|
var pos = e.GetPosition(Mapa);
|
|
var sp = new ScreenPosition(pos.X, pos.Y);
|
|
|
|
// algumas versões têm sobrecarga com tolerância (px). Tente esta:
|
|
var info = Mapa.GetMapInfo(sp, Mapa.Map.Layers /*, 18*/);
|
|
var feat = info?.Feature;
|
|
|
|
if (info?.Layer == markers.Layer && feat != null && markers != null && markers.TryGetIdFromFeature(feat, out var id))
|
|
{
|
|
if (SemRoverFocado)
|
|
OnMarkerClicked(id);
|
|
}
|
|
else if (info?.Layer == MapaPlantacao_Layer && feat is MIFeature roadFeature)
|
|
{
|
|
if (ParametrizandoOperacao)
|
|
HighlightRoad(roadFeature);
|
|
}
|
|
else if (EmModoRetorno)
|
|
{
|
|
AlternarPontoNoMapa(sp);
|
|
}
|
|
else
|
|
{
|
|
OnMarkerClicked(null);
|
|
}
|
|
|
|
};
|
|
|
|
// Hover: cursor de mão (mantém como já fez)
|
|
Mapa.MouseMove += (s, e) =>
|
|
{
|
|
try
|
|
{
|
|
var pos = e.GetPosition(Mapa);
|
|
var sp = new ScreenPosition(pos.X, pos.Y);
|
|
var info = Mapa.GetMapInfo(sp, Mapa.Map.Layers /*, 18*/);
|
|
var feat = info?.Feature;
|
|
|
|
if (feat != null && markers != null && markers.TryGetIdFromFeature(feat, out _) && SemRoverFocado)
|
|
Mapa.Cursor = System.Windows.Input.Cursors.Hand;
|
|
else if (feat != null && info?.Layer == MapaPlantacao_Layer && feat is MIFeature roadFeature && ParametrizandoOperacao)
|
|
Mapa.Cursor = System.Windows.Input.Cursors.Hand;
|
|
else if (feat != null && info?.Layer == _layerPontosSelecionados && feat is PointFeature pointFeature && EmModoRetorno)
|
|
Mapa.Cursor = System.Windows.Input.Cursors.Hand;
|
|
else
|
|
Mapa.Cursor = System.Windows.Input.Cursors.Arrow;
|
|
}
|
|
catch
|
|
{
|
|
Mapa.Cursor = System.Windows.Input.Cursors.Arrow;
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
#region RETORNO A BASE MANUAL
|
|
|
|
private MemoryLayer _layerPontosSelecionados;
|
|
public List<GPSModel> _pontosSelecionados = new List<GPSModel>();
|
|
double ToleranciaRemocaoMetros = 2.0;
|
|
private bool EmModoRetorno =>
|
|
!SemRoverFocado &&
|
|
!(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Iniciada ?? false) &&
|
|
(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Status ?? StatusOperacao.NaoIniciado) == StatusOperacao.Parametrizando &&
|
|
Models.Variaveis.Dock?._vm?._viewOperacaoRight?._vm?.SecaoAtual == ViewModels.Views.Operacao.SecaoRightBar.Parametrizacao &&
|
|
Models.Variaveis.Dock?._vm?._viewOperacaoRight?.areaParametrizacao?._vm?.Parametros?.Modo == ModoOperacao.RetornoBase;
|
|
|
|
private void AlternarPontoNoMapa(ScreenPosition sp)
|
|
{
|
|
try
|
|
{
|
|
// Dependendo da versão do Mapsui, pode ser:
|
|
// var world = Mapa.Navigator.Viewport.ScreenToWorld(sp.X, sp.Y);
|
|
// ou:
|
|
var world = Mapa.Map.Navigator.Viewport.ScreenToWorld(sp.X, sp.Y);
|
|
|
|
// Normalmente o mapa está em EPSG:3857
|
|
var lonLat = Mapsui.Projections.SphericalMercator.ToLonLat(world.X, world.Y);
|
|
|
|
GPSModel ponto = new GPSModel() { Latitude = lonLat.lat, Longitude = lonLat.lon };
|
|
|
|
var existente = _pontosSelecionados
|
|
.FirstOrDefault(p => GPSUtils.DistanciaEntrePontos(ponto, p) <= ToleranciaRemocaoMetros);
|
|
|
|
if (existente != null)
|
|
{
|
|
_pontosSelecionados.Remove(existente);
|
|
}
|
|
else
|
|
{
|
|
_pontosSelecionados.Add(ponto);
|
|
}
|
|
|
|
RedesenharPontosSelecionados();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"Erro ao alternar ponto no mapa: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
Models.Variaveis.MostrarLog($"Pontos: {_pontosSelecionados.Count}");
|
|
}
|
|
}
|
|
|
|
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 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();
|
|
}
|
|
|
|
#region CARREGAR ARQUIVO DE MAPA
|
|
|
|
private void OnLoadMapClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new OpenFileDialog
|
|
{
|
|
Filter = "Arquivos de mapa|*.geojson;*.json;*.shp",
|
|
Title = "Selecione o arquivo de mapa"
|
|
};
|
|
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
PathMapaSelecionado = dlg.FileName;
|
|
if (PathMapaSelecionado.EndsWith(".shp", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var geojson = ConvertShpToGeoJson(PathMapaSelecionado);
|
|
AddGeoJsonLayer(geojson);
|
|
}
|
|
else
|
|
{
|
|
var geojson = File.ReadAllText(PathMapaSelecionado);
|
|
AddGeoJsonLayer(geojson);
|
|
}
|
|
}
|
|
}
|
|
|
|
private string ConvertShpToGeoJson(string shpPath)
|
|
{
|
|
// SRID 4326 por padrão (GeoJSON usa WGS84 lon/lat). Se o .prj for outro,
|
|
// a gente reprojeta depois ao desenhar no Mapsui (já fizemos com ProjectingProvider).
|
|
var gf = new GeometryFactory(new PrecisionModel(), 4326);
|
|
var features = new FeatureCollection();
|
|
|
|
using var reader = new ShapefileDataReader(shpPath, gf);
|
|
var header = reader.DbaseHeader;
|
|
|
|
while (reader.Read())
|
|
{
|
|
// Geometria vem daqui (NTS)
|
|
var geom = reader.Geometry;
|
|
if (geom == null) continue;
|
|
|
|
var f = new NFeature(geom, new AttributesTable());
|
|
|
|
// Campos do DBF (pular a coluna de geometria -> usar i+1)
|
|
for (int i = 0; i < header.NumFields; i++)
|
|
{
|
|
var fld = header.Fields[i];
|
|
object? val = null;
|
|
|
|
try
|
|
{
|
|
// A maioria das builds coloca a geometria em ordinal 0 (por isso i+1)
|
|
val = reader.GetValue(i + 1);
|
|
}
|
|
catch
|
|
{
|
|
// fallback caso a implementação local não tenha a coluna 0 como geometria
|
|
val = reader.GetValue(i);
|
|
}
|
|
|
|
if (val == null || val is DBNull) continue;
|
|
|
|
// Opcional: normalizar strings/numéricos
|
|
f.Attributes.Add(fld.Name, val);
|
|
}
|
|
|
|
features.Add(f);
|
|
}
|
|
|
|
var writer = new GeoJsonWriter(); // << evita loop de referência
|
|
string txt = writer.Write(features); // 'features' é o FeatureCollection
|
|
return txt;
|
|
}
|
|
|
|
private void AddGeoJsonLayer(string geojson)
|
|
{
|
|
var reader = new GeoJsonReader();
|
|
var fc = reader.Read<FeatureCollection>(geojson);
|
|
|
|
RuasMapaCarregado = new List<StreetMapDictionaryModel>();
|
|
|
|
var idsUsados = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
int idsCorrigidos = 0;
|
|
int total = 0;
|
|
|
|
foreach (var f in fc)
|
|
{
|
|
if (f.Geometry == null)
|
|
continue;
|
|
|
|
total++;
|
|
|
|
var gf = new GeometryFeature
|
|
{
|
|
Geometry = f.Geometry
|
|
};
|
|
|
|
// copia atributos
|
|
foreach (var name in f.Attributes.GetNames())
|
|
gf[name] = f.Attributes[name];
|
|
|
|
bool foiGeradoAutomaticamente;
|
|
string idFinal = ResolveUniqueStreetId(gf, RuasMapaCarregado.Count, idsUsados, out foiGeradoAutomaticamente);
|
|
|
|
// guarda o original, se quiser rastrear/debugar
|
|
var idOriginal = GetAttr(gf, "Id") ??
|
|
GetAttr(gf, "ID") ??
|
|
GetAttr(gf, "id");
|
|
|
|
gf["Id"] = idFinal;
|
|
|
|
if (!string.IsNullOrWhiteSpace(idOriginal))
|
|
gf["IdOriginal"] = idOriginal;
|
|
|
|
if (foiGeradoAutomaticamente)
|
|
idsCorrigidos++;
|
|
|
|
RuasMapaCarregado.Add(new StreetMapDictionaryModel()
|
|
{
|
|
Id = idFinal,
|
|
Feature = gf,
|
|
Selected = false,
|
|
Status = StreetMapStyle.Normal
|
|
});
|
|
}
|
|
|
|
if (idsCorrigidos > 0)
|
|
{
|
|
Models.Variaveis.MostrarLog(
|
|
$"Mapa carregado com correção automática de IDs. " +
|
|
$"Total de ruas: {RuasMapaCarregado.Count}. " +
|
|
$"IDs corrigidos/gerados: {idsCorrigidos}.");
|
|
}
|
|
|
|
var mem = new MemoryProvider(RuasMapaCarregado.Select(x => x.Feature))
|
|
{
|
|
CRS = "EPSG:4326"
|
|
};
|
|
|
|
var projecting = new ProjectingProvider(mem)
|
|
{
|
|
CRS = "EPSG:3857"
|
|
};
|
|
|
|
if (Mapa.Map.Layers.Any(x => x.Name == MapaPlantacao_Layer?.Name))
|
|
Mapa.Map.Layers.Remove(MapaPlantacao_Layer);
|
|
|
|
MapaPlantacao_Layer = new Layer("GeoJson")
|
|
{
|
|
DataSource = projecting,
|
|
Style = new VectorStyle
|
|
{
|
|
Line = new Pen(Color.FromArgb(255, 46, 204, 113), 2),
|
|
Fill = new Brush(Color.FromArgb(60, 46, 204, 113))
|
|
}
|
|
};
|
|
|
|
Mapa.Map.Layers.Add(MapaPlantacao_Layer);
|
|
|
|
markers.BringMarkersToFront();
|
|
Mapa.Refresh();
|
|
|
|
try
|
|
{
|
|
double _long = ((dynamic)RuasMapaCarregado[0].Feature).Geometry.Coordinate.X;
|
|
double _lat = ((dynamic)RuasMapaCarregado[0].Feature).Geometry.Coordinate.Y;
|
|
SetView(_lat, _long, 1);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Models.Variaveis.MostrarLog($"Erro ao carregar coordenadas do mapa: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private string ResolveUniqueStreetId(GeometryFeature feature, int index, HashSet<string> idsUsados, out bool foiGeradoAutomaticamente)
|
|
{
|
|
foiGeradoAutomaticamente = false;
|
|
|
|
// tenta achar algum campo comum de ID
|
|
string? idOriginal =
|
|
GetAttr(feature, "Id") ??
|
|
GetAttr(feature, "ID") ??
|
|
GetAttr(feature, "id") ??
|
|
GetAttr(feature, "OBJECTID") ??
|
|
GetAttr(feature, "ObjectID") ??
|
|
GetAttr(feature, "FID");
|
|
|
|
idOriginal = idOriginal?.Trim();
|
|
|
|
// caso esteja vazio/null
|
|
if (string.IsNullOrWhiteSpace(idOriginal))
|
|
{
|
|
string novoId = $"AUTO_{index + 1:D4}";
|
|
idsUsados.Add(novoId);
|
|
foiGeradoAutomaticamente = true;
|
|
return novoId;
|
|
}
|
|
|
|
// se ainda não foi usado, aceita
|
|
if (!idsUsados.Contains(idOriginal))
|
|
{
|
|
idsUsados.Add(idOriginal);
|
|
return idOriginal;
|
|
}
|
|
|
|
// se repetiu, mantém rastreabilidade mas torna único
|
|
int sufixo = 2;
|
|
string candidato;
|
|
do
|
|
{
|
|
candidato = $"{idOriginal}_{sufixo:D2}";
|
|
sufixo++;
|
|
}
|
|
while (idsUsados.Contains(candidato));
|
|
|
|
idsUsados.Add(candidato);
|
|
foiGeradoAutomaticamente = true;
|
|
return candidato;
|
|
}
|
|
|
|
private void OnCenterAreaClicked(object sender, RoutedEventArgs e)
|
|
{
|
|
NavigateToView();
|
|
}
|
|
|
|
|
|
public void LoadMapFile(string? path, TipoMapaOperacao tipo = TipoMapaOperacao.RuasPlantacao)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path) || path == PathMapaSelecionado)
|
|
return;
|
|
|
|
TipoMapaSelecionado = tipo;
|
|
PathMapaSelecionado = path;
|
|
|
|
if (PathMapaSelecionado.EndsWith(".shp", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var geojson = ConvertShpToGeoJson(PathMapaSelecionado);
|
|
AddGeoJsonLayer(geojson);
|
|
}
|
|
else
|
|
{
|
|
var geojson = File.ReadAllText(PathMapaSelecionado);
|
|
AddGeoJsonLayer(geojson);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
private Dictionary<string, string> ParseFeatureAttributes(MIFeature feature)
|
|
{
|
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (feature == null)
|
|
return dict;
|
|
|
|
var text = feature.ToStringOfKeyValuePairs();
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
return dict;
|
|
|
|
var lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
var parts = line.Split(new[] { ':' }, 2); // divide em "Nome" e "valor"
|
|
if (parts.Length == 2)
|
|
{
|
|
var key = parts[0].Trim();
|
|
var value = parts[1].Trim();
|
|
if (!string.IsNullOrEmpty(key))
|
|
dict[key] = value;
|
|
}
|
|
}
|
|
|
|
return dict;
|
|
}
|
|
|
|
private string? GetAttr(MIFeature feature, string fieldName)
|
|
{
|
|
var dict = ParseFeatureAttributes(feature);
|
|
return dict.TryGetValue(fieldName, out var v) ? v : null;
|
|
}
|
|
|
|
public AgroBase.Models.MapaFeatureCollectionModel CriarDadosMapa(bool selecionadas = false)
|
|
{
|
|
AgroBase.Models.MapaFeatureCollectionModel _mapa = new AgroBase.Models.MapaFeatureCollectionModel() { type = "FeatureCollection", features = new List<AgroBase.Models.MapaFeatureModel>() };
|
|
foreach (var x in RuasMapaCarregado.Where(x => selecionadas ? x.Selected : true))
|
|
{
|
|
string id = ((dynamic)x.Feature).Id.ToString();
|
|
string type = ((dynamic)x.Feature).Geometry.GeometryType;
|
|
string s = ((dynamic)x.Feature).Geometry.CoordinateSequence.ToString();
|
|
s = s.Trim().TrimStart('(').TrimEnd(')');
|
|
var pairs = s.Split("),", StringSplitOptions.RemoveEmptyEntries);
|
|
var coordinates = new List<List<double>>();
|
|
foreach (var p in pairs)
|
|
{
|
|
var clean = p.Replace("(", "").Replace(")", "").Trim();
|
|
var parts = clean.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length == 2 && double.TryParse(parts[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lon) && double.TryParse(parts[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lat))
|
|
{
|
|
coordinates.Add(new List<double> { lon, lat });
|
|
}
|
|
}
|
|
|
|
AgroBase.Models.MapaFeatureGeometryModel geometry = new AgroBase.Models.MapaFeatureGeometryModel()
|
|
{
|
|
id = GetAttr(x.Feature, "Id"),
|
|
type = type,
|
|
coordinates = coordinates
|
|
};
|
|
|
|
_mapa.features.Add(new AgroBase.Models.MapaFeatureModel()
|
|
{
|
|
type = "Feature",
|
|
geometry = geometry,
|
|
properties = new AgroBase.Models.MapaFeaturePropertiesModel()
|
|
{
|
|
Id = GetAttr(x.Feature, "Id"),
|
|
Dist1 = double.Parse(GetAttr(x.Feature, "Dist1")),
|
|
Dist2 = double.Parse(GetAttr(x.Feature, "Dist2")),
|
|
Length = double.Parse(GetAttr(x.Feature, "Length")),
|
|
Name = GetAttr(x.Feature, "Name")
|
|
}
|
|
});
|
|
};
|
|
|
|
return _mapa;
|
|
}
|
|
|
|
public void CarregarDadosMapa(AgroBase.Models.MapaFeatureCollectionModel Mapa, List<string> RuasSelecionadas, List<double[]> pontosSelecionados)
|
|
{
|
|
if (RuasMapaCarregado.Any())
|
|
{
|
|
foreach (var rua in RuasMapaCarregado)
|
|
{
|
|
var dado = Mapa?.features?.FirstOrDefault(x => x.properties.Id == rua.Id);
|
|
if (dado == null) continue;
|
|
rua.Selected = RuasSelecionadas.Contains(dado.properties.Id);
|
|
rua.OrderSelection = RuasMapaCarregado.IndexOf(rua) + 1;
|
|
}
|
|
|
|
AtualizarStylesRuas();
|
|
}
|
|
_pontosSelecionados = (pontosSelecionados ?? new List<double[]>()).Select(x => new GPSModel() { Latitude = x[0], Longitude = x[1] }).ToList();
|
|
RedesenharPontosSelecionados();
|
|
}
|
|
|
|
|
|
public void LimparRastrosRover(string? roverId, bool limparPredicao = true)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(roverId))
|
|
return;
|
|
|
|
markers?.ClearMarkerTrail(roverId, limparPredicao);
|
|
}
|
|
}
|
|
|
|
public class MapMarkerManager
|
|
{
|
|
public MapMarkerManager(MapControl map, string layerName = "Markers")
|
|
{
|
|
_map = map ?? throw new ArgumentNullException(nameof(map));
|
|
_layerName = layerName;
|
|
|
|
_provider = new MemoryProvider(_features.SelectMany(x => x.Features));
|
|
_layer = new Layer
|
|
{
|
|
Name = _layerName,
|
|
DataSource = _provider,
|
|
Style = null,
|
|
};
|
|
|
|
_map.Map!.Layers.Add(_layer);
|
|
}
|
|
|
|
private readonly MapControl _map;
|
|
private readonly string _layerName;
|
|
|
|
private readonly List<MapMarkerDictionaryModel> _features = new();
|
|
private readonly MemoryProvider _provider;
|
|
private readonly Layer _layer;
|
|
public Layer Layer { get { return _layer; } }
|
|
|
|
public enum MarkerStatus
|
|
{
|
|
Normal,
|
|
Warning,
|
|
Error,
|
|
Offline
|
|
}
|
|
|
|
|
|
|
|
public bool Added(string id)
|
|
{
|
|
return _features.Any(x => x.Id == id);
|
|
}
|
|
|
|
public bool TryGetIdFromFeature(MIFeature feature, out string id)
|
|
{
|
|
//var f = _features.FirstOrDefault(x => x.Features.Contains(feature));
|
|
var f = _features.FirstOrDefault(x => x.Features.Contains(feature) || x.MarkerShapes.Contains(feature));
|
|
if (f != null)
|
|
{
|
|
id = f.Id;
|
|
return true;
|
|
}
|
|
id = null;
|
|
return false;
|
|
}
|
|
|
|
public void SetMarkerFocused(string id, bool focused)
|
|
{
|
|
ClearMarkerFocused();
|
|
|
|
var f = _features.FirstOrDefault(x => x.Id == id);
|
|
if (f != null)
|
|
{
|
|
f.Focused = focused;
|
|
f.UpdateData();
|
|
}
|
|
|
|
UpdateFeatures();
|
|
}
|
|
|
|
public void ClearMarkerFocused()
|
|
{
|
|
foreach (var f in _features)
|
|
{
|
|
if (f.Focused)
|
|
{
|
|
f.Focused = false;
|
|
f.UpdateData();
|
|
}
|
|
}
|
|
|
|
UpdateFeatures();
|
|
}
|
|
|
|
private void UpdateFeatures()
|
|
{
|
|
_layer.DataSource = new MemoryProvider(_features.SelectMany(x => x.Features));
|
|
_layer.DataHasChanged();
|
|
_map.Refresh();
|
|
}
|
|
|
|
public void BringMarkersToFront()
|
|
{
|
|
var layers = _map.Map?.Layers;
|
|
if (layers == null) return;
|
|
|
|
// remove e re-adiciona a camada de marcadores no final da lista
|
|
if (layers.Contains(_layer))
|
|
{
|
|
layers.Remove(_layer);
|
|
layers.Add(_layer);
|
|
_map.Refresh();
|
|
}
|
|
}
|
|
|
|
public void AddMarker(
|
|
string id,
|
|
bool isBase,
|
|
double? lat = null,
|
|
double? lon = null,
|
|
double? heading = null,
|
|
System.Windows.Media.Color? color = null,
|
|
string? label = null,
|
|
StatusOperacao? status = null,
|
|
bool? pulv = null,
|
|
bool center = false,
|
|
double? rawGnssLat = null,
|
|
double? rawGnssLon = null
|
|
)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id)) return;
|
|
|
|
string lbl = label ?? id;
|
|
|
|
var marker = _features.FirstOrDefault(x => x.Id == id);
|
|
if (marker != null)
|
|
{
|
|
marker.UpdateData(
|
|
lat: lat,
|
|
lon: lon,
|
|
heading: heading,
|
|
label: label,
|
|
color: color,
|
|
status: status,
|
|
pulverizando: pulv,
|
|
rawGnssLat: rawGnssLat,
|
|
rawGnssLon: rawGnssLon
|
|
);
|
|
}
|
|
else
|
|
{
|
|
marker = new MapMarkerDictionaryModel()
|
|
{
|
|
Id = id,
|
|
Features = new List<MIFeature>(),
|
|
Color = color ?? GetColorForMarker(id, isBase),
|
|
Heading = heading ?? 0,
|
|
Lat = lat ?? 0,
|
|
Lon = lon ?? 0,
|
|
RawGnssLat = rawGnssLat,
|
|
RawGnssLon = rawGnssLon,
|
|
Label = lbl,
|
|
MarkerShapes = new List<MIFeature>(),
|
|
IsBase = isBase,
|
|
Status = status ?? StatusOperacao.NaoIniciado,
|
|
Pulverizando = pulv ?? false,
|
|
Focused = false
|
|
};
|
|
marker.CreateTrajectory(_map);
|
|
marker.UpdateData();
|
|
_features.Add(marker);
|
|
}
|
|
|
|
UpdateFeatures();
|
|
|
|
if (center)
|
|
_map.Map?.Navigator?.CenterOn(marker.Point.Point);
|
|
}
|
|
|
|
public void UpdateMarkerPosition(
|
|
string id,
|
|
double? lat = null,
|
|
double? lon = null,
|
|
double? heading = null,
|
|
List<double[]>? predict = null,
|
|
double? rawGnssLat = null,
|
|
double? rawGnssLon = null
|
|
)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id)) return;
|
|
|
|
var marker = _features.FirstOrDefault(x => x.Id == id);
|
|
if (marker == null) return;
|
|
|
|
marker.UpdateData(
|
|
lat: lat,
|
|
lon: lon,
|
|
heading: heading,
|
|
predict: predict,
|
|
rawGnssLat: rawGnssLat,
|
|
rawGnssLon: rawGnssLon
|
|
);
|
|
|
|
UpdateFeatures();
|
|
}
|
|
|
|
public void UpdateMarkerInfo(string id, StatusOperacao? status = null, bool? pulv = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id)) return;
|
|
|
|
var marker = _features.FirstOrDefault(x => x.Id == id);
|
|
if (marker == null) return;
|
|
|
|
marker.UpdateData(status: status, pulverizando: pulv);
|
|
|
|
UpdateFeatures();
|
|
}
|
|
|
|
public void RemoveMarker(string id)
|
|
{
|
|
var marker = _features.FirstOrDefault(x => x.Id == id);
|
|
if (marker == null) return;
|
|
|
|
_features.Remove(marker);
|
|
UpdateFeatures();
|
|
}
|
|
|
|
|
|
public void ClearMarkerTrail(string id, bool clearPrediction = true)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id)) return;
|
|
|
|
var marker = _features.FirstOrDefault(x => x.Id == id);
|
|
if (marker == null) return;
|
|
|
|
marker.ClearTrajectory(clearPrediction);
|
|
_map.Refresh();
|
|
}
|
|
|
|
|
|
public class MapMarkerDictionaryModel
|
|
{
|
|
public void CreateTrajectory(MapControl _map)
|
|
{
|
|
_mapParent = _map;
|
|
_trajProvider = new MemoryProvider(_trajFeatures);
|
|
_trajLayer = new Layer
|
|
{
|
|
Name = $"traj_{Id}",
|
|
DataSource = _trajProvider,
|
|
Style = null
|
|
};
|
|
_mapParent.Map!.Layers.Add(_trajLayer);
|
|
_predProvider = new MemoryProvider(_predFeatures);
|
|
_predLayer = new Layer
|
|
{
|
|
Name = $"pred_{Id}",
|
|
DataSource = _predProvider,
|
|
Style = null
|
|
};
|
|
_mapParent.Map!.Layers.Add(_predLayer);
|
|
}
|
|
|
|
MapControl _mapParent;
|
|
public string Id { get; set; }
|
|
public bool Focused { get; set; }
|
|
public bool Pulverizando { get; set; }
|
|
public PointFeature Point { get; set; }
|
|
public List<MIFeature> Features { get; set; }
|
|
public double Lat { get; set; }
|
|
public double Lon { get; set; }
|
|
public double Heading { get; set; }
|
|
public double? RawGnssLat { get; set; }
|
|
public double? RawGnssLon { get; set; }
|
|
public bool MostrarPontosLeverArm { get; set; } = true;
|
|
public string Label { get; set; }
|
|
public System.Windows.Media.Color Color { get; set; }
|
|
public StatusOperacao Status { get; set; }
|
|
public List<MIFeature> MarkerShapes { get; set; }
|
|
public bool IsBase { get; set; }
|
|
public Coordinate Position { get; set; }
|
|
public List<double[]> Predict { get; set; }
|
|
|
|
public void UpdateData(
|
|
double? lat = null,
|
|
double? lon = null,
|
|
double? heading = null,
|
|
string? label = null,
|
|
System.Windows.Media.Color? color = null,
|
|
bool? pulverizando = null,
|
|
StatusOperacao? status = null,
|
|
List<double[]>? predict = null,
|
|
double? rawGnssLat = null,
|
|
double? rawGnssLon = null
|
|
)
|
|
{
|
|
Features.Clear();
|
|
|
|
if (lat != null) Lat = (double)lat;
|
|
if (lon != null) Lon = (double)lon;
|
|
if (rawGnssLat != null) RawGnssLat = rawGnssLat;
|
|
if (rawGnssLon != null) RawGnssLon = rawGnssLon;
|
|
if (heading != null) Heading = (double)heading;
|
|
if (label != null) Label = label;
|
|
if (color != null) Color = (System.Windows.Media.Color)color;
|
|
if (status != null) Status = (StatusOperacao)status;
|
|
if (pulverizando != null) Pulverizando = (bool)pulverizando;
|
|
if (predict != null) Predict = predict;
|
|
|
|
var (x, y) = Mapsui.Projections.SphericalMercator.FromLonLat(Lon, Lat);
|
|
var newPoint = new MPoint(x, y);
|
|
|
|
MarkerShapes.Clear();
|
|
|
|
if (IsBase)
|
|
CreateBaseMarker(newPoint);
|
|
else
|
|
CreateRoverTriangleMarker(newPoint);
|
|
|
|
foreach (var shape in MarkerShapes)
|
|
Features.Add(shape);
|
|
|
|
// ponto invisível/pequeno para hit-test e precisão
|
|
Point = new PointFeature(newPoint);
|
|
SetStyles();
|
|
Features.Add(Point);
|
|
|
|
if (lat != null || lon != null)
|
|
UpdateRoverPosition();
|
|
if (predict != null)
|
|
UpdatePredictTrajectory();
|
|
}
|
|
|
|
private (double lengthWorld, double widthWorld, bool realScale) GetRoverSizeWorld()
|
|
{
|
|
double resolution = 1.0;
|
|
|
|
try
|
|
{
|
|
resolution = _mapParent.Map.Navigator.Viewport.Resolution;
|
|
}
|
|
catch
|
|
{
|
|
resolution = 1.0;
|
|
}
|
|
|
|
// Dimensões reais do robô em metros
|
|
double robotLengthM = 1.20;
|
|
double robotWidthM = 1.00;
|
|
|
|
// Acima disso: ícone fixo em px
|
|
// Abaixo disso: tamanho real em metros
|
|
double realScaleResolutionThreshold = 0.14;
|
|
|
|
if (resolution < realScaleResolutionThreshold)
|
|
{
|
|
return (robotLengthM, robotWidthM, true);
|
|
}
|
|
|
|
double normalPx = 24;
|
|
double minPx = 16;
|
|
double maxPx = 32;
|
|
|
|
double sizePx = Math.Clamp(normalPx, minPx, maxPx);
|
|
double lengthWorld = sizePx * resolution;
|
|
double widthWorld = lengthWorld * 0.75;
|
|
|
|
return (lengthWorld, widthWorld, false);
|
|
}
|
|
|
|
private void CreateRoverTriangleMarker(MPoint centerWorld)
|
|
{
|
|
var (baseColor, _, _) = GetStatusVisual();
|
|
|
|
var (lengthWorld, widthWorld, realScale) = GetRoverSizeWorld();
|
|
|
|
double frontLength = lengthWorld * 0.55;
|
|
double backLength = lengthWorld * 0.45;
|
|
double halfWidth = widthWorld * 0.5;
|
|
|
|
// Heading: 0° = Norte
|
|
double rad = Heading * Math.PI / 180.0;
|
|
|
|
double ux = Math.Sin(rad);
|
|
double uy = Math.Cos(rad);
|
|
|
|
// perpendicular
|
|
double px = Math.Cos(rad);
|
|
double py = -Math.Sin(rad);
|
|
|
|
var front = new Coordinate(
|
|
centerWorld.X + ux * frontLength,
|
|
centerWorld.Y + uy * frontLength
|
|
);
|
|
|
|
var left = new Coordinate(
|
|
centerWorld.X - ux * backLength + px * halfWidth,
|
|
centerWorld.Y - uy * backLength + py * halfWidth
|
|
);
|
|
|
|
var right = new Coordinate(
|
|
centerWorld.X - ux * backLength - px * halfWidth,
|
|
centerWorld.Y - uy * backLength - py * halfWidth
|
|
);
|
|
|
|
var ring = new LinearRing(new[]
|
|
{
|
|
front,
|
|
left,
|
|
right,
|
|
front
|
|
});
|
|
|
|
var polygon = new Polygon(ring);
|
|
|
|
var triangle = new GeometryFeature
|
|
{
|
|
Geometry = polygon
|
|
};
|
|
|
|
triangle.Styles.Add(new VectorStyle
|
|
{
|
|
Fill = new Brush(baseColor),
|
|
Outline = new Pen(Mapsui.Styles.Color.White, Focused ? 3 : 2)
|
|
});
|
|
|
|
MarkerShapes.Add(triangle);
|
|
CreateLeverArmReferencePoints(centerWorld);
|
|
|
|
if (Focused)
|
|
{
|
|
var focusRing = new PointFeature(centerWorld);
|
|
focusRing.Styles.Add(new SymbolStyle
|
|
{
|
|
SymbolType = SymbolType.Ellipse,
|
|
SymbolScale = 1.35,
|
|
Fill = new Brush(new Mapsui.Styles.Color(baseColor.R, baseColor.G, baseColor.B, 45)),
|
|
Outline = new Pen(new Mapsui.Styles.Color(baseColor.R, baseColor.G, baseColor.B, 180), 2)
|
|
});
|
|
|
|
MarkerShapes.Add(focusRing);
|
|
}
|
|
}
|
|
|
|
private double GetMarkerSizeWorld(double normalPx = 22, double minPx = 16, double maxPx = 30)
|
|
{
|
|
double resolution = 1.0;
|
|
|
|
try
|
|
{
|
|
// metros por pixel no zoom atual
|
|
resolution = _mapParent.Map.Navigator.Viewport.Resolution;
|
|
}
|
|
catch
|
|
{
|
|
resolution = 1.0;
|
|
}
|
|
|
|
double sizePx = Math.Clamp(normalPx, minPx, maxPx);
|
|
return sizePx * resolution;
|
|
}
|
|
|
|
private void CreateBaseMarker(MPoint centerWorld)
|
|
{
|
|
var baseColor = new Mapsui.Styles.Color(220, 45, 45, 255);
|
|
|
|
double radius = GetMarkerSizeWorld(22, 16, 30);
|
|
|
|
var coords = new List<Coordinate>();
|
|
|
|
// hexágono
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
double ang = Math.PI / 6.0 + i * Math.PI / 3.0;
|
|
|
|
coords.Add(new Coordinate(
|
|
centerWorld.X + Math.Cos(ang) * radius,
|
|
centerWorld.Y + Math.Sin(ang) * radius
|
|
));
|
|
}
|
|
|
|
coords.Add(coords[0]);
|
|
|
|
var hex = new GeometryFeature
|
|
{
|
|
Geometry = new Polygon(new LinearRing(coords.ToArray()))
|
|
};
|
|
|
|
hex.Styles.Add(new VectorStyle
|
|
{
|
|
Fill = new Brush(baseColor),
|
|
Outline = new Pen(Mapsui.Styles.Color.White, Focused ? 3 : 2)
|
|
});
|
|
|
|
MarkerShapes.Add(hex);
|
|
|
|
var center = new PointFeature(centerWorld);
|
|
center.Styles.Add(new SymbolStyle
|
|
{
|
|
SymbolType = SymbolType.Ellipse,
|
|
SymbolScale = 0.45,
|
|
Fill = new Brush(Mapsui.Styles.Color.White),
|
|
Outline = null
|
|
});
|
|
|
|
MarkerShapes.Add(center);
|
|
}
|
|
|
|
private void SetStyles(double _pulseFactor = 1.0)
|
|
{
|
|
if (Point.Styles == null) Point.Styles = new List<IStyle>();
|
|
else Point.Styles.Clear();
|
|
|
|
var (baseColor, statusGlyph, labelBackColor) = GetStatusVisual();
|
|
|
|
// ponto central pequeno para referência/hit-test
|
|
Point.Styles.Add(new SymbolStyle
|
|
{
|
|
SymbolScale = 0.10,
|
|
Fill = new Brush(Mapsui.Styles.Color.White),
|
|
Outline = null
|
|
});
|
|
|
|
bool showLabel =
|
|
Focused ||
|
|
true ||
|
|
Status == StatusOperacao.Erro ||
|
|
Status == StatusOperacao.Parado;
|
|
|
|
if (showLabel && !string.IsNullOrWhiteSpace(Label))
|
|
{
|
|
var texto = string.IsNullOrEmpty(statusGlyph)
|
|
? Label
|
|
: $"{Label} {statusGlyph}";
|
|
|
|
var backColor = labelBackColor ?? new Mapsui.Styles.Color(0, 0, 0, 150);
|
|
|
|
Point.Styles.Add(new LabelStyle
|
|
{
|
|
Text = texto,
|
|
ForeColor = Mapsui.Styles.Color.White,
|
|
BackColor = new Brush(backColor),
|
|
Offset = new Offset(0, -34),
|
|
Font = new Font
|
|
{
|
|
FontFamily = "Segoe UI Emoji",
|
|
Size = 15
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private (Mapsui.Styles.Color baseColor, string statusGlyph, Mapsui.Styles.Color? labelBackColor) GetStatusVisual()
|
|
{
|
|
string glyph = "";
|
|
Mapsui.Styles.Color? back = null;
|
|
|
|
// Base default = cor do robô (personalizada)
|
|
var baseColor = new Mapsui.Styles.Color(Color.R, Color.G, Color.B, Color.A);
|
|
|
|
switch (Status)
|
|
{
|
|
case StatusOperacao.NaoIniciado:
|
|
glyph = "⏳";
|
|
baseColor = new Mapsui.Styles.Color(150, 150, 150, 255); // cinza médio
|
|
back = new Mapsui.Styles.Color(60, 60, 60, 160); // cinza escuro translúcido
|
|
break;
|
|
|
|
case StatusOperacao.Parametrizando:
|
|
glyph = "🔧";
|
|
baseColor = new Mapsui.Styles.Color(40, 120, 255, 255); // azul forte
|
|
back = new Mapsui.Styles.Color(100, 160, 255, 140); // azul claro translúcido
|
|
break;
|
|
|
|
case StatusOperacao.Calibrando:
|
|
glyph = "🎯";
|
|
baseColor = new Mapsui.Styles.Color(160, 70, 255, 255); // roxo vibrante
|
|
back = new Mapsui.Styles.Color(180, 120, 255, 140); // roxo suave
|
|
break;
|
|
|
|
case StatusOperacao.Aguardando:
|
|
glyph = "💤";
|
|
baseColor = new Mapsui.Styles.Color(0, 110, 140, 255); // azul petróleo
|
|
back = new Mapsui.Styles.Color(0, 150, 180, 140); // turquesa suave
|
|
break;
|
|
|
|
case StatusOperacao.EmAndamento:
|
|
glyph = "▶";
|
|
baseColor = new Mapsui.Styles.Color(0, 220, 40, 255); // verde limão
|
|
back = new Mapsui.Styles.Color(0, 200, 30, 140); // verde limão translúcido
|
|
break;
|
|
|
|
case StatusOperacao.Parado:
|
|
glyph = "⏸";
|
|
baseColor = new Mapsui.Styles.Color(255, 180, 0, 255); // amarelo dourado
|
|
back = new Mapsui.Styles.Color(255, 210, 80, 140); // amarelo claro suaaave
|
|
break;
|
|
|
|
case StatusOperacao.Concluido:
|
|
glyph = "✔";
|
|
baseColor = new Mapsui.Styles.Color(0, 150, 60, 255); // verde escuro
|
|
back = new Mapsui.Styles.Color(0, 130, 50, 140); // verde escuro suave
|
|
break;
|
|
|
|
case StatusOperacao.Erro:
|
|
glyph = "⛔";
|
|
baseColor = new Mapsui.Styles.Color(220, 40, 40, 255);
|
|
back = new Mapsui.Styles.Color(120, 0, 0, 190);
|
|
break;
|
|
|
|
default:
|
|
glyph = "";
|
|
break;
|
|
}
|
|
|
|
return (baseColor, glyph, back);
|
|
}
|
|
|
|
private void CreateLeverArmReferencePoints(MPoint correctedWorld)
|
|
{
|
|
if (!MostrarPontosLeverArm)
|
|
return;
|
|
|
|
if (!IsValidLatLon(RawGnssLat, RawGnssLon))
|
|
return;
|
|
|
|
var rawMercator = Mapsui.Projections.SphericalMercator.FromLonLat(
|
|
RawGnssLon!.Value,
|
|
RawGnssLat!.Value
|
|
);
|
|
|
|
var rawWorld = new MPoint(rawMercator.x, rawMercator.y);
|
|
|
|
// Linha tracejada entre GNSS bruto e ponto corrigido
|
|
var line = new GeometryFeature
|
|
{
|
|
Geometry = new LineString(new[]
|
|
{
|
|
new Coordinate(rawWorld.X, rawWorld.Y),
|
|
new Coordinate(correctedWorld.X, correctedWorld.Y)
|
|
})
|
|
};
|
|
|
|
line.Styles.Add(new VectorStyle
|
|
{
|
|
Line = new Pen
|
|
{
|
|
Color = new Mapsui.Styles.Color(255, 255, 255, 170),
|
|
Width = 1,
|
|
PenStyle = PenStyle.Dash
|
|
}
|
|
});
|
|
|
|
MarkerShapes.Add(line);
|
|
|
|
// Ponto GNSS bruto: amarelo
|
|
var rawPoint = new PointFeature(rawWorld);
|
|
rawPoint["MarkerPart"] = "GnssBruto";
|
|
rawPoint.Styles.Add(new SymbolStyle
|
|
{
|
|
SymbolType = SymbolType.Ellipse,
|
|
SymbolScale = 0.55,
|
|
Fill = new Brush(new Mapsui.Styles.Color(255, 215, 0, 255)),
|
|
Outline = new Pen(new Mapsui.Styles.Color(20, 20, 20, 230), 1)
|
|
});
|
|
|
|
MarkerShapes.Add(rawPoint);
|
|
|
|
// Ponto corrigido: branco com contorno preto
|
|
var correctedPoint = new PointFeature(correctedWorld);
|
|
correctedPoint["MarkerPart"] = "GnssCorrigidoLeverArm";
|
|
correctedPoint.Styles.Add(new SymbolStyle
|
|
{
|
|
SymbolType = SymbolType.Ellipse,
|
|
SymbolScale = 0.50,
|
|
Fill = new Brush(Mapsui.Styles.Color.White),
|
|
Outline = new Pen(new Mapsui.Styles.Color(20, 20, 20, 230), 1)
|
|
});
|
|
|
|
MarkerShapes.Add(correctedPoint);
|
|
}
|
|
|
|
private bool IsValidLatLon(double? lat, double? lon)
|
|
{
|
|
if (!lat.HasValue || !lon.HasValue)
|
|
return false;
|
|
|
|
if (lat.Value < -90 || lat.Value > 90)
|
|
return false;
|
|
|
|
if (lon.Value < -180 || lon.Value > 180)
|
|
return false;
|
|
|
|
// Evita desenhar ponto fake em 0,0
|
|
if (Math.Abs(lat.Value) < 0.0000001 && Math.Abs(lon.Value) < 0.0000001)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
public void ClearTrajectory(bool clearPrediction = true)
|
|
{
|
|
_trajFeatures.Clear();
|
|
|
|
if (_trajLayer != null)
|
|
{
|
|
_trajLayer.DataSource = new MemoryProvider(_trajFeatures);
|
|
_trajLayer.DataHasChanged();
|
|
}
|
|
|
|
if (clearPrediction)
|
|
{
|
|
_predFeatures.Clear();
|
|
|
|
if (_predLayer != null)
|
|
{
|
|
_predLayer.DataSource = new MemoryProvider(_predFeatures);
|
|
_predLayer.DataHasChanged();
|
|
}
|
|
}
|
|
|
|
// Reseta a posição base do rastro para a posição atual do rover,
|
|
// evitando que ao voltar a andar ele desenhe uma linha gigante do passado.
|
|
var (x, y) = Mapsui.Projections.SphericalMercator.FromLonLat(Lon, Lat);
|
|
Position = new Coordinate(x, y);
|
|
}
|
|
|
|
|
|
#region TRAJETORIA
|
|
|
|
private MemoryProvider _trajProvider;
|
|
private Layer _trajLayer;
|
|
private List<MIFeature> _trajFeatures { get; set; } = new List<MIFeature>();
|
|
|
|
private VectorStyle CreateTrajectoryStyle()
|
|
{
|
|
if (Pulverizando)
|
|
{
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Mapsui.Styles.Color.FromArgb(255, Color.R, Color.G, Color.B), 4)
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen(Mapsui.Styles.Color.FromArgb(180, Color.R, Color.G, Color.B), 2)
|
|
};
|
|
}
|
|
}
|
|
|
|
public void UpdateRoverPosition()
|
|
{
|
|
var (x, y) = Mapsui.Projections.SphericalMercator.FromLonLat(Lon, Lat);
|
|
var newCoord = new Coordinate(x, y);
|
|
if (Position == null) Position = newCoord;
|
|
var line = new LineString(new[] { Position, newCoord });
|
|
var gf = new GeometryFeature
|
|
{
|
|
Geometry = line
|
|
};
|
|
gf["RoverId"] = Id;
|
|
gf["Pulverizando"] = Pulverizando ? "1" : "0";
|
|
gf.Styles.Add(CreateTrajectoryStyle());
|
|
_trajFeatures.Add(gf);
|
|
_trajLayer.DataSource = new MemoryProvider(_trajFeatures);
|
|
_trajLayer.DataHasChanged();
|
|
Position = newCoord;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region SIMULACAO
|
|
|
|
private MemoryProvider _predProvider;
|
|
private Layer _predLayer;
|
|
private List<MIFeature> _predFeatures { get; set; } = new List<MIFeature>();
|
|
|
|
public void UpdatePredictTrajectory()
|
|
{
|
|
if (_predLayer == null || Predict == null || Predict.Count < 2)
|
|
return;
|
|
|
|
_predFeatures.Clear();
|
|
|
|
// Converte lista (lon, lat) -> coordenadas em SphericalMercator
|
|
var coords = new List<Coordinate>();
|
|
foreach (var lon_lat in Predict)
|
|
{
|
|
double lon = lon_lat[0];
|
|
double lat = lon_lat[1];
|
|
var (x, y) = Mapsui.Projections.SphericalMercator.FromLonLat(lon, lat);
|
|
coords.Add(new Coordinate(x, y));
|
|
}
|
|
|
|
// Garante que tem pelo menos 2 pontos
|
|
if (coords.Count < 2)
|
|
return;
|
|
|
|
var line = new LineString(coords.ToArray());
|
|
|
|
var gf = new GeometryFeature
|
|
{
|
|
Geometry = line
|
|
};
|
|
|
|
gf["RoverId"] = Id;
|
|
gf["Tipo"] = "Predict";
|
|
|
|
gf.Styles.Add(CreatePredictStyle());
|
|
|
|
_predFeatures.Add(gf);
|
|
|
|
_predLayer.DataSource = new MemoryProvider(_predFeatures);
|
|
_predLayer.DataHasChanged();
|
|
}
|
|
|
|
private IStyle CreatePredictStyle()
|
|
{
|
|
return new VectorStyle
|
|
{
|
|
Line = new Pen
|
|
{
|
|
Color = Mapsui.Styles.Color.Lime, // verde-limão
|
|
Width = 2,
|
|
PenStyle = PenStyle.Dash // linha tracejada
|
|
}
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
public class MarkerClickedEventArgs : EventArgs
|
|
{
|
|
public string MarkerId { get; }
|
|
|
|
public MarkerClickedEventArgs(string markerId)
|
|
{
|
|
MarkerId = markerId;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly Dictionary<string, System.Windows.Media.Color> _markerColors = new();
|
|
private readonly HashSet<System.Windows.Media.Color> _usedColors = new();
|
|
|
|
// Paleta de cores "legíveis" no mapa
|
|
private readonly List<System.Windows.Media.Color> _palette = new()
|
|
{
|
|
System.Windows.Media.Colors.DeepSkyBlue,
|
|
System.Windows.Media.Colors.LimeGreen,
|
|
System.Windows.Media.Colors.Yellow,
|
|
System.Windows.Media.Colors.Orange,
|
|
System.Windows.Media.Colors.MediumPurple,
|
|
System.Windows.Media.Colors.Cyan,
|
|
System.Windows.Media.Colors.Magenta,
|
|
System.Windows.Media.Colors.Gold,
|
|
System.Windows.Media.Colors.SpringGreen,
|
|
System.Windows.Media.Colors.Coral,
|
|
System.Windows.Media.Colors.DodgerBlue,
|
|
System.Windows.Media.Colors.HotPink
|
|
};
|
|
|
|
private readonly System.Windows.Media.Color _baseColor = System.Windows.Media.Colors.Red;
|
|
|
|
public System.Windows.Media.Color GetColorForMarker(string markerId, bool isBase = false)
|
|
{
|
|
if (isBase)
|
|
return _baseColor;
|
|
|
|
// Já tem cor atribuída?
|
|
if (_markerColors.TryGetValue(markerId, out var existing))
|
|
return existing;
|
|
|
|
// Pega a próxima cor disponível da paleta
|
|
var color = GetNextAvailableColor();
|
|
|
|
_markerColors[markerId] = color;
|
|
_usedColors.Add(color);
|
|
|
|
return color;
|
|
}
|
|
|
|
private System.Windows.Media.Color GetNextAvailableColor()
|
|
{
|
|
// 1) Tenta achar uma cor da paleta que ainda não foi usada
|
|
var free = _palette.FirstOrDefault(c => !_usedColors.Contains(c));
|
|
|
|
if (free != default(System.Windows.Media.Color))
|
|
return free;
|
|
|
|
// 2) Se todas as cores foram usadas, gera uma nova pseudo-aleatória
|
|
// com base na quantidade atual de marcadores (espalhando no círculo de matiz)
|
|
int n = _markerColors.Count + 1;
|
|
double hue = (n * 47) % 360; // 47 para “espalhar” melhor
|
|
|
|
return ColorFromHsv(hue, 0.9, 0.9);
|
|
}
|
|
|
|
private System.Windows.Media.Color ColorFromHsv(double hue, double saturation, double value)
|
|
{
|
|
// Conversão básica HSV -> RGB
|
|
int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
|
|
double f = hue / 60 - Math.Floor(hue / 60);
|
|
|
|
value = value * 255;
|
|
byte v = (byte)value;
|
|
byte p = (byte)(value * (1 - saturation));
|
|
byte q = (byte)(value * (1 - f * saturation));
|
|
byte t = (byte)(value * (1 - (1 - f) * saturation));
|
|
|
|
return hi switch
|
|
{
|
|
0 => System.Windows.Media.Color.FromArgb(255, v, t, p),
|
|
1 => System.Windows.Media.Color.FromArgb(255, q, v, p),
|
|
2 => System.Windows.Media.Color.FromArgb(255, p, v, t),
|
|
3 => System.Windows.Media.Color.FromArgb(255, p, q, v),
|
|
4 => System.Windows.Media.Color.FromArgb(255, t, p, v),
|
|
_ => System.Windows.Media.Color.FromArgb(255, v, p, q),
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|