Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
GeoBoxCollection.cs
1using System;
4using System.Diagnostics;
5using System.Text;
6using System.Xml;
8
9namespace Waher.Runtime.Geo
10{
14 public class GeoBoxCollection<T> : ICollection<T>
15 where T : IGeoBoundingBox
16 {
17 private const int DefaultGridSize = 8;
18 private const int DefaultMaxCellCount = 8;
19
20 private readonly Dictionary<string, BoxNode> boxesById = new Dictionary<string, BoxNode>();
21 private readonly object synchObj = new object();
22 private readonly int gridSize;
23 private readonly int maxCellCount;
24 private volatile int token = 0;
25 private int count = 0;
26 private BoxGrid root = null;
27
28 private class BoxNode
29 {
30 public T Box;
31 public ChunkedList<BoxListReference> References;
32 }
33
34 private class BoxListReference
35 {
36 public BoxGrid Grid;
37 public NormalizedBox Box;
38 public int X;
39 public int Y;
40 }
41
42 private class BoxGrid
43 {
44 public ChunkedList<NormalizedBox> EntireArea;
45 public Cell[,] Grid;
46 public readonly BoxGrid Parent;
47 public readonly NormalizedBox Box;
48 public readonly int Width;
49 public readonly int Height;
50 public readonly int MaxCellCount;
51 public readonly int X;
52 public readonly int Y;
53
54 public BoxGrid(BoxGrid Parent, NormalizedBox Box, int Width, int Height,
55 int MaxCellCount, int X, int Y)
56 {
57 this.Parent = Parent;
58 this.Box = Box;
59 this.Width = Width;
60 this.Height = Height;
61 this.MaxCellCount = MaxCellCount;
62 this.Grid = new Cell[this.Width, this.Height];
63 this.X = X;
64 this.Y = Y;
65 }
66
67 public void Fill(NormalizedBox Box)
68 {
69 if (this.EntireArea is null)
70 this.EntireArea = new ChunkedList<NormalizedBox>(1);
71
72 this.EntireArea.Add(Box);
73
74 if (Box.Node.References is null)
75 Box.Node.References = new ChunkedList<BoxListReference>(1);
76
77 Box.Node.References.Add(new BoxListReference()
78 {
79 Grid = this,
80 Box = Box,
81 X = -1,
82 Y = -1
83 });
84 }
85
86 public void Add(NormalizedBox Box)
87 {
88 if (Box.Contains(this.Box))
89 this.Fill(Box);
90 else
91 {
92 int ScaleX = this.Width;
93 int ScaleY = this.Height;
94 int Scale = this.Box.Scale;
95
96 if (Scale > 1)
97 {
98 ScaleX *= Scale;
99 ScaleY *= Scale;
100
101 ScaleX <<= 1;
102 }
103
104 int x0 = (int)((Box.MinLongitude - this.Box.MinLongitude) * ScaleX);
105 int x1 = (int)((Box.MaxLongitude - this.Box.MinLongitude) * ScaleX);
106 int y0 = (int)((Box.MinLatitude - this.Box.MinLatitude) * ScaleY);
107 int y1 = (int)((Box.MaxLatitude - this.Box.MinLatitude) * ScaleY);
108
109 if (x0 < 0)
110 x0 = 0;
111 else if (x0 >= this.Width)
112 x0 = this.Width - 1;
113
114 if (y0 < 0)
115 y0 = 0;
116 else if (y0 >= this.Height)
117 y0 = this.Height - 1;
118
119 if (x1 < 0)
120 x1 = 0;
121 else if (x1 >= this.Width)
122 x1 = this.Width - 1;
123
124 if (y1 < 0)
125 y1 = 0;
126 else if (y1 >= this.Height)
127 y1 = this.Height - 1;
128
129 if (Box.Node.References is null)
130 Box.Node.References = new ChunkedList<BoxListReference>((x1 - x0 + 1) * (y1 - y0 + 1));
131
132 for (int y = y0; y <= y1; y++)
133 {
134 for (int x = x0; x <= x1; x++)
135 this.AddCell(Box, x, y);
136 }
137 }
138 }
139
140 public void AddCell(NormalizedBox Box, int x, int y)
141 {
142 Cell Cell = this.Grid[x, y];
143
144 if (Cell is null)
145 {
146 this.Grid[x, y] = new Cell()
147 {
148 List = new ChunkedList<NormalizedBox>(1) { Box }
149 };
150
151 Box.Node.References.Add(new BoxListReference()
152 {
153 Grid = this,
154 Box = Box,
155 X = x,
156 Y = y
157 });
158 }
159 else if (!(Cell.Grid is null))
160 Cell.Grid.Add(Box);
161 else if (Cell.List.Count >= this.MaxCellCount && this.Box.CanIncreaseScale(this.Height))
162 {
163 double DeltaLat = this.Box.DeltaLatitude / this.Height;
164 double DeltaLong = this.Box.DeltaLongitude / this.Width;
165
166 Cell.Grid = new BoxGrid(this, new NormalizedBox(default)
167 {
168 MinLatitude = this.Box.MinLatitude + y * DeltaLat,
169 MaxLatitude = this.Box.MinLatitude + (y + 1) * DeltaLat,
170 MinLongitude = this.Box.MinLongitude + x * DeltaLong,
171 MaxLongitude = this.Box.MinLongitude + (x + 1) * DeltaLong,
172 Scale = this.Box.Scale * this.Height,
173 }, this.Height, this.Height, this.MaxCellCount, x, y); // Only first level grids have different widths and heights, deeper levels are square.
174
175 Cell.Grid.Add(Box);
176 }
177 else
178 {
179 Cell.List.Add(Box);
180
181 Box.Node.References.Add(new BoxListReference()
182 {
183 Grid = this,
184 Box = Box,
185 X = x,
186 Y = y
187 });
188 }
189 }
190
191 public bool Remove(BoxGrid Grid)
192 {
193 if (this.Grid is null)
194 return false;
195
196 Cell Cell = this.Grid[Grid.X, Grid.Y];
197 if (Cell is null)
198 return false;
199
200 if (!(Cell.Grid == Grid))
201 return false;
202
203 Cell.Grid = null;
204
205 if (Cell.List is null)
206 this.Grid[Grid.X, Grid.Y] = null;
207
208 return true;
209 }
210
211 public bool Remove(BoxListReference Reference)
212 {
213 if (Reference.X < 0 || Reference.Y < 0)
214 {
215 if (!this.EntireArea.Remove(Reference.Box))
216 return false;
217
218 if (this.EntireArea.Count == 0)
219 {
220 this.EntireArea = null;
221
222 for (int y = 0; y < this.Height; y++)
223 {
224 for (int x = 0; x < this.Width; x++)
225 {
226 if (!(this.Grid[x, y] is null))
227 return true;
228 }
229 }
230
231 this.Parent?.Remove(this);
232 }
233 }
234 else
235 {
236 Cell Cell = this.Grid[Reference.X, Reference.Y];
237 if (Cell is null)
238 return false;
239
240 if (!(Cell.List?.Remove(Reference.Box) ?? false))
241 return false;
242
243 if (Cell.List.Count == 0)
244 {
245 Cell.List = null;
246
247 if (Cell.Grid is null)
248 {
249 this.Grid[Reference.X, Reference.Y] = null;
250
251 if (this.EntireArea is null)
252 {
253 for (int y = 0; y < this.Height; y++)
254 {
255 for (int x = 0; x < this.Width; x++)
256 {
257 if (!(this.Grid[x, y] is null))
258 return true;
259 }
260 }
261
262 this.Parent?.Remove(this);
263 }
264 }
265 }
266 }
267
268 return true;
269 }
270
271 public void Find(GeoPosition Position, ref ChunkedList<T> Result)
272 {
273 if (!(this.EntireArea is null))
274 {
275 foreach (NormalizedBox Box in this.EntireArea)
276 {
277 if (Box.Node.Box.Contains(Position))
278 {
279 if (Result is null)
280 Result = new ChunkedList<T>();
281
282 Result.Add(Box.Node.Box);
283 }
284 }
285 }
286
287 int ScaleX = this.Width;
288 int ScaleY = this.Height;
289 int Scale = this.Box.Scale;
290
291 if (Scale > 1)
292 {
293 ScaleX *= Scale;
294 ScaleY *= Scale;
295
296 ScaleX <<= 1;
297 }
298
299 int x = (int)((Position.NormalizedLongitude - this.Box.MinLongitude) * ScaleX);
300 int y = (int)((Position.NormalizedLatitude - this.Box.MinLatitude) * ScaleY);
301
302 if (x >= 0 && x < this.Width && y >= 0 && y < this.Height)
303 {
304 Cell Cell = this.Grid[x, y];
305
306 if (!(Cell is null))
307 {
308 if (!(Cell.List is null))
309 {
310 foreach (NormalizedBox Box in Cell.List)
311 {
312 if (Box.Node.Box.Contains(Position))
313 {
314 if (Result is null)
315 Result = new ChunkedList<T>();
316
317 Result.Add(Box.Node.Box);
318 }
319 }
320 }
321
322 Cell.Grid?.Find(Position, ref Result);
323 }
324 }
325 }
326
327 public void Export(XmlWriter Output)
328 {
329 Output.WriteStartElement("Grid");
330 Output.WriteAttributeString("width", this.Width.ToString());
331 Output.WriteAttributeString("height", this.Height.ToString());
332
333 this.Box.Export(Output, "GridNBox");
334
335 if (!(this.EntireArea is null))
336 {
337 foreach (NormalizedBox Box in this.EntireArea)
338 Box.Export(Output, "FullNBox");
339 }
340
341 for (int y = 0; y < this.Height; y++)
342 {
343 for (int x = 0; x < this.Width; x++)
344 this.Grid[x, y]?.Export(Output, x, y);
345 }
346
347 Output.WriteEndElement();
348 }
349 }
350
351 private class Cell
352 {
353 public ChunkedList<NormalizedBox> List;
354 public BoxGrid Grid;
355
356 public void Export(XmlWriter Output, int X, int Y)
357 {
358 Output.WriteStartElement("Cell");
359 Output.WriteAttributeString("x", X.ToString());
360 Output.WriteAttributeString("y", Y.ToString());
361
362 if (!(this.List is null))
363 {
364 foreach (NormalizedBox Box in this.List)
365 Box.Export(Output, "NBoxRef");
366 }
367
368 this.Grid?.Export(Output);
369 Output.WriteEndElement();
370 }
371 }
372
373 [DebuggerDisplay("{MinLatitude},{MinLongitude} - {MaxLatitude},{MaxLongitude}")]
374 private class NormalizedBox
375 {
376 public NormalizedBox(BoxNode Node)
377 {
378 this.Node = Node;
379 }
380
381 public BoxNode Node;
382 public double MinLatitude;
383 public double MaxLatitude;
384 public double MinLongitude;
385 public double MaxLongitude;
386 public int Scale;
387
388 public double DeltaLatitude => this.MaxLatitude - this.MinLatitude;
389 public double DeltaLongitude => this.MaxLongitude - this.MinLongitude;
390
391 public bool Contains(NormalizedBox Box)
392 {
393 return
394 Box.MinLatitude >= this.MinLatitude &&
395 Box.MinLongitude >= this.MinLongitude &&
396 Box.MaxLatitude <= this.MaxLatitude &&
397 Box.MaxLongitude <= this.MaxLongitude;
398 }
399
400 public bool CanIncreaseScale(int Factor)
401 {
402 long x = this.Scale;
403 x *= Factor;
404 x <<= 1;
405
406 return ((int)x) == x;
407 }
408
409 public void Export(XmlWriter Output, string ElementName)
410 {
411 Output.WriteStartElement(ElementName);
412 Output.WriteAttributeString("minLat", GeoPosition.ToString(this.MinLatitude));
413 Output.WriteAttributeString("maxLat", GeoPosition.ToString(this.MaxLatitude));
414 Output.WriteAttributeString("minLong", GeoPosition.ToString(this.MinLongitude));
415 Output.WriteAttributeString("maxLong", GeoPosition.ToString(this.MaxLongitude));
416 Output.WriteAttributeString("scale", this.Scale.ToString());
417
418 if (!(this.Node is null))
419 {
420 Output.WriteAttributeString("boxId", this.Node.Box.BoxId);
421
422 Output.WriteStartElement("Box");
423 Output.WriteAttributeString("minLat", GeoPosition.ToString(this.Node.Box.Min.Latitude));
424 Output.WriteAttributeString("maxLat", GeoPosition.ToString(this.Node.Box.Max.Latitude));
425 Output.WriteAttributeString("minLong", GeoPosition.ToString(this.Node.Box.Min.Longitude));
426 Output.WriteAttributeString("maxLong", GeoPosition.ToString(this.Node.Box.Max.Longitude));
427 Output.WriteAttributeString("minIncl", GeoPosition.ToString(this.Node.Box.IncludeMin));
428 Output.WriteAttributeString("maxIncl", GeoPosition.ToString(this.Node.Box.IncludeMax));
429 Output.WriteEndElement();
430 }
431
432 Output.WriteEndElement();
433 }
434 }
435
440 : this(DefaultGridSize, DefaultMaxCellCount)
441 {
442 }
443
451 public GeoBoxCollection(int GridSize, int MaxCellCount)
452 {
453 this.gridSize = GridSize;
454 this.maxCellCount = MaxCellCount;
455 this.Clear();
456 }
457
461 public int Count
462 {
463 get
464 {
465 lock (this.synchObj)
466 {
467 return this.count;
468 }
469 }
470 }
471
475 public bool IsReadOnly => false;
476
481 public void Add(T Box)
482 {
483 lock (this.synchObj)
484 {
485 this.AddLocked(Box);
486 }
487 }
488
489 private void AddLocked(T Box)
490 {
491 if (this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
492 {
493 if (Node.Box.Equals(Box))
494 return;
495 else
496 throw new ArgumentException("An bounding box with the same ID already exists in the collection.", nameof(Box));
497 }
498
499 Node = new BoxNode()
500 {
501 Box = Box
502 };
503 this.boxesById[Box.BoxId] = Node;
504
505 if (Box.LongitudeWrapped)
506 {
507 double MinLat = Box.Min.NormalizedLatitude;
508 double MaxLat = Box.Max.NormalizedLatitude;
509
510 this.root.Add(new NormalizedBox(Node)
511 {
512 MinLatitude = MinLat,
513 MaxLatitude = MaxLat,
514 MinLongitude = Box.Min.NormalizedLongitude,
515 MaxLongitude = 1,
516 Scale = 1
517 });
518
519 this.root.Add(new NormalizedBox(Node)
520 {
521 MinLatitude = MinLat,
522 MaxLatitude = MaxLat,
523 MinLongitude = 0,
524 MaxLongitude = Box.Max.NormalizedLongitude,
525 Scale = 1
526 });
527 }
528 else
529 {
530 this.root.Add(new NormalizedBox(Node)
531 {
532 MinLatitude = Box.Min.NormalizedLatitude,
533 MaxLatitude = Box.Max.NormalizedLatitude,
534 MinLongitude = Box.Min.NormalizedLongitude,
535 MaxLongitude = Box.Max.NormalizedLongitude,
536 Scale = 1
537 });
538 }
539
540 this.token++;
541 this.count++;
542 }
543
547 public void Clear()
548 {
549 lock (this.synchObj)
550 {
551 this.root = new BoxGrid(null, new NormalizedBox(default)
552 {
553 MinLatitude = 0,
554 MaxLatitude = 1,
555 MinLongitude = 0,
556 MaxLongitude = 1,
557 Scale = 1
558 }, 2 * this.gridSize, this.gridSize, this.maxCellCount, 0, 0);
559
560 this.boxesById.Clear();
561 this.count = 0;
562 this.token++;
563 }
564 }
565
571 public bool Contains(T Box)
572 {
573 lock (this.synchObj)
574 {
575 if (this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
576 {
577 if (Node.Box.Equals(Box))
578 return true;
579 else
580 return false;
581 }
582 else
583 return false;
584 }
585 }
586
592 public bool Contains(string BoxId)
593 {
594 lock (this.synchObj)
595 {
596 return this.boxesById.ContainsKey(BoxId);
597 }
598 }
599
606 public bool TryGetBox(string BoxId, out T Box)
607 {
608 lock (this.synchObj)
609 {
610 if (!this.boxesById.TryGetValue(BoxId, out BoxNode Node))
611 {
612 Box = default;
613 return false;
614 }
615 else
616 {
617 Box = Node.Box;
618 return true;
619 }
620 }
621 }
622
628 public void CopyTo(T[] Destination, int Offset)
629 {
630 if (Destination is null)
631 throw new ArgumentNullException(nameof(Destination));
632
633 if (Offset < 0 || Offset >= Destination.Length)
634 throw new ArgumentOutOfRangeException(nameof(Offset), "Offset must be a non-negative number less than the size of the destination array.");
635
636 lock (this.synchObj)
637 {
638 if (Offset + this.boxesById.Count > Destination.Length)
639 throw new ArgumentException("Offset must allow all elements to be copied into the destination array.", nameof(Offset));
640
641 foreach (BoxNode Node in this.boxesById.Values)
642 Destination[Offset++] = Node.Box;
643 }
644 }
645
650 public T[] ToArray()
651 {
652 lock (this.synchObj)
653 {
654 T[] Result = new T[this.count];
655 int i = 0;
656
657 foreach (BoxNode Node in this.boxesById.Values)
658 Result[i++] = Node.Box;
659
660 return Result;
661 }
662 }
663
668 public IEnumerator<T> GetEnumerator()
669 {
670 lock (this.synchObj)
671 {
672 return new GeoBoxEnumerator(this, this.boxesById.Values.GetEnumerator());
673 }
674 }
675
680 IEnumerator IEnumerable.GetEnumerator()
681 {
682 return this.GetEnumerator();
683 }
684
685 private class GeoBoxEnumerator : IEnumerator<T>
686 {
687 private readonly IEnumerator<BoxNode> e;
688 private readonly GeoBoxCollection<T> collection;
689 private readonly int token;
690
691 public GeoBoxEnumerator(GeoBoxCollection<T> Collection, IEnumerator<BoxNode> Enumerator)
692 {
693 this.collection = Collection;
694 this.token = Collection.token;
695 this.e = Enumerator;
696 }
697
698 public T Current
699 {
700 get
701 {
702 lock (this.collection.synchObj)
703 {
704 if (this.token != this.collection.token)
705 throw new InvalidOperationException("Collection has been modified.");
706
707 return this.e.Current.Box;
708 }
709 }
710 }
711
712 object IEnumerator.Current => this.Current;
713
714 public void Dispose()
715 {
716 lock (this.collection.synchObj)
717 {
718 this.e.Dispose();
719 }
720 }
721
722 public bool MoveNext()
723 {
724 lock (this.collection.synchObj)
725 {
726 if (this.token != this.collection.token)
727 throw new InvalidOperationException("Collection has been modified.");
728
729 return this.e.MoveNext();
730 }
731 }
732
733 public void Reset()
734 {
735 lock (this.collection.synchObj)
736 {
737 if (this.token != this.collection.token)
738 throw new InvalidOperationException("Collection has been modified.");
739
740 this.e.Reset();
741 }
742 }
743 }
744
750 public bool Remove(T Box)
751 {
752 lock (this.synchObj)
753 {
754 return this.RemoveLocked(Box);
755 }
756 }
757
763 public bool Remove(string BoxId)
764 {
765 return this.Remove(BoxId, out _);
766 }
767
774 public bool Remove(string BoxId, out T Box)
775 {
776 lock (this.synchObj)
777 {
778 if (!this.boxesById.TryGetValue(BoxId, out BoxNode Node) ||
779 !this.RemoveLocked(Node))
780 {
781 Box = default;
782 return false;
783 }
784 else
785 {
786 Box = Node.Box;
787 return true;
788 }
789 }
790 }
791
792 private bool RemoveLocked(T Box)
793 {
794 if (!this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
795 return false;
796
797 if (!Node.Box.Equals(Box))
798 return false;
799
800 return this.RemoveLocked(Node);
801 }
802
803 private bool RemoveLocked(BoxNode Node)
804 {
805 T Box = Node.Box;
806
807 this.boxesById.Remove(Box.BoxId);
808 this.count--;
809 this.token++;
810
811 bool Result = false;
812
813 if (!(Node.References is null))
814 {
815 foreach (BoxListReference Reference in Node.References)
816 {
817 if (Reference.Grid.Remove(Reference))
818 Result = true;
819 }
820
821 Node.References.Clear();
822 Node.References = null;
823 }
824
825 return Result;
826 }
827
836 public bool Moved(T Box)
837 {
838 lock (this.synchObj)
839 {
840 if (!this.RemoveLocked(Box))
841 return false;
842
843 this.AddLocked(Box);
844
845 return true;
846 }
847 }
848
854 public T[] Find(GeoPosition Position)
855 {
856 ChunkedList<T> Result = null;
857
858 lock (this.synchObj)
859 {
860 this.root.Find(Position, ref Result);
861 }
862
863 return Result?.ToArray() ?? Array.Empty<T>();
864 }
865
870 public string Export()
871 {
872 StringBuilder Output = new StringBuilder();
873 this.Export(Output);
874 return Output.ToString();
875 }
876
881 public void Export(StringBuilder Output)
882 {
883 XmlWriterSettings Settings = new XmlWriterSettings()
884 {
885 Indent = true,
886 IndentChars = "\t",
887 NewLineChars = Environment.NewLine,
888 OmitXmlDeclaration = true
889 };
890
891 using (XmlWriter w = XmlWriter.Create(Output, Settings))
892 {
893 this.Export(w);
894 w.Flush();
895 }
896 }
897
902 public void Export(XmlWriter Output)
903 {
904 lock (this.synchObj)
905 {
906 Output.WriteStartElement("Boxes");
907 Output.WriteAttributeString("gridSize", this.gridSize.ToString());
908 Output.WriteAttributeString("maxCellCount", this.maxCellCount.ToString());
909 Output.WriteAttributeString("count", this.count.ToString());
910 Output.WriteAttributeString("token", this.token.ToString());
911
912 this.root.Export(Output);
913
914 Output.WriteEndElement();
915 }
916 }
917 }
918}
A chunked list is a linked list of chunks of objects of type T .
Definition: ChunkedList.cs:54
bool Remove(T Item)
Removes an element from the collection.
Definition: ChunkedList.cs:357
int Count
Number of elements in collection.
Definition: ChunkedList.cs:68
void Add(T Item)
Adds an item to the collection.
Definition: ChunkedList.cs:272
T[] ToArray()
Returns an array containing all elements of the collection.
In-memory thread-safe geo-spatial collection of bounding boxes.
bool Moved(T Box)
Each time a box has moved (and/or been resized), the collection must be notified. This is done by cal...
bool TryGetBox(string BoxId, out T Box)
Tries to get a geo-spatial bounding box, given its ID.
void Clear()
Clears the collection.
void Add(T Box)
Adds a geo-spatial bounding box.
int Count
Number of items in collection
bool Remove(string BoxId, out T Box)
Removes a bounding box from the collection, given its ID.
void Export(XmlWriter Output)
Outputs the contents of the collection to XML.
IEnumerator< T > GetEnumerator()
Returns an enumerator for all the elements in the collection.
bool Remove(T Box)
Removes a bounding box from the collection.
string Export()
Outputs the contents of the collection to XML.
void Export(StringBuilder Output)
Outputs the contents of the collection to XML.
T[] ToArray()
Returns the elements of the collection as an array.
GeoBoxCollection()
In-memory thread-safe geo-spatial collection of bounding boxes.
GeoBoxCollection(int GridSize, int MaxCellCount)
In-memory thread-safe geo-spatial collection of bounding boxes.
T[] Find(GeoPosition Position)
Finds all bounding boxes containing a geo-spatial position.
bool Contains(string BoxId)
Checks if the collection contains a geo-spatial bounding box.
bool Contains(T Box)
Checks if the collection contains a geo-spatial bounding box.
bool Remove(string BoxId)
Removes a bounding box from the collection, given its ID.
void CopyTo(T[] Destination, int Offset)
Copies the contents of the collection to a destination array.
bool IsReadOnly
If collection is read-only.
Contains information about a position in a geo-spatial coordinate system.
Definition: GeoPosition.cs:17
double NormalizedLongitude
Normalized longitude. The range [-180,180] is linearly mapped to [0,1].
Definition: GeoPosition.cs:123
double NormalizedLatitude
Normalized longitude. The range [-90,90] is linearly mapped to [0,1].
Definition: GeoPosition.cs:109
Interface for a geo-spatial bounding box using the Mercator Projection.
Definition: ImplTypes.g.cs:58