From 103322be33feaed5c5bc364aeac0b6e9fac8904b Mon Sep 17 00:00:00 2001 From: Vrabbers <25323861+Vrabbers@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:30:40 -0300 Subject: [PATCH] Bins --- .editorconfig | 115 ------------------ BnbnavNetClient/Models/IntRect.cs | 10 ++ BnbnavNetClient/Models/MapBin.cs | 102 ++++++++++++++++ .../EditControllers/NodeJoinEditController.cs | 3 +- BnbnavNetClient/Services/MapService.cs | 29 +++-- BnbnavNetClient/Views/MainView.axaml.cs | 16 ++- BnbnavNetClient/Views/MapView.axaml.cs | 67 +++++----- Directory.Build.props | 2 +- 8 files changed, 176 insertions(+), 168 deletions(-) delete mode 100644 .editorconfig create mode 100644 BnbnavNetClient/Models/IntRect.cs create mode 100644 BnbnavNetClient/Models/MapBin.cs diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e54d734..0000000 --- a/.editorconfig +++ /dev/null @@ -1,115 +0,0 @@ - -[*.{cs,vb}] -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_style_operator_placement_when_wrapping = end_of_line -tab_width = 4 -indent_size = 4 -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_readonly_field = true:suggestion -dotnet_style_predefined_type_for_member_access = true:silent -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion -dotnet_style_allow_multiple_blank_lines_experimental = true:silent -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent -dotnet_code_quality_unused_parameters = all:suggestion - -[*.cs] -csharp_indent_labels = one_less_than_current -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = when_multiline:silent -csharp_style_namespace_declarations = file_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_expression_bodied_methods = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = true:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = true:silent -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent -csharp_style_prefer_readonly_struct = true:suggestion -csharp_prefer_static_local_function = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_conditional_delegate_call = true:suggestion - -# ReSharper properties -resharper_default_private_modifier = implicit - -# ReSharper inspection severities -resharper_arrange_type_member_modifiers_highlighting = suggestion diff --git a/BnbnavNetClient/Models/IntRect.cs b/BnbnavNetClient/Models/IntRect.cs new file mode 100644 index 0000000..c327594 --- /dev/null +++ b/BnbnavNetClient/Models/IntRect.cs @@ -0,0 +1,10 @@ +namespace BnbnavNetClient.Models; + +public readonly record struct IntRect(int Left, int Top, int Right, int Bottom) +{ + public bool Contains(int x, int y) => + Left <= x && x <= Right && Top <= y && y <= Bottom; + + public IntRect Expand(int amt) => + new(Left - amt, Top - amt, Right + amt, Bottom + amt); +} \ No newline at end of file diff --git a/BnbnavNetClient/Models/MapBin.cs b/BnbnavNetClient/Models/MapBin.cs new file mode 100644 index 0000000..891b125 --- /dev/null +++ b/BnbnavNetClient/Models/MapBin.cs @@ -0,0 +1,102 @@ +using Avalonia; +using DynamicData; + +namespace BnbnavNetClient.Models; + +public sealed class MapBins +{ + public class Bin + { + public List Nodes { get; } = []; + public List Edges { get; } = []; + } + public const int BinSideLength = 256; + + private readonly Bin?[,] _bins; + + private int BinsXLength => _bins.GetLength(0); + private int BinsYLength => _bins.GetLength(1); + + public IntRect Bounds { get; private set; } + + public MapBins(IntRect bounds, IEnumerable nodes, IEnumerable edges) + { + Bounds = bounds.Expand(5); + var xLength = Bounds.Right - Bounds.Left; + var yLength = Bounds.Bottom - Bounds.Top; + var xNumBins = xLength / BinSideLength; + var yNumBins = yLength / BinSideLength; + _bins = new Bin?[xNumBins + 1, yNumBins + 1]; + + foreach (var node in nodes) + { + InsertNode(node); + } + + foreach (var edge in edges) + { + Insert(edge); + } + } + + public void InsertNode(Node node) + { + if (!Bounds.Contains(node.X, node.Z)) + throw new NotImplementedException(); + + var x = node.X - Bounds.Left; + var y = node.Z - Bounds.Top; + var binX = x / BinSideLength; + var binY = y / BinSideLength; + + ref var bin = ref _bins[binX, binY]; + bin ??= new Bin(); + bin.Nodes.Add(node); + } + + public void Insert(Edge edge) + { + var minX = int.Min(edge.From.X, edge.To.X); + var minY = int.Min(edge.From.Z, edge.To.Z); + var maxX = int.Max(edge.From.X, edge.To.X); + var maxY = int.Max(edge.From.Z, edge.To.Z); + var edgeBounds = new IntRect(minX, minY, maxX, maxY); + var expandedBounds = edgeBounds.Expand(5); + var startX = (expandedBounds.Left - Bounds.Left) / BinSideLength; + var startY = (expandedBounds.Top - Bounds.Top) / BinSideLength; + var endX = (expandedBounds.Right - Bounds.Left) / BinSideLength; + var endY = (expandedBounds.Top - Bounds.Top) / BinSideLength; + + for (var i = startX; i <= endX; i++) + { + for (var j = startY; j <= endY; j++) + { + ref var bin = ref _bins[i, j]; + bin ??= new Bin(); + bin.Edges.Add(edge); + } + } + } + + public void Query(IntRect rect, ref List nodes, ref List edges) + { + var startX = (rect.Left - Bounds.Left - BinSideLength / 2) / BinSideLength; + var startY = (rect.Top - Bounds.Top - BinSideLength / 2) / BinSideLength; + var endX = (rect.Right - Bounds.Left + BinSideLength / 2) / BinSideLength; + var endY = (rect.Bottom - Bounds.Top + BinSideLength / 2) / BinSideLength; + + for (var i = startX; i <= endX; i++) + { + for (var j = startY; j <= endY; j++) + { + ref var bin = ref _bins[i, j]; + if (bin is null) + continue; + foreach (var node in bin.Nodes) + nodes.Add(node); + foreach (var edge in bin.Edges) + edges.Add(edge); + } + } + } +} \ No newline at end of file diff --git a/BnbnavNetClient/Services/EditControllers/NodeJoinEditController.cs b/BnbnavNetClient/Services/EditControllers/NodeJoinEditController.cs index 8572e09..4389090 100644 --- a/BnbnavNetClient/Services/EditControllers/NodeJoinEditController.cs +++ b/BnbnavNetClient/Services/EditControllers/NodeJoinEditController.cs @@ -196,8 +196,9 @@ public override void Render(MapView mapView, DrawingContext context) } var nodeBorder = (Pen)mapView.ThemeDict["NodeBorder"]!; var selNodeBrush = (Brush)mapView.ThemeDict["SelectedNodeFill"]!; - foreach (var (rect, _) in mapView.DrawnNodes.Where(x => ghosts.Contains(x.Item2))) + foreach (var node in mapView.DrawnNodes.Where(x => ghosts.Contains(x))) { + var rect = node.BoundingRect(mapView); context.DrawRectangle(selNodeBrush, nodeBorder, rect); } } diff --git a/BnbnavNetClient/Services/MapService.cs b/BnbnavNetClient/Services/MapService.cs index 224a3e5..7fd843b 100644 --- a/BnbnavNetClient/Services/MapService.cs +++ b/BnbnavNetClient/Services/MapService.cs @@ -79,6 +79,8 @@ public enum RouteOptions public ReadOnlyDictionary Players { get; } public bool PlayerGone { get; set; } + public MapBins MapBins { get; } + [Reactive] public AvaloniaList Worlds { get; private set; } = []; @@ -108,7 +110,7 @@ public static string? AuthenticationToken } } - MapService(IEnumerable nodes, IEnumerable edges, IEnumerable roads, IEnumerable landmarks, IEnumerable annotations, BnbnavWebsocketService websocketService) + MapService(IEnumerable nodes, IEnumerable edges, IEnumerable roads, IEnumerable landmarks, IEnumerable annotations, BnbnavWebsocketService websocketService, MapBins bins) { _nodes = new Dictionary(nodes.ToDictionary(n => n.Id)); @@ -125,6 +127,7 @@ public static string? AuthenticationToken _websocketService = websocketService; _i18N = Locator.Current.GetI18Next(); + MapBins = bins; this.WhenAnyValue(x => x.LoggedInUsername).Subscribe(Observer.Create(_ => UpdateLoggedInPlayer())); this.WhenPropertyChanged(x => x.Players) @@ -423,17 +426,18 @@ IEnumerable GenerateTemporaryEdgesFromPointToEdge(Node point, Edge edge, b public static async Task DownloadInitialMapAsync() { - var content = await HttpClient.GetStringAsync("/api/data"); - using var jsonDom = JsonDocument.Parse(content); - - if (jsonDom is null) - throw new InvalidOperationException("Error in JSON document."); + var stream = await HttpClient.GetStreamAsync("/api/data"); + using var jsonDom = await JsonDocument.ParseAsync(stream); //TODO: Gracefully fail if there is no such property - this might be a new server w/o landmarks, nodes, etc. - + var root = jsonDom.RootElement; var jsonNodes = root.GetProperty("nodes"u8); var nodes = new Dictionary(); + var minX = int.MaxValue; + var minY = int.MaxValue; + var maxX = int.MinValue; + var maxY = int.MinValue; foreach (var jsonNode in jsonNodes.EnumerateObject()) { var id = jsonNode.Name; @@ -443,6 +447,10 @@ public static async Task DownloadInitialMapAsync() var z = obj.GetProperty("z"u8).GetInt32(); var world = obj.GetProperty("world"u8).GetString()!; nodes.Add(id, new Node(id, x, y, z, world)); + minX = int.Min(minX, x); + minY = int.Min(minY, z); + maxX = int.Max(maxX, x); + maxY = int.Max(maxY, z); } var jsonLandmarks = root.GetProperty("landmarks"u8); @@ -488,10 +496,13 @@ public static async Task DownloadInitialMapAsync() var obj = jsonAnnotation.Value; annotations.Add(new Annotation(id, obj.Clone())); } - + + var bounds = new IntRect(minX, minY, maxX, maxY); + var bins = new MapBins(bounds, nodes.Values, edges); + var ws = new BnbnavWebsocketService(); await ws.ConnectAsync(CancellationToken.None); - var service = new MapService(nodes.Values, edges, roads.Values, landmarks, annotations, ws); + var service = new MapService(nodes.Values, edges, roads.Values, landmarks, annotations, ws, bins); _ = service.ProcessChangesAsync(); service.SetupWorlds(); return service; diff --git a/BnbnavNetClient/Views/MainView.axaml.cs b/BnbnavNetClient/Views/MainView.axaml.cs index 82c5f13..886951e 100644 --- a/BnbnavNetClient/Views/MainView.axaml.cs +++ b/BnbnavNetClient/Views/MainView.axaml.cs @@ -42,16 +42,14 @@ public async void ViewLoaded(object sender, RoutedEventArgs e) vm.RaisePropertyChanged(nameof(MainViewModel.PanText)); // c.f. issue #32 for why we disable the blur effect on windows - if (!OperatingSystem.IsWindows()) + vm.WhenAnyValue(x => x.Popup).Subscribe(p => { - vm.WhenAnyValue(x => x.Popup).Subscribe(p => - { - if (p is null) - MainUiGrid.Classes.Clear(); - else - MainUiGrid.Classes.Add("blur"); - }); - } + if (p is null) + MainUiGrid.Classes.Clear(); + else + MainUiGrid.Classes.Add("blur"); + }); + } public async void ColorModeSwitch(object? _, RoutedEventArgs? __) diff --git a/BnbnavNetClient/Views/MapView.axaml.cs b/BnbnavNetClient/Views/MapView.axaml.cs index 8431eab..b535a23 100644 --- a/BnbnavNetClient/Views/MapView.axaml.cs +++ b/BnbnavNetClient/Views/MapView.axaml.cs @@ -366,11 +366,16 @@ void UpdateContextMenuItems() } - readonly IAvaloniaI18Next _i18N; + private readonly IAvaloniaI18Next _i18N; + private List _drawnEdges = []; + private List _drawnNodes = []; + + private List DrawnEdges => _drawnEdges; + + private List<(Rect, Landmark)> DrawnLandmarks { get; set; } = []; + + public List DrawnNodes => _drawnNodes; - List<(Point, Point, Edge)> DrawnEdges { get; set; } = []; - List<(Rect, Landmark)> DrawnLandmarks { get; set; } = []; - public List<(Rect, Node)> DrawnNodes { get; set; } = []; public List SpiedNodes { get; set; } = []; void UpdateFollowMeState() @@ -402,13 +407,16 @@ public IEnumerable HitTest(Point point) if (rect.Contains(point)) yield return landmark; } - foreach (var (rect, node) in DrawnNodes) + foreach (var node in DrawnNodes) { - if (rect.Contains(point)) yield return node; + var rect = node.BoundingRect(this); + if (rect.Contains(point)) + yield return node; } - foreach (var (a, b, edge) in DrawnEdges) + foreach (var edge in DrawnEdges) { + var (a, b) = edge.Extents(this); if (GeometryHelper.LineSegmentToPointDistance(a, b, point) <= ThicknessForRoadType(edge.Road.RoadType) * MapViewModel.Scale / 2) yield return edge; } @@ -430,34 +438,24 @@ void UpdateDrawnItems(Rect? boundsRect = null) } var bounds = boundsRect ?? Bounds; - - var drawnEdgesEnumerable = mapService.AllEdges - .Where(edge => edge.From.World == edge.To.World && edge.To.World == MapViewModel.ChosenWorld).Select(edge => - { - var (from, to) = edge.Extents(this); - return (from, to, edge); - }).Where(edge => GeometryHelper.LineIntersects(edge.from, edge.to, bounds)); - // This avoids re-allocating the whole entire drawn edge list, instead using the same one as before. - DrawnEdges.Clear(); - foreach (var edge in drawnEdgesEnumerable) - { - DrawnEdges.Add(edge); - } + _drawnEdges.Clear(); + _drawnNodes.Clear(); + + var worldTl = ToWorld(bounds.TopLeft); + var worldBr = ToWorld(bounds.BottomRight); - DrawnLandmarks = mapService.Landmarks.Values.Where(landmark => landmark.Node.World == MapViewModel.ChosenWorld).Select(landmark => (landmark.BoundingRect(this), landmark)) - .Where(landmark => bounds.Intersects(landmark.Item1)).ToList(); + var intBounds = new IntRect((int)double.Floor(worldTl.X), (int)double.Floor(worldTl.Y), + (int)double.Ceiling(worldBr.X), (int)double.Ceiling(worldBr.Y)); - DrawnNodes = mapService.Nodes.Values.Where(node => node.World == MapViewModel.ChosenWorld).Select(node => (node.BoundingRect(this), node)) - .Where(node => bounds.Intersects(node.Item1)).ToList(); + mapService.MapBins.Query(intBounds, ref _drawnNodes, ref _drawnEdges); - SpiedNodes = DrawnNodes.Select(nodeInfo => nodeInfo.Item2).Where(node => + DrawnLandmarks = mapService.Landmarks.Values.Where(landmark => landmark.Node.World == MapViewModel.ChosenWorld).Select(landmark => (landmark.BoundingRect(this), landmark)) + .Where(landmark => bounds.Intersects(landmark.Item1)).ToList(); + + SpiedNodes = DrawnNodes.Where(node => { - if (MapViewModel.HighlightInterWorldNodesEnabled) - { - return mapService.AllEdges.Where(edge => edge.From.Id == node.Id || edge.To.Id == node.Id).Any(edge => edge.From.World != edge.To.World); - } - return false; + return MapViewModel.HighlightInterWorldNodesEnabled && mapService.AllEdges.Where(edge => edge.From.Id == node.Id || edge.To.Id == node.Id).Any(edge => edge.From.World != edge.To.World); }).ToList(); InvalidateVisual(); @@ -562,9 +560,11 @@ public override void Render(DrawingContext context) } - foreach (var (from, to, edge) in DrawnEdges) + foreach (var edge in DrawnEdges) { - if (noRender.Contains(edge)) continue; + if (noRender.Contains(edge)) + continue; + var (from, to) = edge.Extents(this); DrawEdge(context, edge.Road.RoadType, from, to, drawRoute: MapViewModel.MapService.CurrentRoute?.Edges.Contains(edge) ?? false); } @@ -601,8 +601,9 @@ public override void Render(DrawingContext context) var nodeBrush = (Brush)ThemeDict["NodeFill"]!; var spiedBorder = (Pen)ThemeDict["SpiedNodeBorder"]!; var spiedBrush = (Brush)ThemeDict["SpiedNodeFill"]!; - foreach (var (rect, node) in DrawnNodes) + foreach (var node in DrawnNodes) { + var rect = node.BoundingRect(this); if (noRender.Contains(node)) continue; if (SpiedNodes.Any(spied => spied.Id == node.Id)) diff --git a/Directory.Build.props b/Directory.Build.props index b37fb29..de9b0b2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,6 @@ enable enable - 11.0.7 + 11.0.10