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

1537 lines
56 KiB
C#
Raw Normal View History

2025-11-14 17:11:58 +00:00
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)
2025-11-24 16:05:50 +00:00
using UserControl = System.Windows.Controls.UserControl;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
2025-11-14 17:11:58 +00:00
using MIFeature = Mapsui.IFeature;
2025-11-24 16:05:50 +00:00
using Brush = Mapsui.Styles.Brush;
using Pen = Mapsui.Styles.Pen;
using Color = Mapsui.Styles.Color;
using Font = Mapsui.Styles.Font;
2025-11-14 17:11:58 +00:00
using NFeature = NetTopologySuite.Features.Feature;
using Mapsui.Manipulations;
using Mapsui.Extensions;
using OperationControl.Models;
2025-11-24 16:05:50 +00:00
using static AgroBase.Models.Enums;
using System.Windows.Input;
2025-11-14 17:11:58 +00:00
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 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);
}
2025-11-14 17:11:58 +00:00
#region MARCADORES
public MapMarkerManager markers;
public event EventHandler<MapMarkerManager.MarkerClickedEventArgs>? MarkerClicked;
protected virtual void OnMarkerClicked(string markerId)
{
2025-11-24 16:05:50 +00:00
if (markerId == null) markers?.ClearMarkerFocused();
else markers?.SetMarkerFocused(markerId, true);
2025-11-14 17:11:58 +00:00
MarkerClicked?.Invoke(this, new MapMarkerManager.MarkerClickedEventArgs(markerId));
}
private void OnMarkerClickedInternal(MapMarkerManager.MarkerClickedEventArgs e)
{
MarkerClicked?.Invoke(this, e);
2025-11-14 17:11:58 +00:00
if (MarkerClickedCommand?.CanExecute(e) == true)
MarkerClickedCommand.Execute(e);
}
2025-11-14 17:11:58 +00:00
#endregion
2025-11-14 17:11:58 +00:00
#region RUAS PLANTACAO
private Layer MapaPlantacao_Layer;
2025-11-24 16:05:50 +00:00
public List<StreetMapDictionaryModel> RuasMapaCarregado;
2025-11-14 17:11:58 +00:00
public event EventHandler<StreetMapClickedEventArgs>? StreetMapClicked;
private bool ParametrizandoOperacao => !(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Iniciada ?? false) && (VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Status ?? StatusOperacao.NaoIniciado) == StatusOperacao.Parametrizando;
2025-11-14 17:11:58 +00:00
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);
}
2025-12-04 13:51:11 +00:00
private void HighlightRoad(MIFeature? feature = null, string? id = null)
2025-11-14 17:11:58 +00:00
{
2025-12-04 13:51:11 +00:00
if (string.IsNullOrEmpty(id))
id = GetAttr(feature, "Id");
2025-11-14 17:11:58 +00:00
var rua = RuasMapaCarregado.FirstOrDefault(x => x.Id == id);
if (rua == null) return;
rua.Selected = !rua.Selected;
rua.Status = (rua.Selected) ? StreetMapStyle.Selected : StreetMapStyle.Normal;
rua.Feature.Styles.Clear();
rua.Feature.Styles.Add(CreateStreetStyle(rua.Status));
Mapa.Refresh();
OnStreetMapClicked(id);
}
2025-12-04 13:51:11 +00:00
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();
}
2025-11-14 17:11:58 +00:00
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 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");
}
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;
2025-11-24 16:05:50 +00:00
if (info?.Layer == markers.Layer && feat != null && markers != null && markers.TryGetIdFromFeature(feat, out var id))
2025-11-14 17:11:58 +00:00
{
2025-11-24 16:05:50 +00:00
OnMarkerClicked(id);
2025-11-14 17:11:58 +00:00
}
else if (info?.Layer == MapaPlantacao_Layer && feat is MIFeature roadFeature)
{
if (ParametrizandoOperacao)
2025-12-04 13:51:11 +00:00
HighlightRoad(roadFeature);
2025-11-14 17:11:58 +00:00
}
2025-11-24 16:05:50 +00:00
else
{
OnMarkerClicked(null);
}
2025-11-14 17:11:58 +00:00
};
// 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;
2025-11-24 16:05:50 +00:00
if (feat != null && markers != null && markers.TryGetIdFromFeature(feat, out _))
Mapa.Cursor = System.Windows.Input.Cursors.Hand;
else if (feat != null && info?.Layer == MapaPlantacao_Layer && feat is MIFeature roadFeature && ParametrizandoOperacao)
2025-11-14 17:11:58 +00:00
Mapa.Cursor = System.Windows.Input.Cursors.Hand;
else
Mapa.Cursor = System.Windows.Input.Cursors.Arrow;
}
catch
{
Mapa.Cursor = System.Windows.Input.Cursors.Arrow;
}
};
}
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)
{
Latitude = latitude;
Longitude = longitude;
if (scale.HasValue) Scale = scale.Value;
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)
{
var path = dlg.FileName;
if (path.EndsWith(".shp", StringComparison.OrdinalIgnoreCase))
{
var geojson = ConvertShpToGeoJson(path);
AddGeoJsonLayer(geojson);
}
else
{
var geojson = File.ReadAllText(path);
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);
// 1) Converte NTS -> IFeature (GeometryFeature) mantendo WGS84
RuasMapaCarregado = new List<StreetMapDictionaryModel>();
foreach (var f in fc)
{
if (f.Geometry == null) continue;
var gf = new GeometryFeature
{
Geometry = f.Geometry // NTS geometry em WGS84 (lon/lat)
};
// copia atributos (opcional)
foreach (var name in f.Attributes.GetNames())
gf[name] = f.Attributes[name];
RuasMapaCarregado.Add(new StreetMapDictionaryModel()
{
Id = GetAttr(gf, "Id"),
Feature = gf,
Selected = false,
Status = StreetMapStyle.Normal
});
}
// 2) Provider em WGS84 (fonte)
var mem = new MemoryProvider(RuasMapaCarregado.Select(x => x.Feature))
{
CRS = "EPSG:4326"
};
// 3) Projeta on-the-fly para o CRS do mapa (WebMercator)
// Garanta que seu Mapa.Map.CRS esteja em "EPSG:3857" quando criar o Map.
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);
// 4) Camada
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();
// 5) Auto-zoom usando o próprio provider projetado
//var extent = projecting.GetExtent();
//if (extent != null)
// Mapa.Map.Navigator?.ZoomToBox(extent);
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)
{
Variaveis.MostrarLog($"Erro ao carregar coordenadas do mapa: {ex.Message}");
}
}
private void OnCenterAreaClicked(object sender, RoutedEventArgs e)
{
NavigateToView();
}
public void LoadMapFile(string path)
{
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
return;
if (path.EndsWith(".shp", StringComparison.OrdinalIgnoreCase))
{
var geojson = ConvertShpToGeoJson(path);
AddGeoJsonLayer(geojson);
}
else
{
var geojson = File.ReadAllText(path);
AddGeoJsonLayer(geojson);
}
}
2025-11-14 17:11:58 +00:00
#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;
}
2025-11-24 16:05:50 +00:00
public AgroBase.Models.MapaFeatureCollectionModel CriarDadosMapa()
{
AgroBase.Models.MapaFeatureCollectionModel _mapa = new AgroBase.Models.MapaFeatureCollectionModel() { type = "FeatureCollection", features = new List<AgroBase.Models.MapaFeatureModel>() };
foreach (var x in RuasMapaCarregado.Where(x => x.Selected))
{
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;
}
2025-12-04 13:51:11 +00:00
public void CarregarDadosMapa(AgroBase.Models.MapaFeatureCollectionModel Mapa, List<string> RuasSelecionadas)
{
if (RuasMapaCarregado.Any())
{
foreach (var rua in RuasMapaCarregado)
{
var dado = Mapa?.features?.FirstOrDefault(x => x.properties.Id == rua.Id);
2025-12-04 13:51:11 +00:00
if (dado == null) continue;
rua.Selected = RuasSelecionadas.Contains(dado.properties.Id);
}
AtualizarStylesRuas();
}
}
2025-11-14 17:11:58 +00:00
}
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; } }
2025-11-24 16:05:50 +00:00
public enum MarkerStatus
{
Normal,
Warning,
Error,
Offline
}
2025-11-14 17:11:58 +00:00
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));
if (f != null)
{
id = f.Id;
return true;
}
id = null;
return false;
}
public void SetMarkerFocused(string id, bool focused)
{
2025-11-24 16:05:50 +00:00
ClearMarkerFocused();
2025-11-14 17:11:58 +00:00
var f = _features.FirstOrDefault(x => x.Id == id);
if (f != null) f.Focused = focused;
}
public void ClearMarkerFocused()
{
_features.ForEach(f => f.Focused = false);
}
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();
}
}
2025-11-24 16:05:50 +00:00
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)
2025-11-14 17:11:58 +00:00
{
if (string.IsNullOrWhiteSpace(id)) return;
2025-11-14 17:11:58 +00:00
string lbl = label ?? id;
var marker = _features.FirstOrDefault(x => x.Id == id);
if (marker != null)
{
2025-11-24 16:05:50 +00:00
marker.UpdateData(lat: lat, lon: lon, heading: heading, label: label, color: color, status: status, pulverizando: pulv);
2025-11-14 17:11:58 +00:00
}
else
{
marker = new MapMarkerDictionaryModel()
{
Id = id,
Features = new List<MIFeature>(),
2025-11-24 16:05:50 +00:00
Color = color ?? GetColorForMarker(id, isBase),
Heading = heading ?? 0,
Lat = lat ?? 0,
Lon = lon ?? 0,
2025-11-14 17:11:58 +00:00
Label = lbl,
Seta = new List<GeometryFeature>(),
2025-11-24 16:05:50 +00:00
Status = status ?? StatusOperacao.NaoIniciado,
Pulverizando = pulv ?? false,
Focused = false
2025-11-14 17:11:58 +00:00
};
marker.CreateTrajectory(_map);
2025-11-24 16:05:50 +00:00
marker.UpdateData();
2025-11-14 17:11:58 +00:00
_features.Add(marker);
}
UpdateFeatures();
if (center)
_map.Map?.Navigator?.CenterOn(marker.Point.Point);
}
2025-11-24 16:05:50 +00:00
public void UpdateMarkerPosition(string id, double? lat = null, double? lon = null, double? heading = null, List<double[]>? predict = null)
{
if (string.IsNullOrWhiteSpace(id)) return;
2025-11-24 16:05:50 +00:00
var marker = _features.FirstOrDefault(x => x.Id == id);
if (marker == null) return;
marker.UpdateData(lat: lat, lon: lon, heading: heading, predict: predict);
UpdateFeatures();
}
public void UpdateMarkerInfo(string id, StatusOperacao? status = null, bool? pulv = null)
2025-11-14 17:11:58 +00:00
{
if (string.IsNullOrWhiteSpace(id)) return;
2025-11-24 16:05:50 +00:00
2025-11-14 17:11:58 +00:00
var marker = _features.FirstOrDefault(x => x.Id == id);
if (marker == null) return;
2025-11-24 16:05:50 +00:00
marker.UpdateData(status: status, pulverizando: pulv);
2025-11-14 17:11:58 +00:00
UpdateFeatures();
}
public void RemoveMarker(string id)
{
var marker = _features.FirstOrDefault(x => x.Id == id);
if (marker == null) return;
_features.Remove(marker);
UpdateFeatures();
}
public class MapMarkerDictionaryModel
{
public void CreateTrajectory(MapControl _map)
{
_trajProvider = new MemoryProvider(_trajFeatures);
_trajLayer = new Layer
{
Name = $"traj_{Id}",
DataSource = _trajProvider,
Style = null
};
_map.Map!.Layers.Add(_trajLayer);
2025-11-24 16:05:50 +00:00
_predProvider = new MemoryProvider(_predFeatures);
_predLayer = new Layer
{
Name = $"pred_{Id}",
DataSource = _predProvider,
Style = null
};
_map.Map!.Layers.Add(_predLayer);
2025-11-14 17:11:58 +00:00
}
public string Id { get; set; }
public bool Focused { get; set; }
2025-11-24 16:05:50 +00:00
public bool Pulverizando { get; set; }
2025-11-14 17:11:58 +00:00
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 string Label { get; set; }
public System.Windows.Media.Color Color { get; set; }
2025-11-24 16:05:50 +00:00
public StatusOperacao Status { get; set; }
2025-11-14 17:11:58 +00:00
public List<GeometryFeature> Seta { get; set; }
public Coordinate Position { get; set; }
2025-11-24 16:05:50 +00:00
public List<double[]> Predict { get; set; }
2025-11-14 17:11:58 +00:00
2025-11-24 16:05:50 +00:00
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)
2025-11-14 17:11:58 +00:00
{
Features.Clear();
2025-11-24 16:05:50 +00:00
if (lat != null) Lat = (double)lat;
if (lon != null) Lon = (double)lon;
if (heading != null) Heading = (double)heading;
2025-11-14 17:11:58 +00:00
if (label != null) Label = label;
if (color != null) Color = (System.Windows.Media.Color)color;
2025-11-24 16:05:50 +00:00
if (status != null) Status = (StatusOperacao)status;
if (pulverizando != null) Pulverizando = (bool)pulverizando;
if (predict != null) Predict = predict;
2025-11-14 17:11:58 +00:00
var (x, y) = Mapsui.Projections.SphericalMercator.FromLonLat(Lon, Lat);
var newPoint = new MPoint(x, y);
UpsertHeadingFeature(newPoint, withArrowHead: true);
Point = new PointFeature(newPoint);
SetStyles();
Features.Add(Point);
Seta.ForEach(f => Features.Add(f));
2025-11-24 16:05:50 +00:00
if (lat != null || lon != null)
UpdateRoverPosition();
if (predict != null)
UpdatePredictTrajectory();
2025-11-14 17:11:58 +00:00
}
private void UpsertHeadingFeature(MPoint centerWorld, bool withArrowHead = true)
{
Seta.Clear();
// 1) Comprimento constante em tela: px -> metros
//var resolution = _map.Map?.Navigator?.Resolutions?.LastOrDefault() ?? 1.0; // m/px no WebMercator
var resolution = 1.0;
var lenPx = 9; // corpo da seta (ajuste fino: 12~20 px)
var headPx = 6; // “asas” da seta (se withArrowHead = true)
var len = lenPx * resolution; // metros
var headLen = headPx * resolution; // metros
// 2) 0° = Norte (↑). Se o seu 0° for Leste (→), troque sin/cos conforme comentário abaixo.
var rad = Heading * Math.PI / 180.0;
var dx = len * Math.Sin(rad); // se 0° = Leste → use cos(rad)
var dy = len * Math.Cos(rad); // se 0° = Leste → use sin(rad)
var p0 = centerWorld;
var p1 = new MPoint(centerWorld.X + dx, centerWorld.Y + dy);
// 3) Corpo da seta (linha principal)
var mainLine = new LineString(new Coordinate[] { new Coordinate(p0.X, p0.Y), new Coordinate(p1.X, p1.Y) });
var mainFeat = new GeometryFeature { Geometry = mainLine };
SetHeadingStyle(mainFeat, Color);
Seta.Add(mainFeat);
// 5) (Opcional) cabeça da seta em “V”: duas linhas com ±θ a partir da ponta
if (withArrowHead)
{
var theta = 25 * Math.PI / 180.0; // abertura do V (~25°)
// perna esquerda
var dxL = headLen * Math.Sin(rad + Math.PI - theta);
var dyL = headLen * Math.Cos(rad + Math.PI - theta);
var pL = new MPoint(p1.X + dxL, p1.Y + dyL);
var leftLine = new LineString(new Coordinate[] { new Coordinate(p1.X, p1.Y), new Coordinate(pL.X, pL.Y) });
// perna direita
var dxR = headLen * Math.Sin(rad + Math.PI + theta);
var dyR = headLen * Math.Cos(rad + Math.PI + theta);
var pR = new MPoint(p1.X + dxR, p1.Y + dyR);
var rightLine = new LineString(new Coordinate[] { new Coordinate(p1.X, p1.Y), new Coordinate(pR.X, pR.Y) });
var fL = new GeometryFeature { Geometry = leftLine };
var fR = new GeometryFeature { Geometry = rightLine };
SetHeadingStyle(fL, Color);
SetHeadingStyle(fR, Color);
Seta.Add(fL);
Seta.Add(fR);
}
}
private static void SetHeadingStyle(GeometryFeature f, System.Windows.Media.Color wpfColor)
{
if (f.Styles == null) f.Styles = new List<IStyle>(); else f.Styles.Clear();
var c = new Mapsui.Styles.Color(wpfColor.R, wpfColor.G, wpfColor.B, 230);
f.Styles.Add(new VectorStyle
{
Line = new Pen(c, 2), // espessura em px na tela
Outline = null,
});
}
2025-11-24 16:05:50 +00:00
private void SetStyles(double _pulseFactor = 1.0)
2025-11-14 17:11:58 +00:00
{
if (Point.Styles == null) Point.Styles = new List<IStyle>();
else Point.Styles.Clear();
2025-11-24 16:05:50 +00:00
// Pega o “visual” conforme o Status
var (baseColor, statusGlyph, labelBackColor) = GetStatusVisual();
2025-11-14 17:11:58 +00:00
// 1) Ponto central minúsculo (precisão)
Point.Styles.Add(new SymbolStyle
{
2025-11-24 16:05:50 +00:00
SymbolScale = 0.22,
Fill = new Brush(baseColor),
2025-11-14 17:11:58 +00:00
Outline = null
});
// 2) “Aura” simulando gradiente (3 círculos com alpha/escala crescentes)
Point.Styles.Add(new SymbolStyle
{
2025-11-24 16:05:50 +00:00
SymbolScale = _pulseFactor * 0.55,
Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 120)),
2025-11-14 17:11:58 +00:00
Outline = null
});
Point.Styles.Add(new SymbolStyle
{
2025-11-24 16:05:50 +00:00
SymbolScale = _pulseFactor * 0.95,
Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 70)),
2025-11-14 17:11:58 +00:00
Outline = null
});
Point.Styles.Add(new SymbolStyle
{
2025-11-24 16:05:50 +00:00
SymbolScale = _pulseFactor * 1.4,
Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 30)),
2025-11-14 17:11:58 +00:00
Outline = null
});
// 3) Rótulo do id (pequeno, acima do ponto)
if (!string.IsNullOrWhiteSpace(Label))
{
2025-11-24 16:05:50 +00:00
// texto: se tiver glyph, coloca na frente
var texto = string.IsNullOrEmpty(statusGlyph)
? Label
: $"{Label} {statusGlyph}";
var backColor = labelBackColor ?? new Color(0, 0, 0, 140);
2025-11-14 17:11:58 +00:00
Point.Styles.Add(new LabelStyle
{
2025-11-24 16:05:50 +00:00
Text = texto,
2025-11-14 17:11:58 +00:00
ForeColor = Mapsui.Styles.Color.White,
2025-11-24 16:05:50 +00:00
BackColor = new Brush(backColor),
Offset = new Offset(0, -30),
Font = new Font { FontFamily = "Segoe UI Emoji", Size = 18 }
2025-11-14 17:11:58 +00:00
});
}
}
2025-11-24 16:05:50 +00:00
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);
}
2025-11-14 17:11:58 +00:00
#region TRAJETORIA
private MemoryProvider _trajProvider;
private Layer _trajLayer;
private List<MIFeature> _trajFeatures { get; set; } = new List<MIFeature>();
2025-11-24 16:05:50 +00:00
private VectorStyle CreateTrajectoryStyle()
2025-11-14 17:11:58 +00:00
{
2025-11-24 16:05:50 +00:00
if (Pulverizando)
2025-11-14 17:11:58 +00:00
{
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)
};
}
}
2025-11-24 16:05:50 +00:00
public void UpdateRoverPosition()
2025-11-14 17:11:58 +00:00
{
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;
2025-11-24 16:05:50 +00:00
gf["Pulverizando"] = Pulverizando ? "1" : "0";
gf.Styles.Add(CreateTrajectoryStyle());
2025-11-14 17:11:58 +00:00
_trajFeatures.Add(gf);
_trajLayer.DataSource = new MemoryProvider(_trajFeatures);
_trajLayer.DataHasChanged();
Position = newCoord;
}
#endregion
2025-11-24 16:05:50 +00:00
#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
2025-11-14 17:11:58 +00:00
}
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),
};
}
}
}