From 05efac1fd0c25c18b1c60cbf0f2bc058a77884d7 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Tue, 12 Sep 2023 06:40:31 +0100 Subject: [PATCH] Optimizations and map display bug fixes (#3168) * Overlay features added in batch * Remove redundant feature copy * Warnings as errors * Map cache as list * Remove unused * Optimize memory allocations * Improve locking of map cache * Settings optimization * Reducing feature properties deserialized to reduce memory usage --------- Co-authored-by: Rupert Benbrook --- .../AltitudeAngelWings.Plugin/AASettings.cs | 5 +- .../AltitudeAngelWings.Plugin/MapAdapter.cs | 18 +- .../OverlayAdapter.cs | 140 ++++---- .../AltitudeAngelWings.csproj | 10 + .../Clients/Api/Model/FeatureExtensions.cs | 10 +- .../Clients/Api/Model/FeatureProperties.cs | 60 ---- ExtLibs/AltitudeAngelWings/IMap.cs | 2 - ExtLibs/AltitudeAngelWings/IOverlay.cs | 10 +- ExtLibs/AltitudeAngelWings/OverlayFeature.cs | 24 ++ .../AltitudeAngelWings/OverlayFeatureType.cs | 8 + .../Service/AltitudeAngelService.cs | 301 +++++++++--------- 11 files changed, 265 insertions(+), 323 deletions(-) create mode 100644 ExtLibs/AltitudeAngelWings/OverlayFeature.cs create mode 100644 ExtLibs/AltitudeAngelWings/OverlayFeatureType.cs diff --git a/ExtLibs/AltitudeAngelWings.Plugin/AASettings.cs b/ExtLibs/AltitudeAngelWings.Plugin/AASettings.cs index ed26f977bd..cac00a3e3f 100644 --- a/ExtLibs/AltitudeAngelWings.Plugin/AASettings.cs +++ b/ExtLibs/AltitudeAngelWings.Plugin/AASettings.cs @@ -130,7 +130,10 @@ private void trv_MapLayers_AfterCheck(object sender, TreeViewEventArgs e) { var item = (FilterInfoDisplay)e.Node.Tag; item.Visible = e.Node.Checked; - ProcessMapsFromCache(); + if (trv_MapLayers.Visible && trv_MapLayers.Focused) + { + ProcessMapsFromCache(); + } } private void txt_FlightPlanName_TextChanged(object sender, EventArgs e) diff --git a/ExtLibs/AltitudeAngelWings.Plugin/MapAdapter.cs b/ExtLibs/AltitudeAngelWings.Plugin/MapAdapter.cs index d9a1b5addf..80c6aa07de 100644 --- a/ExtLibs/AltitudeAngelWings.Plugin/MapAdapter.cs +++ b/ExtLibs/AltitudeAngelWings.Plugin/MapAdapter.cs @@ -147,13 +147,6 @@ private Feature[] GetMapItemsOnMouseClick(Point point) return mapItems.ToArray(); } - public LatLong GetCenter() - { - var pointLatLng = default(PointLatLng); - _context.Send(_ => pointLatLng = _mapControl.Position, null); - return new LatLong(pointLatLng.Lat, pointLatLng.Lng); - } - public BoundingLatLong GetViewArea() { var rectLatLng = default(RectLatLng); @@ -170,9 +163,6 @@ public BoundingLatLong GetViewArea() }; } - public void AddOverlay(string name) - => _context.Send(state => _mapControl.Overlays.Add(new GMapOverlay(name)), null); - public void DeleteOverlay(string name) => _context.Send(_ => { @@ -192,10 +182,8 @@ public IOverlay GetOverlay(string name, bool createIfNotExists = false) if (overlay == null) { if (!createIfNotExists) throw new ArgumentException($"Overlay {name} not found."); - AddOverlay(name); - result = GetOverlay(name); - return; - + overlay = new GMapOverlay(name); + _mapControl.Overlays.Add(overlay); } result = new OverlayAdapter(overlay); @@ -208,7 +196,7 @@ public void Invalidate() _mapControl.Invalidate(); } - protected void Dispose(bool isDisposing) + protected virtual void Dispose(bool isDisposing) { if (isDisposing) { diff --git a/ExtLibs/AltitudeAngelWings.Plugin/OverlayAdapter.cs b/ExtLibs/AltitudeAngelWings.Plugin/OverlayAdapter.cs index db5bf48152..7a03453367 100644 --- a/ExtLibs/AltitudeAngelWings.Plugin/OverlayAdapter.cs +++ b/ExtLibs/AltitudeAngelWings.Plugin/OverlayAdapter.cs @@ -3,8 +3,6 @@ using System.Linq; using System.Threading; using System.Windows.Forms; -using AltitudeAngelWings.Clients.Api.Model; -using GeoJSON.Net.Feature; using GMap.NET; using GMap.NET.WindowsForms; @@ -21,99 +19,87 @@ public OverlayAdapter(GMapOverlay overlay) _overlay = overlay; } - public bool IsVisible - { - get - { - var value = false; - _context.Send(state => - { - value = _overlay.IsVisibile; - }, null); - return value; - } - set - { - _context.Send(state => - { - _overlay.IsVisibile = value; - }, null); - } - } - - public void AddOrUpdatePolygon(string name, List points, ColorInfo colorInfo, Feature featureInfo) + public void SetFeatures(IReadOnlyList features) { _context.Send(_ => { - var polygon = _overlay.Polygons.FirstOrDefault(p => p.Name == name); - if (polygon == null) + var existing = _overlay.Polygons.Union(_overlay.Routes.Cast()).ToDictionary(i => i.Name, i => i); + var index = features.ToDictionary(f => f.Name, f => f); + + // Remove polygons and routes not in features + foreach (var remove in existing.Keys.Except(index.Keys)) { - polygon = new GMapPolygon(points.ConvertAll(p => new PointLatLng(p.Latitude, p.Longitude)), name); - _overlay.Polygons.Add(polygon); + var item = existing[remove]; + switch (item) + { + case GMapPolygon polygon: + _overlay.Polygons.Remove(polygon); + break; + case GMapRoute route: + _overlay.Routes.Remove(route); + break; + } } - polygon.Fill = new SolidBrush(Color.FromArgb((int)colorInfo.FillColor)); - polygon.Stroke = new Pen(Color.FromArgb((int)colorInfo.StrokeColor), colorInfo.StrokeWidth); - polygon.IsHitTestVisible = true; - polygon.Tag = featureInfo; - }, null); - } - public void RemovePolygonsExcept(List names) - { - _context.Send(_ => - { - var remove = _overlay.Polygons - .Where(p => !names.Contains(p.Name)) - .ToArray(); - foreach (var polygon in remove) + // Update polygons and routes already in features and remove from index as updated + foreach (var update in existing.Keys.Intersect(index.Keys)) { - _overlay.Polygons.Remove(polygon); - } - }, null); - } + var item = existing[update]; + var feature = index[update]; + switch (item) + { + case GMapPolygon polygon: + polygon.Points.Clear(); + polygon.Points.AddRange(feature.Points.Select(p => new PointLatLng(p.Latitude, p.Longitude))); + SetPolygon(polygon, feature); + break; + case GMapRoute route: + route.Points.Clear(); + route.Points.AddRange(feature.Points.Select(p => new PointLatLng(p.Latitude, p.Longitude))); + SetRoute(route, feature); + break; + } - public bool PolygonExists(string name) - { - var polygonExists = false; - _context.Send(_ => polygonExists = _overlay.Polygons.Any(i => i.Name == name), null); - return polygonExists; - } + index.Remove(update); + } - public void AddOrUpdateLine(string name, List points, ColorInfo colorInfo, Feature featureInfo) - { - _context.Send(_ => - { - var route = _overlay.Routes.FirstOrDefault(r => r.Name == name); - if (route == null) + // Add polygons and routes that are left in the index + foreach (var item in index.Values) { - route = new GMapRoute(points.ConvertAll(p => new PointLatLng(p.Latitude, p.Longitude)), name); - _overlay.Routes.Add(route); + switch (item.Type) + { + case OverlayFeatureType.Polygon: + var polygon = new GMapPolygon( + item.Points.Select(p => new PointLatLng(p.Latitude, p.Longitude)).ToList(), + item.Name); + SetPolygon(polygon, item); + _overlay.Polygons.Add(polygon); + break; + case OverlayFeatureType.Line: + var route = new GMapRoute( + item.Points.Select(p => new PointLatLng(p.Latitude, p.Longitude)).ToList(), + item.Name); + SetRoute(route, item); + _overlay.Routes.Add(route); + break; + } } - route.Stroke = new Pen(Color.FromArgb((int)colorInfo.StrokeColor), colorInfo.StrokeWidth + 2); - route.IsHitTestVisible = true; - route.Tag = featureInfo; }, null); } - public void RemoveLinesExcept(List names) + private static void SetRoute(GMapRoute route, OverlayFeature feature) { - _context.Send(_ => - { - var remove = _overlay.Routes - .Where(p => !names.Contains(p.Name)) - .ToArray(); - foreach (var route in remove) - { - _overlay.Routes.Remove(route); - } - }, null); + route.Stroke = new Pen(Color.FromArgb((int)feature.ColorInfo.StrokeColor), feature.ColorInfo.StrokeWidth); + route.IsHitTestVisible = true; + route.Tag = feature.FeatureInfo; } - public bool LineExists(string name) + private static void SetPolygon(GMapPolygon polygon, OverlayFeature feature) { - var exists = false; - _context.Send(_ => exists = _overlay.Routes.Any(i => i.Name == name), null); - return exists; + polygon.Fill = new SolidBrush(Color.FromArgb((int)feature.ColorInfo.FillColor)); + polygon.Stroke = new Pen(Color.FromArgb((int)feature.ColorInfo.StrokeColor), feature.ColorInfo.StrokeWidth); + polygon.IsHitTestVisible = true; + polygon.Tag = feature.FeatureInfo; } } } \ No newline at end of file diff --git a/ExtLibs/AltitudeAngelWings/AltitudeAngelWings.csproj b/ExtLibs/AltitudeAngelWings/AltitudeAngelWings.csproj index 4bd0221eef..449baab0fd 100644 --- a/ExtLibs/AltitudeAngelWings/AltitudeAngelWings.csproj +++ b/ExtLibs/AltitudeAngelWings/AltitudeAngelWings.csproj @@ -4,6 +4,16 @@ netstandard2.0 false + + 9999 + + True + + + 9999 + + True + diff --git a/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureExtensions.cs b/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureExtensions.cs index 99c10ebdbd..c9be3ff974 100644 --- a/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureExtensions.cs +++ b/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureExtensions.cs @@ -1,17 +1,14 @@ using System.Collections.Generic; using System.Linq; using GeoJSON.Net.Feature; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace AltitudeAngelWings.Clients.Api.Model { public static class FeatureExtensions { - private static readonly char[] DefaultWhitespaceChars = { ' ', '\r', '\n', '\t' }; - public static FeatureProperties GetFeatureProperties(this Feature feature) - => JsonConvert.DeserializeObject(new JObject(feature.Properties.Select(p => new JProperty(p.Key, p.Value))).ToString()); + => new JObject(feature.Properties.Select(p => new JProperty(p.Key, p.Value))).ToObject(); public static IEnumerable GetFilterInfo(this Feature feature) { @@ -20,7 +17,7 @@ public static IEnumerable GetFilterInfo(this Feature feature) yield break; } - var filters = JsonConvert.DeserializeObject>(feature.Properties["filters"].ToString()); + var filters = ((JArray)feature.Properties["filters"]).ToObject>(); var visible = filters.All(f => f.Active); foreach (var f in filters) { @@ -68,8 +65,5 @@ public static void UpdateFilterInfo(this IEnumerable features, IList JsonConvert.DeserializeObject(feature.Properties["display"].ToString()); } } diff --git a/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureProperties.cs b/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureProperties.cs index 5ae74a560d..3483760623 100644 --- a/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureProperties.cs +++ b/ExtLibs/AltitudeAngelWings/Clients/Api/Model/FeatureProperties.cs @@ -8,51 +8,9 @@ public class FeatureProperties [JsonProperty("detailedCategory")] public string DetailedCategory { get; set; } - [JsonProperty("description")] - public string Description { get; set; } - - [JsonProperty("fromUtc")] - public string FromUtc { get; set; } - - [JsonProperty("toUtc")] - public string ToUtc { get; set; } - - [JsonProperty("fromLocal")] - public string FromLocal { get; set; } - - [JsonProperty("toLocal")] - public string ToLocal { get; set; } - - [JsonProperty("timeZone")] - public string TimeZone { get; set; } - - [JsonProperty("flightType")] - public string FlightType { get; set; } - - [JsonProperty("radiusMeters")] - public string RadiusMeters { get; set; } - [JsonProperty("isOwner")] public bool IsOwner { get; set; } - [JsonProperty("alertSummary")] - public string AlertSummary { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("state")] - public string State { get; set; } - - [JsonProperty("iconUrl")] - public string IconUrl { get; set; } - - [JsonProperty("hazardFactor")] - public string HazardFactor { get; set; } - - [JsonProperty("hazardFactorName")] - public string HazardFactorName { get; set; } - [JsonProperty("fillColor")] public string FillColor { get; set; } @@ -68,33 +26,15 @@ public class FeatureProperties [JsonProperty("strokeWidth")] public string StrokeWidth { get; set; } - [JsonProperty("borderColor")] - public string BorderColor { get; set; } - - [JsonProperty("borderOpacity")] - public string BorderOpacity { get; set; } - - [JsonProperty("borderWidth")] - public string BorderWidth { get; set; } - - [JsonProperty("category")] - public string Category { get; set; } - [JsonProperty("radius")] public string Radius { get; set; } - [JsonProperty("filters")] - public IList Filters { get; set; } - [JsonProperty("display")] public DisplayInfo DisplayInfo { get; set; } [JsonProperty("altitudeFloor")] public AltitudeProperty AltitudeFloor { get; set; } - [JsonProperty("altitudeCeiling")] - public AltitudeProperty AltitudeCeiling { get; set; } - [JsonProperty("utmStatus")] public UtmStatus UtmStatus { get; set; } diff --git a/ExtLibs/AltitudeAngelWings/IMap.cs b/ExtLibs/AltitudeAngelWings/IMap.cs index 2a9fb05bf7..5d8cbc0d7a 100644 --- a/ExtLibs/AltitudeAngelWings/IMap.cs +++ b/ExtLibs/AltitudeAngelWings/IMap.cs @@ -8,9 +8,7 @@ namespace AltitudeAngelWings public interface IMap { bool Enabled { get; } - LatLong GetCenter(); BoundingLatLong GetViewArea(); - void AddOverlay(string name); void DeleteOverlay(string name); IOverlay GetOverlay(string name, bool createIfNotExists = false); IObservable MapChanged { get; } diff --git a/ExtLibs/AltitudeAngelWings/IOverlay.cs b/ExtLibs/AltitudeAngelWings/IOverlay.cs index d7244d6732..e994af3182 100644 --- a/ExtLibs/AltitudeAngelWings/IOverlay.cs +++ b/ExtLibs/AltitudeAngelWings/IOverlay.cs @@ -1,17 +1,9 @@ using System.Collections.Generic; -using AltitudeAngelWings.Clients.Api.Model; -using GeoJSON.Net.Feature; namespace AltitudeAngelWings { public interface IOverlay { - void AddOrUpdatePolygon(string name, List points, ColorInfo colorInfo, Feature featureInfo); - void AddOrUpdateLine(string name, List points, ColorInfo colorInfo, Feature featureInfo); - bool LineExists(string name); - bool PolygonExists(string name); - bool IsVisible { get; set; } - void RemovePolygonsExcept(List names); - void RemoveLinesExcept(List names); + void SetFeatures(IReadOnlyList features); } } diff --git a/ExtLibs/AltitudeAngelWings/OverlayFeature.cs b/ExtLibs/AltitudeAngelWings/OverlayFeature.cs new file mode 100644 index 0000000000..8e44692ba4 --- /dev/null +++ b/ExtLibs/AltitudeAngelWings/OverlayFeature.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using AltitudeAngelWings.Clients.Api.Model; +using GeoJSON.Net.Feature; + +namespace AltitudeAngelWings +{ + public class OverlayFeature + { + public string Name { get; } + public OverlayFeatureType Type { get; } + public IReadOnlyList Points { get; } + public ColorInfo ColorInfo { get; } + public Feature FeatureInfo { get; } + + public OverlayFeature(string name, OverlayFeatureType type, IReadOnlyList points, ColorInfo colorInfo, Feature featureInfo) + { + Name = name; + Type = type; + Points = points; + ColorInfo = colorInfo; + FeatureInfo = featureInfo; + } + } +} \ No newline at end of file diff --git a/ExtLibs/AltitudeAngelWings/OverlayFeatureType.cs b/ExtLibs/AltitudeAngelWings/OverlayFeatureType.cs new file mode 100644 index 0000000000..d85f190293 --- /dev/null +++ b/ExtLibs/AltitudeAngelWings/OverlayFeatureType.cs @@ -0,0 +1,8 @@ +namespace AltitudeAngelWings +{ + public enum OverlayFeatureType + { + Polygon, + Line + } +} \ No newline at end of file diff --git a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs index 78fa488fd7..d315de8843 100644 --- a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs +++ b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs @@ -24,7 +24,7 @@ namespace AltitudeAngelWings.Service public class AltitudeAngelService : IAltitudeAngelService { private const string MapOverlayName = "AAMapData"; - private static readonly Dictionary MapFeatureCache = new Dictionary(); + private static readonly List MapFeatureCache = new List(); private readonly IMessagesService _messagesService; private readonly IMissionPlanner _missionPlanner; @@ -35,7 +35,7 @@ public class AltitudeAngelService : IAltitudeAngelService private readonly ISettings _settings; private readonly ITokenProvider _tokenProvider; private readonly SemaphoreSlim _signInLock = new SemaphoreSlim(1); - private readonly SemaphoreSlim _processLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim _mapCacheLock = new SemaphoreSlim(1); public ObservableProperty IsSignedIn { get; } public ObservableProperty WeatherReport { get; } @@ -97,6 +97,7 @@ public async Task SignInAsync(CancellationToken cancellationToken = default) // Attempt to get a token var token = await _tokenProvider.GetToken(cancellationToken); + GC.KeepAlive(token); } catch (FlurlHttpException ex) when (ex.StatusCode == 401) { @@ -132,208 +133,204 @@ public Task DisconnectAsync() private async Task UpdateMapData(IMap map, CancellationToken cancellationToken) { - if (!(IsSignedIn.Value || _settings.CheckEnableAltitudeAngel)) - { - MapFeatureCache.Clear(); - } - - if (!map.Enabled) - { - map.DeleteOverlay(MapOverlayName); - map.Invalidate(); - return; - } - try { - var area = map.GetViewArea().RoundExpand(4); - var sw = new Stopwatch(); - sw.Start(); - var mapData = await _apiClient.GetMapData(area, cancellationToken); - sw.Stop(); + if (!await _mapCacheLock.WaitAsync(TimeSpan.FromSeconds(1), cancellationToken)) return; + if (!(IsSignedIn.Value || _settings.CheckEnableAltitudeAngel)) + { + MapFeatureCache.Clear(); + } - foreach (var errorReason in mapData.ExcludedData.Select(e => e.ErrorReason).Distinct()) + if (!map.Enabled) { - var reasons = mapData.ExcludedData.Where(e => e.ErrorReason == errorReason).ToList(); - if (reasons.Count <= 0) continue; - string message; - switch (errorReason) + map.DeleteOverlay(MapOverlayName); + map.Invalidate(); + return; + } + + try + { + var area = map.GetViewArea().RoundExpand(4); + var sw = new Stopwatch(); + sw.Start(); + var mapData = await _apiClient.GetMapData(area, cancellationToken); + sw.Stop(); + + foreach (var errorReason in mapData.ExcludedData.Select(e => e.ErrorReason).Distinct()) { - case "QueryAreaTooLarge": - message = $"Zoom in to see {reasons.Select(d => d.Detail.DisplayName).AsReadableList()} from Altitude Angel."; - break; + var reasons = mapData.ExcludedData.Where(e => e.ErrorReason == errorReason).ToList(); + if (reasons.Count <= 0) continue; + string message; + switch (errorReason) + { + case "QueryAreaTooLarge": + message = $"Zoom in to see {reasons.Select(d => d.Detail.DisplayName).AsReadableList()} from Altitude Angel."; + break; - default: - message = $"Warning: {reasons.Select(d => d.Detail.DisplayName).AsReadableList()} have been excluded from the Altitude Angel data."; - break; + default: + message = $"Warning: {reasons.Select(d => d.Detail.DisplayName).AsReadableList()} have been excluded from the Altitude Angel data."; + break; + } + await _messagesService.AddMessageAsync(Message.ForInfo(errorReason, message, TimeSpan.FromSeconds(_settings.MapUpdateRefresh))); } - await _messagesService.AddMessageAsync(Message.ForInfo(errorReason, message, TimeSpan.FromSeconds(_settings.MapUpdateRefresh))); - } - mapData.Features.UpdateFilterInfo(FilterInfoDisplay); - _settings.MapFilters = FilterInfoDisplay; + mapData.Features.UpdateFilterInfo(FilterInfoDisplay); + _settings.MapFilters = FilterInfoDisplay; - await _messagesService.AddMessageAsync(Message.ForInfo( - "UpdateMapData", - $"Map area loaded {area.NorthEast.Latitude:F4}, {area.SouthWest.Latitude:F4}, {area.SouthWest.Longitude:F4}, {area.NorthEast.Longitude:F4} in {sw.Elapsed.TotalMilliseconds:N2}ms", - TimeSpan.FromSeconds(1))); + await _messagesService.AddMessageAsync(Message.ForInfo( + "UpdateMapData", + $"Map area loaded {area.NorthEast.Latitude:F4}, {area.SouthWest.Latitude:F4}, {area.SouthWest.Longitude:F4}, {area.NorthEast.Longitude:F4} in {sw.Elapsed.TotalMilliseconds:N2}ms", + TimeSpan.FromSeconds(1))); - // add all items to cache - MapFeatureCache.Clear(); - mapData.Features.ForEach(feature => MapFeatureCache[feature.Id] = feature); + // add all items to cache + MapFeatureCache.Clear(); + MapFeatureCache.AddRange(mapData.Features); - // Only get the features that are enabled by default, and have not been filtered out - ProcessFeatures(map, mapData.Features); + // Only get the features that are enabled by default, and have not been filtered out + ProcessFeatures(map); + } + catch (Exception ex) when (!(ex is FlurlHttpException) && !(ex.InnerException is TaskCanceledException)) + { + await _messagesService.AddMessageAsync(Message.ForError("UpdateMapData", "Failed to update map data.", ex)); + } } - catch (Exception ex) when (!(ex is FlurlHttpException) && !(ex.InnerException is TaskCanceledException)) + finally { - await _messagesService.AddMessageAsync(Message.ForError("UpdateMapData", "Failed to update map data.", ex)); + _mapCacheLock.Release(); } } public void ProcessAllFromCache(IMap map, bool resetFilters = false) { map.DeleteOverlay(MapOverlayName); - if (!(IsSignedIn || _settings.CheckEnableAltitudeAngel)) + try { - MapFeatureCache.Clear(); - } + _mapCacheLock.Wait(); + if (!(IsSignedIn || _settings.CheckEnableAltitudeAngel)) + { + MapFeatureCache.Clear(); + } - if (!map.Enabled) - { - map.DeleteOverlay(MapOverlayName); - map.Invalidate(); - return; - } + if (!map.Enabled) + { + map.DeleteOverlay(MapOverlayName); + map.Invalidate(); + return; + } + + if (resetFilters) + { + MapFeatureCache.UpdateFilterInfo(FilterInfoDisplay, true); + } - if (resetFilters) + ProcessFeatures(map); + } + finally { - MapFeatureCache.Values.UpdateFilterInfo(FilterInfoDisplay, true); + _mapCacheLock.Release(); } - - ProcessFeatures(map, MapFeatureCache.Values); map.Invalidate(); } - private void ProcessFeatures(IMap map, IEnumerable features) + private void ProcessFeatures(IMap map) { - try + var overlay = map.GetOverlay(MapOverlayName, true); + var overlayFeatures = new List(); + foreach (var feature in MapFeatureCache) { - if (!_processLock.Wait(TimeSpan.FromSeconds(1))) return; - var overlay = map.GetOverlay(MapOverlayName, true); - var polygons = new List(); - var lines = new List(); + if (!FilterInfoDisplay + .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) + .Any(i => i.Visible)) + { + continue; + } - foreach (var feature in features) + var properties = feature.GetFeatureProperties(); + if (properties.AltitudeFloor != null) { - if (!FilterInfoDisplay - .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) - .Any(i => i.Visible)) + // TODO: Ignoring datum for now + if (properties.AltitudeFloor.Meters > _settings.AltitudeFilter) { continue; } + } - var properties = feature.GetFeatureProperties(); - if (properties.AltitudeFloor != null) + switch (feature.Geometry.Type) + { + case GeoJSONObjectType.Point: { - // TODO: Ignoring datum for now - if (properties.AltitudeFloor.Meters > _settings.AltitudeFilter) + var pnt = (Point)feature.Geometry; + + var coordinates = new List(); + + if (!string.IsNullOrEmpty(properties.Radius)) { - continue; + var rad = double.Parse(properties.Radius); + for (var i = 0; i <= 360; i += 10) + { + coordinates.Add( + PositionFromBearingAndDistance(new LatLong(((Position)pnt.Coordinates).Latitude, + ((Position)pnt.Coordinates).Longitude), i, rad)); + } } + + var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Polygon, coordinates, colorInfo, feature)); + break; } - switch (feature.Geometry.Type) + case GeoJSONObjectType.LineString: { - case GeoJSONObjectType.Point: - { - var pnt = (Point)feature.Geometry; - - var coordinates = new List(); - - if (!string.IsNullOrEmpty(properties.Radius)) - { - var rad = double.Parse(properties.Radius); - for (var i = 0; i <= 360; i += 10) - { - coordinates.Add( - PositionFromBearingAndDistance(new LatLong(((Position)pnt.Coordinates).Latitude, - ((Position)pnt.Coordinates).Longitude), i, rad)); - } - } + var line = (LineString)feature.Geometry; + var coordinates = line.Coordinates.OfType() + .Select(c => new LatLong(c.Latitude, c.Longitude)) + .ToList(); + var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Line, coordinates, colorInfo, feature)); + break; + } - var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); - overlay.AddOrUpdatePolygon(feature.Id, coordinates, colorInfo, feature); - polygons.Add(feature.Id); - } - break; - case GeoJSONObjectType.MultiPoint: - break; - case GeoJSONObjectType.LineString: - { - var line = (LineString)feature.Geometry; - var coordinates = line.Coordinates.OfType() + case GeoJSONObjectType.Polygon: + { + var poly = (Polygon)feature.Geometry; + var coordinates = + poly.Coordinates[0].Coordinates.OfType() .Select(c => new LatLong(c.Latitude, c.Longitude)) .ToList(); - var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); - overlay.AddOrUpdateLine(feature.Id, coordinates, colorInfo, feature); - lines.Add(feature.Id); - } - break; - case GeoJSONObjectType.MultiLineString: - break; - case GeoJSONObjectType.Polygon: + var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Polygon, coordinates, colorInfo, feature)); + break; + } + + case GeoJSONObjectType.MultiPolygon: + // TODO: This does not work for polygons with holes and just does the outer polygon + foreach (var poly in ((MultiPolygon)feature.Geometry).Coordinates) { - var poly = (Polygon)feature.Geometry; var coordinates = poly.Coordinates[0].Coordinates.OfType() .Select(c => new LatLong(c.Latitude, c.Longitude)) .ToList(); var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); - overlay.AddOrUpdatePolygon(feature.Id, coordinates, colorInfo, feature); - polygons.Add(feature.Id); + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Line, coordinates, colorInfo, feature)); } - break; - case GeoJSONObjectType.MultiPolygon: - foreach (var poly in ((MultiPolygon)feature.Geometry).Coordinates) - { - var coordinates = - poly.Coordinates[0].Coordinates.OfType() - .Select(c => new LatLong(c.Latitude, c.Longitude)) - .ToList(); - - var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); - overlay.AddOrUpdatePolygon(feature.Id, coordinates, colorInfo, feature); - polygons.Add(feature.Id); - } - break; - case GeoJSONObjectType.GeometryCollection: - break; - case GeoJSONObjectType.Feature: - break; - case GeoJSONObjectType.FeatureCollection: - break; - default: - throw new ArgumentOutOfRangeException(); - } - } + break; - overlay.RemovePolygonsExcept(polygons); - overlay.RemoveLinesExcept(lines); - } - finally - { - _processLock.Release(); + default: + _messagesService.AddMessageAsync(Message.ForInfo($"GeoJSON object type {feature.Geometry.Type} cannot be handled in feature ID {feature.Id}.")); + break; + } } + + overlay.SetFeatures(overlayFeatures); } private static LatLong PositionFromBearingAndDistance(LatLong input, double bearing, double distance) { - const double rad2deg = 180 / Math.PI; - const double deg2rad = 1.0 / rad2deg; + const double radiansInDegrees = 180 / Math.PI; + const double degreesInRadians = 1.0 / radiansInDegrees; // '''extrapolate latitude/longitude given a heading and distance // thanks to http://www.movable-type.co.uk/scripts/latlong.html @@ -341,17 +338,17 @@ private static LatLong PositionFromBearingAndDistance(LatLong input, double bear // from math import sin, asin, cos, atan2, radians, degrees var radiusOfEarth = 6378100.0;//# in meters - var lat1 = deg2rad * input.Latitude; - var lon1 = deg2rad * input.Longitude; - var brng = deg2rad * bearing; + var lat1 = degreesInRadians * input.Latitude; + var lon1 = degreesInRadians * input.Longitude; + var radBearing = degreesInRadians * bearing; var dr = distance / radiusOfEarth; var lat2 = Math.Asin(Math.Sin(lat1) * Math.Cos(dr) + - Math.Cos(lat1) * Math.Sin(dr) * Math.Cos(brng)); - var lon2 = lon1 + Math.Atan2(Math.Sin(brng) * Math.Sin(dr) * Math.Cos(lat1), + Math.Cos(lat1) * Math.Sin(dr) * Math.Cos(radBearing)); + var lon2 = lon1 + Math.Atan2(Math.Sin(radBearing) * Math.Sin(dr) * Math.Cos(lat1), Math.Cos(dr) - Math.Sin(lat1) * Math.Sin(lat2)); - return new LatLong(rad2deg * lat2, rad2deg * lon2); + return new LatLong(radiansInDegrees * lat2, radiansInDegrees * lon2); } private async Task OnFlightReportClicked(Feature feature) @@ -364,7 +361,7 @@ private async Task OnFlightReportClicked(Feature feature) if (_settings.CurrentFlightPlanId == null && _settings.ExistingFlightPlanId != Guid.Parse(feature.Id)) { if (!await _missionPlanner.ShowYesNoMessageBox( - $"You have clicked your flight plan '{feature.GetDisplayInfo().Title}'.{Environment.NewLine}Would you like to use this flight plan when you arm your drone?", + $"You have clicked your flight plan '{feature.GetFeatureProperties().DisplayInfo.Title}'.{Environment.NewLine}Would you like to use this flight plan when you arm your drone?", "Flight Plan")) return; _settings.ExistingFlightPlanId = Guid.Parse(feature.Id); _settings.UseExistingFlightPlanId = true; @@ -385,6 +382,8 @@ private void Dispose(bool isDisposing) _telemetryService.Dispose(); _flightService.Dispose(); _disposer?.Dispose(); + _signInLock?.Dispose(); + _mapCacheLock?.Dispose(); } } }