4using System.Diagnostics;
17 private const int DefaultGridSize = 8;
18 private const int DefaultMaxCellCount = 8;
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;
34 private class BoxListReference
37 public NormalizedBox Box;
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;
54 public BoxGrid(BoxGrid Parent, NormalizedBox Box,
int Width,
int Height,
55 int MaxCellCount,
int X,
int Y)
61 this.MaxCellCount = MaxCellCount;
62 this.Grid =
new Cell[this.Width, this.Height];
67 public void Fill(NormalizedBox Box)
69 if (this.EntireArea is
null)
72 this.EntireArea.
Add(Box);
74 if (Box.Node.References is
null)
77 Box.Node.References.Add(
new BoxListReference()
86 public void Add(NormalizedBox Box)
88 if (Box.Contains(
this.Box))
92 int ScaleX = this.Width;
93 int ScaleY = this.Height;
94 int Scale = this.Box.Scale;
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);
111 else if (x0 >= this.Width)
116 else if (y0 >= this.Height)
117 y0 = this.Height - 1;
121 else if (x1 >= this.Width)
126 else if (y1 >= this.Height)
127 y1 = this.Height - 1;
129 if (Box.Node.References is
null)
132 for (
int y = y0; y <= y1; y++)
134 for (
int x = x0; x <= x1; x++)
135 this.AddCell(Box, x, y);
140 public void AddCell(NormalizedBox Box,
int x,
int y)
142 Cell Cell = this.Grid[x, y];
146 this.Grid[x, y] =
new Cell()
151 Box.Node.References.
Add(
new BoxListReference()
159 else if (!(Cell.Grid is
null))
161 else if (Cell.List.Count >=
this.MaxCellCount &&
this.Box.CanIncreaseScale(
this.Height))
163 double DeltaLat = this.Box.DeltaLatitude / this.Height;
164 double DeltaLong = this.Box.DeltaLongitude / this.Width;
166 Cell.Grid =
new BoxGrid(
this,
new NormalizedBox(
default)
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);
181 Box.Node.References.Add(
new BoxListReference()
191 public bool Remove(BoxGrid Grid)
193 if (this.Grid is
null)
196 Cell Cell = this.Grid[Grid.X, Grid.Y];
200 if (!(Cell.Grid == Grid))
205 if (Cell.List is
null)
206 this.Grid[Grid.X, Grid.Y] =
null;
211 public bool Remove(BoxListReference Reference)
213 if (Reference.X < 0 || Reference.Y < 0)
215 if (!this.EntireArea.
Remove(Reference.Box))
218 if (this.EntireArea.
Count == 0)
220 this.EntireArea =
null;
222 for (
int y = 0; y < this.Height; y++)
224 for (
int x = 0; x < this.Width; x++)
226 if (!(this.Grid[x, y] is
null))
231 this.Parent?.Remove(
this);
236 Cell Cell = this.Grid[Reference.X, Reference.Y];
240 if (!(Cell.List?.Remove(Reference.Box) ??
false))
243 if (Cell.List.Count == 0)
247 if (Cell.Grid is
null)
249 this.Grid[Reference.X, Reference.Y] =
null;
251 if (this.EntireArea is
null)
253 for (
int y = 0; y < this.Height; y++)
255 for (
int x = 0; x < this.Width; x++)
257 if (!(this.Grid[x, y] is
null))
262 this.Parent?.Remove(
this);
273 if (!(this.EntireArea is
null))
275 foreach (NormalizedBox Box
in this.EntireArea)
277 if (Box.Node.Box.Contains(Position))
282 Result.Add(Box.Node.Box);
287 int ScaleX = this.Width;
288 int ScaleY = this.Height;
289 int Scale = this.Box.Scale;
302 if (x >= 0 && x < this.Width && y >= 0 && y < this.Height)
304 Cell Cell = this.Grid[x, y];
308 if (!(Cell.List is
null))
310 foreach (NormalizedBox Box
in Cell.List)
312 if (Box.Node.Box.Contains(Position))
317 Result.Add(Box.Node.Box);
322 Cell.Grid?.Find(Position, ref Result);
327 public void Export(XmlWriter Output)
329 Output.WriteStartElement(
"Grid");
330 Output.WriteAttributeString(
"width", this.Width.ToString());
331 Output.WriteAttributeString(
"height", this.Height.ToString());
333 this.Box.Export(Output,
"GridNBox");
335 if (!(this.EntireArea is
null))
337 foreach (NormalizedBox Box
in this.EntireArea)
338 Box.Export(Output,
"FullNBox");
341 for (
int y = 0; y < this.Height; y++)
343 for (
int x = 0; x < this.Width; x++)
344 this.Grid[x, y]?.
Export(Output, x, y);
347 Output.WriteEndElement();
356 public void Export(XmlWriter Output,
int X,
int Y)
358 Output.WriteStartElement(
"Cell");
359 Output.WriteAttributeString(
"x", X.ToString());
360 Output.WriteAttributeString(
"y", Y.ToString());
362 if (!(this.List is
null))
364 foreach (NormalizedBox Box
in this.List)
365 Box.Export(Output,
"NBoxRef");
368 this.Grid?.Export(Output);
369 Output.WriteEndElement();
373 [DebuggerDisplay(
"{MinLatitude},{MinLongitude} - {MaxLatitude},{MaxLongitude}")]
374 private class NormalizedBox
376 public NormalizedBox(BoxNode Node)
382 public double MinLatitude;
383 public double MaxLatitude;
384 public double MinLongitude;
385 public double MaxLongitude;
388 public double DeltaLatitude => this.MaxLatitude - this.MinLatitude;
389 public double DeltaLongitude => this.MaxLongitude - this.MinLongitude;
391 public bool Contains(NormalizedBox Box)
394 Box.MinLatitude >= this.MinLatitude &&
395 Box.MinLongitude >= this.MinLongitude &&
396 Box.MaxLatitude <= this.MaxLatitude &&
397 Box.MaxLongitude <= this.MaxLongitude;
400 public bool CanIncreaseScale(
int Factor)
406 return ((
int)x) == x;
409 public void Export(XmlWriter Output,
string ElementName)
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());
418 if (!(this.Node is
null))
420 Output.WriteAttributeString(
"boxId", this.Node.Box.BoxId);
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();
432 Output.WriteEndElement();
440 : this(DefaultGridSize, DefaultMaxCellCount)
453 this.gridSize = GridSize;
454 this.maxCellCount = MaxCellCount;
489 private void AddLocked(T Box)
491 if (this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
493 if (Node.Box.Equals(Box))
496 throw new ArgumentException(
"An bounding box with the same ID already exists in the collection.", nameof(Box));
503 this.boxesById[Box.BoxId] = Node;
505 if (Box.LongitudeWrapped)
507 double MinLat = Box.Min.NormalizedLatitude;
508 double MaxLat = Box.Max.NormalizedLatitude;
510 this.root.Add(
new NormalizedBox(Node)
512 MinLatitude = MinLat,
513 MaxLatitude = MaxLat,
514 MinLongitude = Box.Min.NormalizedLongitude,
519 this.root.Add(
new NormalizedBox(Node)
521 MinLatitude = MinLat,
522 MaxLatitude = MaxLat,
524 MaxLongitude = Box.Max.NormalizedLongitude,
530 this.root.Add(
new NormalizedBox(Node)
532 MinLatitude = Box.Min.NormalizedLatitude,
533 MaxLatitude = Box.Max.NormalizedLatitude,
534 MinLongitude = Box.Min.NormalizedLongitude,
535 MaxLongitude = Box.Max.NormalizedLongitude,
551 this.root =
new BoxGrid(
null,
new NormalizedBox(
default)
558 }, 2 * this.gridSize, this.gridSize, this.maxCellCount, 0, 0);
560 this.boxesById.Clear();
575 if (this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
577 if (Node.Box.Equals(Box))
596 return this.boxesById.ContainsKey(BoxId);
610 if (!this.boxesById.TryGetValue(BoxId, out BoxNode Node))
628 public void CopyTo(T[] Destination,
int Offset)
630 if (Destination is
null)
631 throw new ArgumentNullException(nameof(Destination));
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.");
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));
641 foreach (BoxNode Node
in this.boxesById.Values)
642 Destination[Offset++] = Node.Box;
654 T[] Result =
new T[this.count];
657 foreach (BoxNode Node
in this.boxesById.Values)
658 Result[i++] = Node.Box;
672 return new GeoBoxEnumerator(
this, this.boxesById.Values.GetEnumerator());
680 IEnumerator IEnumerable.GetEnumerator()
685 private class GeoBoxEnumerator : IEnumerator<T>
687 private readonly IEnumerator<BoxNode> e;
688 private readonly GeoBoxCollection<T> collection;
689 private readonly
int token;
691 public GeoBoxEnumerator(GeoBoxCollection<T> Collection, IEnumerator<BoxNode> Enumerator)
693 this.collection = Collection;
694 this.token = Collection.token;
702 lock (this.collection.synchObj)
704 if (this.token != this.collection.token)
705 throw new InvalidOperationException(
"Collection has been modified.");
707 return this.e.Current.Box;
712 object IEnumerator.Current => this.Current;
714 public void Dispose()
716 lock (this.collection.synchObj)
722 public bool MoveNext()
724 lock (this.collection.synchObj)
726 if (this.token != this.collection.token)
727 throw new InvalidOperationException(
"Collection has been modified.");
729 return this.e.MoveNext();
735 lock (this.collection.synchObj)
737 if (this.token != this.collection.token)
738 throw new InvalidOperationException(
"Collection has been modified.");
754 return this.RemoveLocked(Box);
765 return this.
Remove(BoxId, out
_);
774 public bool Remove(
string BoxId, out T Box)
778 if (!this.boxesById.TryGetValue(BoxId, out BoxNode Node) ||
779 !
this.RemoveLocked(Node))
792 private bool RemoveLocked(T Box)
794 if (!this.boxesById.TryGetValue(Box.BoxId, out BoxNode Node))
797 if (!Node.Box.Equals(Box))
800 return this.RemoveLocked(Node);
803 private bool RemoveLocked(BoxNode Node)
807 this.boxesById.Remove(Box.BoxId);
813 if (!(Node.References is
null))
815 foreach (BoxListReference Reference
in Node.References)
817 if (Reference.Grid.Remove(Reference))
821 Node.References.Clear();
822 Node.References =
null;
840 if (!this.RemoveLocked(Box))
860 this.root.Find(Position, ref Result);
863 return Result?.
ToArray() ?? Array.Empty<T>();
872 StringBuilder Output =
new StringBuilder();
874 return Output.ToString();
883 XmlWriterSettings Settings =
new XmlWriterSettings()
887 NewLineChars = Environment.NewLine,
888 OmitXmlDeclaration =
true
891 using (XmlWriter w = XmlWriter.Create(Output, Settings))
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());
912 this.root.Export(Output);
914 Output.WriteEndElement();
A chunked list is a linked list of chunks of objects of type T .
bool Remove(T Item)
Removes an element from the collection.
int Count
Number of elements in collection.
void Add(T Item)
Adds an item to the collection.
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.
double NormalizedLongitude
Normalized longitude. The range [-180,180] is linearly mapped to [0,1].
double NormalizedLatitude
Normalized longitude. The range [-90,90] is linearly mapped to [0,1].
Interface for a geo-spatial bounding box using the Mercator Projection.