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

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),
};
}
}
}