From 2f12d62e6f6cc1b628acc1a8416f44aea82aa34a Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Wed, 30 Aug 2023 22:03:28 +0100 Subject: [PATCH 1/9] Overlay features added in batch --- .../OverlayAdapter.cs | 140 ++++++++---------- ExtLibs/AltitudeAngelWings/IOverlay.cs | 10 +- ExtLibs/AltitudeAngelWings/OverlayFeature.cs | 24 +++ .../AltitudeAngelWings/OverlayFeatureType.cs | 8 + .../Service/AltitudeAngelService.cs | 66 ++++----- 5 files changed, 125 insertions(+), 123 deletions(-) create mode 100644 ExtLibs/AltitudeAngelWings/OverlayFeature.cs create mode 100644 ExtLibs/AltitudeAngelWings/OverlayFeatureType.cs 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/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..63596786c1 100644 --- a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs +++ b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs @@ -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) { @@ -183,7 +184,7 @@ await _messagesService.AddMessageAsync(Message.ForInfo( mapData.Features.ForEach(feature => MapFeatureCache[feature.Id] = feature); // Only get the features that are enabled by default, and have not been filtered out - ProcessFeatures(map, mapData.Features); + ProcessFeatures(map); } catch (Exception ex) when (!(ex is FlurlHttpException) && !(ex.InnerException is TaskCanceledException)) { @@ -211,19 +212,20 @@ public void ProcessAllFromCache(IMap map, bool resetFilters = false) MapFeatureCache.Values.UpdateFilterInfo(FilterInfoDisplay, true); } - ProcessFeatures(map, MapFeatureCache.Values); + ProcessFeatures(map); map.Invalidate(); } - private void ProcessFeatures(IMap map, IEnumerable features) + private void ProcessFeatures(IMap map) { try { if (!_processLock.Wait(TimeSpan.FromSeconds(1))) return; var overlay = map.GetOverlay(MapOverlayName, true); - var polygons = new List(); - var lines = new List(); + var features = new Feature[MapFeatureCache.Values.Count]; + MapFeatureCache.Values.CopyTo(features, 0); + var overlayFeatures = new List(); foreach (var feature in features) { if (!FilterInfoDisplay @@ -263,12 +265,10 @@ private void ProcessFeatures(IMap map, IEnumerable features) } var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); - overlay.AddOrUpdatePolygon(feature.Id, coordinates, colorInfo, feature); - polygons.Add(feature.Id); - } - break; - case GeoJSONObjectType.MultiPoint: + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Polygon, coordinates, colorInfo, feature)); break; + } + case GeoJSONObjectType.LineString: { var line = (LineString)feature.Geometry; @@ -276,13 +276,10 @@ private void ProcessFeatures(IMap map, IEnumerable features) .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); - } + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Line, coordinates, colorInfo, feature)); break; + } - case GeoJSONObjectType.MultiLineString: - break; case GeoJSONObjectType.Polygon: { var poly = (Polygon)feature.Geometry; @@ -292,11 +289,12 @@ private void ProcessFeatures(IMap map, IEnumerable features) .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.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 coordinates = @@ -305,24 +303,18 @@ private void ProcessFeatures(IMap map, IEnumerable features) .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.GeometryCollection: - break; - case GeoJSONObjectType.Feature: - break; - case GeoJSONObjectType.FeatureCollection: - break; + default: - throw new ArgumentOutOfRangeException(); + _messagesService.AddMessageAsync(Message.ForInfo($"GeoJSON object type {feature.Geometry.Type} cannot be handled in feature ID {feature.Id}.")); + break; } } - overlay.RemovePolygonsExcept(polygons); - overlay.RemoveLinesExcept(lines); + overlay.SetFeatures(overlayFeatures); } finally { @@ -332,8 +324,8 @@ private void ProcessFeatures(IMap map, IEnumerable features) 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 +333,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) From c92b0a15828bda268c9cd143073878f289470182 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Wed, 30 Aug 2023 22:12:01 +0100 Subject: [PATCH 2/9] Remove redundant feature copy --- ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs index 63596786c1..4d5d2cfb33 100644 --- a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs +++ b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs @@ -222,11 +222,8 @@ private void ProcessFeatures(IMap map) { if (!_processLock.Wait(TimeSpan.FromSeconds(1))) return; var overlay = map.GetOverlay(MapOverlayName, true); - var features = new Feature[MapFeatureCache.Values.Count]; - MapFeatureCache.Values.CopyTo(features, 0); - var overlayFeatures = new List(); - foreach (var feature in features) + foreach (var feature in MapFeatureCache.Values) { if (!FilterInfoDisplay .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) From e3fd83bd841a94d96b67f731035c73582a00762f Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 10:05:51 +0100 Subject: [PATCH 3/9] Warnings as errors --- ExtLibs/AltitudeAngelWings/AltitudeAngelWings.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 + From 0eaea0f90f9f6ec738d814d4fee2a6ca472a21a8 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 10:06:05 +0100 Subject: [PATCH 4/9] Map cache as list --- .../AltitudeAngelWings/Service/AltitudeAngelService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs index 4d5d2cfb33..f2a05ddba3 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; @@ -181,7 +181,7 @@ await _messagesService.AddMessageAsync(Message.ForInfo( // add all items to cache MapFeatureCache.Clear(); - mapData.Features.ForEach(feature => MapFeatureCache[feature.Id] = feature); + MapFeatureCache.AddRange(mapData.Features); // Only get the features that are enabled by default, and have not been filtered out ProcessFeatures(map); @@ -209,7 +209,7 @@ public void ProcessAllFromCache(IMap map, bool resetFilters = false) if (resetFilters) { - MapFeatureCache.Values.UpdateFilterInfo(FilterInfoDisplay, true); + MapFeatureCache.UpdateFilterInfo(FilterInfoDisplay, true); } ProcessFeatures(map); @@ -223,7 +223,7 @@ private void ProcessFeatures(IMap map) if (!_processLock.Wait(TimeSpan.FromSeconds(1))) return; var overlay = map.GetOverlay(MapOverlayName, true); var overlayFeatures = new List(); - foreach (var feature in MapFeatureCache.Values) + foreach (var feature in MapFeatureCache) { if (!FilterInfoDisplay .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) From 94d5019eb3b90bbbd7b9688d823cf75ec3f93c3b Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 20:19:41 +0100 Subject: [PATCH 5/9] Remove unused --- .../AltitudeAngelWings.Plugin/MapAdapter.cs | 18 +++--------------- ExtLibs/AltitudeAngelWings/IMap.cs | 2 -- 2 files changed, 3 insertions(+), 17 deletions(-) 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/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; } From 2e2bbfd684b014523fee3c9cd445fce350d97661 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 20:27:27 +0100 Subject: [PATCH 6/9] Optimize memory allocations --- .../Clients/Api/Model/FeatureExtensions.cs | 10 ++-------- .../AltitudeAngelWings/Service/AltitudeAngelService.cs | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) 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/Service/AltitudeAngelService.cs b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs index f2a05ddba3..c57a7e8fcf 100644 --- a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs +++ b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs @@ -353,7 +353,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; From be053dd58ea8d5c4ed4eb21b11cd0e3599a7c555 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 21:05:30 +0100 Subject: [PATCH 7/9] Improve locking of map cache --- .../Service/AltitudeAngelService.cs | 266 +++++++++--------- 1 file changed, 138 insertions(+), 128 deletions(-) diff --git a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs index c57a7e8fcf..d315de8843 100644 --- a/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs +++ b/ExtLibs/AltitudeAngelWings/Service/AltitudeAngelService.cs @@ -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; } @@ -133,190 +133,198 @@ 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(); - MapFeatureCache.AddRange(mapData.Features); + // 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); + // 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) + if (resetFilters) + { + MapFeatureCache.UpdateFilterInfo(FilterInfoDisplay, true); + } + + ProcessFeatures(map); + } + finally { - MapFeatureCache.UpdateFilterInfo(FilterInfoDisplay, true); + _mapCacheLock.Release(); } - - ProcessFeatures(map); map.Invalidate(); } 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 overlayFeatures = new List(); - foreach (var feature in MapFeatureCache) + if (!FilterInfoDisplay + .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) + .Any(i => i.Visible)) { - if (!FilterInfoDisplay - .Intersect(feature.GetFilterInfo(), new FilterInfoDisplayEqualityComparer()) - .Any(i => i.Visible)) + continue; + } + + var properties = feature.GetFeatureProperties(); + if (properties.AltitudeFloor != null) + { + // 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) - { - continue; - } - } + var pnt = (Point)feature.Geometry; - switch (feature.Geometry.Type) - { - case GeoJSONObjectType.Point: - { - var pnt = (Point)feature.Geometry; + var coordinates = new List(); - var coordinates = new List(); - - if (!string.IsNullOrEmpty(properties.Radius)) + if (!string.IsNullOrEmpty(properties.Radius)) + { + var rad = double.Parse(properties.Radius); + for (var i = 0; i <= 360; i += 10) { - 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)); - } + 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; } - case GeoJSONObjectType.LineString: - { - var line = (LineString)feature.Geometry; - var coordinates = line.Coordinates.OfType() + var colorInfo = properties.ToColorInfo(_settings.MapOpacityAdjust); + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Polygon, coordinates, colorInfo, feature)); + break; + } + + case GeoJSONObjectType.LineString: + { + 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; + } + + 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); - overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Line, coordinates, colorInfo, feature)); - 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); - overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Polygon, coordinates, colorInfo, feature)); - break; + overlayFeatures.Add(new OverlayFeature(feature.Id, OverlayFeatureType.Line, coordinates, colorInfo, feature)); } - 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 coordinates = - poly.Coordinates[0].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; + break; - default: - _messagesService.AddMessageAsync(Message.ForInfo($"GeoJSON object type {feature.Geometry.Type} cannot be handled in feature ID {feature.Id}.")); - break; - } + default: + _messagesService.AddMessageAsync(Message.ForInfo($"GeoJSON object type {feature.Geometry.Type} cannot be handled in feature ID {feature.Id}.")); + break; } - - overlay.SetFeatures(overlayFeatures); - } - finally - { - _processLock.Release(); } + + overlay.SetFeatures(overlayFeatures); } private static LatLong PositionFromBearingAndDistance(LatLong input, double bearing, double distance) @@ -374,6 +382,8 @@ private void Dispose(bool isDisposing) _telemetryService.Dispose(); _flightService.Dispose(); _disposer?.Dispose(); + _signInLock?.Dispose(); + _mapCacheLock?.Dispose(); } } } From 398b4b1aa63fa7a8684c86c7906b0f3267940f95 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 21:33:38 +0100 Subject: [PATCH 8/9] Settings optimization --- ExtLibs/AltitudeAngelWings.Plugin/AASettings.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) From 46ce3ec1333ab5b7ce79253e43d3ba7c6315d259 Mon Sep 17 00:00:00 2001 From: Rupert Benbrook Date: Thu, 31 Aug 2023 21:46:45 +0100 Subject: [PATCH 9/9] Reducing feature properties deserialized to reduce memory usage --- .../Clients/Api/Model/FeatureProperties.cs | 60 ------------------- 1 file changed, 60 deletions(-) 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; }