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; namespace OperationControl.Controls { public partial class MapViewControl : UserControl { public MapViewControl() { InitializeComponent(); Loaded += OnLoaded; markers = new MapMarkerManager(Mapa); RuasMapaCarregado = new List(); 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); } #region MARCADORES public MapMarkerManager markers; public event EventHandler? MarkerClicked; 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 RuasMapaCarregado; public event EventHandler? StreetMapClicked; private bool ParametrizandoOperacao => !(VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Iniciada ?? false) && (VariaveisControleOperacao.RoverEmFoco?.DadosLeitura?.Operacao?.Status ?? StatusOperacao.NaoIniciado) == StatusOperacao.Parametrizando; 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; 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); } 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 class StreetMapClickedEventArgs : EventArgs { public string StreetId { get; } public List SelectedStreetsIds { get; } public StreetMapClickedEventArgs(string streetId, List selectedStreets) { StreetId = streetId; SelectedStreetsIds = new List(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 _baseOptions; public string MbTilesPath { get => (string)GetValue(MbTilesPathProperty); set => SetValue(MbTilesPathProperty, value); } private class BaseLayerOption { public string Name { get; set; } = ""; public Func CreateLayer { get; set; } = default!; public override string ToString() => Name; } private void InitBaseMapOptions() { _baseOptions = new List { 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; if (info?.Layer == markers.Layer && feat != null && markers != null && markers.TryGetIdFromFeature(feat, out var id)) { OnMarkerClicked(id); } else if (info?.Layer == MapaPlantacao_Layer && feat is MIFeature roadFeature) { if (ParametrizandoOperacao) HighlightRoad(roadFeature); } 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 _)) 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 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; } /// /// API pública para ajustar a câmera via código. /// 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(geojson); // 1) Converte NTS -> IFeature (GeometryFeature) mantendo WGS84 RuasMapaCarregado = new List(); 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); } } #endregion private Dictionary ParseFeatureAttributes(MIFeature feature) { var dict = new Dictionary(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() { AgroBase.Models.MapaFeatureCollectionModel _mapa = new AgroBase.Models.MapaFeatureCollectionModel() { type = "FeatureCollection", features = new List() }; 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>(); 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 { 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 RuasSelecionadas) { 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); } AtualizarStylesRuas(); } } } 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 _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)); 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; } 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(); } } 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) { 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); } else { marker = new MapMarkerDictionaryModel() { Id = id, Features = new List(), Color = color ?? GetColorForMarker(id, isBase), Heading = heading ?? 0, Lat = lat ?? 0, Lon = lon ?? 0, Label = lbl, Seta = new List(), 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? predict = 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); 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 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); _predProvider = new MemoryProvider(_predFeatures); _predLayer = new Layer { Name = $"pred_{Id}", DataSource = _predProvider, Style = null }; _map.Map!.Layers.Add(_predLayer); } public string Id { get; set; } public bool Focused { get; set; } public bool Pulverizando { get; set; } public PointFeature Point { get; set; } public List 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; } public StatusOperacao Status { get; set; } public List Seta { get; set; } public Coordinate Position { get; set; } public List 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? predict = null) { Features.Clear(); if (lat != null) Lat = (double)lat; if (lon != null) Lon = (double)lon; 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); UpsertHeadingFeature(newPoint, withArrowHead: true); Point = new PointFeature(newPoint); SetStyles(); Features.Add(Point); Seta.ForEach(f => Features.Add(f)); if (lat != null || lon != null) UpdateRoverPosition(); if (predict != null) UpdatePredictTrajectory(); } 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(); 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, }); } private void SetStyles(double _pulseFactor = 1.0) { if (Point.Styles == null) Point.Styles = new List(); else Point.Styles.Clear(); // Pega o “visual” conforme o Status var (baseColor, statusGlyph, labelBackColor) = GetStatusVisual(); // 1) Ponto central minúsculo (precisão) Point.Styles.Add(new SymbolStyle { SymbolScale = 0.22, Fill = new Brush(baseColor), Outline = null }); // 2) “Aura” simulando gradiente (3 círculos com alpha/escala crescentes) Point.Styles.Add(new SymbolStyle { SymbolScale = _pulseFactor * 0.55, Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 120)), Outline = null }); Point.Styles.Add(new SymbolStyle { SymbolScale = _pulseFactor * 0.95, Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 70)), Outline = null }); Point.Styles.Add(new SymbolStyle { SymbolScale = _pulseFactor * 1.4, Fill = new Brush(new Color(baseColor.R, baseColor.G, baseColor.B, 30)), Outline = null }); // 3) Rótulo do id (pequeno, acima do ponto) if (!string.IsNullOrWhiteSpace(Label)) { // texto: se tiver glyph, coloca na frente var texto = string.IsNullOrEmpty(statusGlyph) ? Label : $"{Label} {statusGlyph}"; var backColor = labelBackColor ?? new Color(0, 0, 0, 140); Point.Styles.Add(new LabelStyle { Text = texto, ForeColor = Mapsui.Styles.Color.White, BackColor = new Brush(backColor), Offset = new Offset(0, -30), Font = new Font { FontFamily = "Segoe UI Emoji", Size = 18 } }); } } 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); } #region TRAJETORIA private MemoryProvider _trajProvider; private Layer _trajLayer; private List _trajFeatures { get; set; } = new List(); 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 _predFeatures { get; set; } = new List(); 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(); 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 _markerColors = new(); private readonly HashSet _usedColors = new(); // Paleta de cores "legíveis" no mapa private readonly List _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), }; } } }