Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
GeoSpatialComponent.cs
1using Paiwise;
2using System;
4using System.Text;
5using System.Text.RegularExpressions;
6using System.Threading.Tasks;
7using System.Xml;
9using Waher.Events;
18
20{
22 {
26 private const int defaultSubscriptionTtl = 60;
27
31 private const int maxSubscriptionTtl = 300;
32
36 private const int maxSubscriptionsPerClient = 5; // TODO: Make an account setting
37
41 private const int maxPublicationsPerClient = 5; // TODO: Make an account setting
42
46 private const int maxSearchResults = 100; // TODO: Make an account setting
47
50 private readonly GeoBoxCollection<GeoSubscription> geoSubscriptions =
52 private readonly Dictionary<CaseInsensitiveString, ChunkedList<GeoSubscription>> geoSubscriptionsByBareJid =
53 new Dictionary<CaseInsensitiveString, ChunkedList<GeoSubscription>>();
54 private readonly Dictionary<CaseInsensitiveString, int> geoPublicationsByBareJid =
55 new Dictionary<CaseInsensitiveString, int>();
56
60 public const string NamespaceGeoSpatialNeuroFoundationV1 = "urn:nf:iot:geo:1.0";
61
65 public static readonly string[] NamespacesGeoSpatial = new string[]
66 {
68 };
69
75 public static bool IsNamespaceGeoSpatial(string Namespace)
76 {
77 return Array.IndexOf(NamespacesGeoSpatial, Namespace) >= 0;
78 }
79
87 : base(Server, Subdomain, Name)
88 {
89 Server.ClientConnectionRemoved += this.ClientConnectionRemoved;
90
91 #region Neuro-Foundation V1 handlers
92
93 this.RegisterIqSetHandler("subscribe", NamespaceGeoSpatialNeuroFoundationV1, this.SubscribeHandler, true);
94 this.RegisterIqSetHandler("unsubscribe", NamespaceGeoSpatialNeuroFoundationV1, this.UnsubscribeHandler, false);
95 this.RegisterIqSetHandler("publish", NamespaceGeoSpatialNeuroFoundationV1, this.PublishHandler, false);
96 this.RegisterIqSetHandler("delete", NamespaceGeoSpatialNeuroFoundationV1, this.DeleteHandler, false);
97 this.RegisterIqGetHandler("search", NamespaceGeoSpatialNeuroFoundationV1, this.SearchHandler, false);
98
99 #endregion
100 }
101
105 public override void Dispose()
106 {
107 this.Server.ClientConnectionRemoved -= this.ClientConnectionRemoved;
108
109 #region Neuro-Foundation V1 handlers
110
111 this.UnregisterIqSetHandler("subscribe", NamespaceGeoSpatialNeuroFoundationV1, this.SubscribeHandler, true);
112 this.UnregisterIqSetHandler("unsubscribe", NamespaceGeoSpatialNeuroFoundationV1, this.UnsubscribeHandler, false);
113 this.UnregisterIqSetHandler("publish", NamespaceGeoSpatialNeuroFoundationV1, this.PublishHandler, false);
114 this.UnregisterIqSetHandler("delete", NamespaceGeoSpatialNeuroFoundationV1, this.DeleteHandler, false);
115 this.UnregisterIqGetHandler("search", NamespaceGeoSpatialNeuroFoundationV1, this.SearchHandler, false);
116
117 #endregion
118 }
119
124 public override bool SupportsAccounts => false;
125
129 internal async Task Load()
130 {
131 try
132 {
133 await Database.Iterate(new ReferenceLoader(this), "GeoObjects");
134 }
135 catch (Exception ex)
136 {
137 Log.Exception(ex);
138 }
139 }
140
145 public async Task Publish(IGeoSpatialObject Object)
146 {
147 if (Object is null)
148 return;
149
150 if (!Object.HasGeoLocation)
151 {
152 await this.Delete(Object);
153 return;
154 }
155
156 GeoPosition PrevLocation;
157
158 if (this.geoPositions.TryGetObject(Object.GeoId, out PersistedGeoSpatialObjectReference Ref))
159 {
160 bool PrevEphemeral = Ref.EphemeralLocation;
161 PrevLocation = Ref.Location;
162
163 Ref.Location = await Object.GetLocation();
164 Ref.EphemeralLocation = Object.EphemeralLocation;
165
166 this.geoPositions.Moved(Ref);
167
168 if (Ref.EphemeralLocation)
169 {
170 if (!PrevEphemeral && !string.IsNullOrEmpty(Ref.ObjectId))
171 {
172 await Database.Delete(Ref);
173 Ref.ObjectId = null;
174 }
175 }
176 else
177 {
178 if (!string.IsNullOrEmpty(Ref.ObjectId))
179 await Database.Update(Ref);
180 else
181 await Database.Insert(Ref);
182 }
183 }
184 else
185 {
186 Ref = await PersistedGeoSpatialObjectReference.Create(Object);
187 this.AddReference(Ref, false);
188
189 if (!Ref.EphemeralLocation)
190 await Database.Insert(Ref);
191
192 PrevLocation = null;
193 }
194
195 Dictionary<string, GeoSubscription> PrevSubscriptions = null;
196 GeoSubscription[] Subscriptions;
197
198 if (!(PrevLocation is null) && !PrevLocation.Equals(Ref.Location))
199 {
200 Subscriptions = this.geoSubscriptions.Find(PrevLocation);
201 if (Subscriptions.Length > 0)
202 {
203 PrevSubscriptions = new Dictionary<string, GeoSubscription>();
204
205 foreach (GeoSubscription Subscription in Subscriptions)
206 PrevSubscriptions[Subscription.BoxId] = Subscription;
207 }
208 }
209
210 Subscriptions = this.geoSubscriptions.Find(Ref.Location);
211
212 if (Subscriptions.Length > 0)
213 {
214 foreach (GeoSubscription Subscription in Subscriptions)
215 {
216 try
217 {
218 if (PrevSubscriptions?.Remove(Subscription.BoxId) ?? false)
219 await Subscription.ObjectUpdated(Ref, this);
220 else
221 await Subscription.ObjectAdded(Ref, this);
222 }
223 catch (Exception ex)
224 {
225 Log.Exception(ex);
226 }
227 }
228 }
229
230 if (!(PrevSubscriptions is null))
231 {
232 foreach (GeoSubscription Subscription in PrevSubscriptions.Values)
233 {
234 try
235 {
236 await Subscription.ObjectRemoved(Ref, this);
237 }
238 catch (Exception ex)
239 {
240 Log.Exception(ex);
241 }
242 }
243 }
244 }
245
246 private bool AddReference(PersistedGeoSpatialObjectReference Ref, bool CheckLimit)
247 {
249 {
250 lock (this.geoPublicationsByBareJid)
251 {
252 if (this.geoPublicationsByBareJid.TryGetValue(Ref.Creator, out int Count))
253 {
254 if (CheckLimit && Count >= maxPublicationsPerClient)
255 return false;
256
257 this.geoPublicationsByBareJid[Ref.Creator] = Count + 1;
258 }
259 else
260 this.geoPublicationsByBareJid[Ref.Creator] = 1;
261 }
262 }
263
264 this.geoPositions.Add(Ref);
265
266 return true;
267 }
268
269 private bool RemoveReference(string GeoId)
270 {
271 return this.RemoveReference(GeoId, out _);
272 }
273
274 private bool RemoveReference(PersistedGeoSpatialObjectReference Ref)
275 {
276 return this.RemoveReference(Ref.GeoId, out _);
277 }
278
279 private bool RemoveReference(string GeoId, out PersistedGeoSpatialObjectReference Ref)
280 {
281 if (!this.geoPositions.Remove(GeoId, out Ref))
282 return false;
283
284 if (Ref.Expires.HasValue)
285 {
286 Gateway.CancelScheduledEvent(Ref.Expires.Value);
287 Ref.Expires = null;
288 }
289
290 if (!CaseInsensitiveString.IsNullOrEmpty(Ref.Creator))
291 {
292 lock (this.geoPublicationsByBareJid)
293 {
294 if (this.geoPublicationsByBareJid.TryGetValue(Ref.Creator, out int Count))
295 {
296 if (Count > 1)
297 this.geoPublicationsByBareJid[Ref.Creator] = Count - 1;
298 else
299 this.geoPublicationsByBareJid.Remove(Ref.Creator);
300 }
301 }
302 }
303
304 return true;
305 }
306
307 internal void Loaded(PersistedGeoSpatialObjectReference Ref)
308 {
309 if (!this.geoPositions.Contains(Ref.GeoId))
310 {
311 this.AddReference(Ref, false);
312
313 if (Ref.Expires.HasValue)
314 Gateway.ScheduleEvent(this.CheckItemExpired, Ref.Expires.Value, Ref.GeoId);
315 }
316 }
317
322 public async Task Delete(IGeoSpatialObject Object)
323 {
324 if (Object is null)
325 return;
326
327 await this.Delete(Object.GeoId);
328 }
329
334 public async Task Delete(string GeoId)
335 {
336 if (this.RemoveReference(GeoId, out PersistedGeoSpatialObjectReference Ref))
337 {
338 if (!string.IsNullOrEmpty(Ref.ObjectId))
339 await Database.Delete(Ref);
340
341 GeoSubscription[] Subscriptions = this.geoSubscriptions.Find(Ref.Location);
342
343 if (Subscriptions.Length > 0)
344 {
345 foreach (GeoSubscription Subscription in Subscriptions)
346 {
347 try
348 {
349 await Subscription.ObjectRemoved(Ref, this);
350 }
351 catch (Exception ex)
352 {
353 Log.Exception(ex);
354 }
355 }
356 }
357 }
358 }
359
360 private async Task SubscribeHandler(object Sender, IqEventArgs e)
361 {
362 string Id = XML.Attribute(e.Query, "id");
363 double MinLat = XML.Attribute(e.Query, "minLat", double.NaN);
364 double MaxLat = XML.Attribute(e.Query, "maxLat", double.NaN);
365 double MinLon = XML.Attribute(e.Query, "minLon", double.NaN);
366 double MaxLon = XML.Attribute(e.Query, "maxLon", double.NaN);
367 double? MinAlt = e.Query.HasAttribute("minAlt") ? XML.Attribute(e.Query, "minAlt", double.NaN) : (double?)null;
368 double? MaxAlt = e.Query.HasAttribute("maxAlt") ? XML.Attribute(e.Query, "maxAlt", double.NaN) : (double?)null;
369 int Ttl = XML.Attribute(e.Query, "ttl", defaultSubscriptionTtl);
370
371 if (double.IsNaN(MinLat) ||
372 double.IsNaN(MaxLat) ||
373 double.IsNaN(MinLon) ||
374 double.IsNaN(MaxLon) ||
375 (MinAlt.HasValue && double.IsNaN(MinAlt.Value)) ||
376 (MaxAlt.HasValue && double.IsNaN(MaxAlt.Value)))
377 {
378 await e.IqErrorBadRequest(e.To, "Invalid floating-point attribute values.", "en");
379 return;
380 }
381
382 if (MinLat < -90 || MinLat > 90 || MaxLat < -90 || MaxLat > 90)
383 {
384 await e.IqErrorBadRequest(e.To, "Valid latitude values reside in the range [-90,90].", "en");
385 return;
386 }
387
388 if (MinLon < -180 || MinLon > 180 || MaxLon < -180 || MaxLon > 180)
389 {
390 await e.IqErrorBadRequest(e.To, "Valid longitude values reside in the range [-180,180].", "en");
391 return;
392 }
393
394 if (MinLat > MaxLat)
395 {
396 await e.IqErrorBadRequest(e.To, "Minimum latitude cannot be larger than the maximum latitude.", "en");
397 return;
398 }
399
400 if (MinAlt.HasValue && MaxAlt.HasValue && MinAlt.Value > MaxAlt.Value)
401 {
402 await e.IqErrorBadRequest(e.To, "Minimum altitude cannot be larger than the maximum altitude.", "en");
403 return;
404 }
405
406 if (Ttl <= 0)
407 {
408 await e.IqErrorBadRequest(e.To, "Invalid time to live (TTL) value.", "en");
409 return;
410 }
411
412 if (Ttl > maxSubscriptionTtl)
413 Ttl = maxSubscriptionTtl;
414
415 Dictionary<string, PersistedGeoSpatialObjectReference> PrevReferences = null;
416 PersistedGeoSpatialObjectReference[] References;
417 GeoSubscription Subscription;
418
419 if (!string.IsNullOrEmpty(Id))
420 {
421 if (!this.geoSubscriptions.TryGetBox(Id, out Subscription))
422 {
423 await e.IqErrorItemNotFound(e.To, "Subscription ID not found.", "en");
424 return;
425 }
426
427 if (Subscription.From.BareJid != e.From.BareJid)
428 {
429 await e.IqErrorForbidden(e.To, "Subscription not created by sender.", "en");
430 return;
431 }
432
433 References = this.geoPositions.Find(Subscription);
434 PrevReferences = new Dictionary<string, PersistedGeoSpatialObjectReference>();
435
436 foreach (PersistedGeoSpatialObjectReference Obj in References)
437 PrevReferences[Obj.GeoId] = Obj;
438
439 Subscription.Min.Latitude = MinLat;
440 Subscription.Min.Longitude = MinLon;
441 Subscription.Min.Altitude = MinAlt;
442 Subscription.Max.Latitude = MaxLat;
443 Subscription.Max.Longitude = MaxLon;
444 Subscription.Max.Altitude = MaxAlt;
445
446 Gateway.CancelScheduledEvent(Subscription.Elapses);
447 Subscription.Elapses = Gateway.ScheduleEvent(this.SubscriptionTimeout,
448 DateTime.Now.AddSeconds(Ttl), Subscription);
449
450 this.geoSubscriptions.Moved(Subscription);
451 }
452 else
453 {
454 Subscription = new GeoSubscription(
455 e.From,
456 new GeoPosition(MinLat, MinLon, MinAlt),
457 new GeoPosition(MaxLat, MaxLon, MaxAlt),
458 DateTime.MinValue);
459
460 lock (this.geoSubscriptionsByBareJid)
461 {
462 if (!this.geoSubscriptionsByBareJid.TryGetValue(e.From.BareJid,
463 out ChunkedList<GeoSubscription> Subscriptions))
464 {
465 Subscriptions = new ChunkedList<GeoSubscription>(1);
466 this.geoSubscriptionsByBareJid[e.From.BareJid] = Subscriptions;
467
468 Subscriptions.Add(Subscription);
469 }
470 else if (Subscriptions.Count >= maxSubscriptionsPerClient)
471 Subscription = null;
472 else
473 Subscriptions.Add(Subscription);
474 }
475
476 if (Subscription is null)
477 {
478 await e.IqErrorResourceConstraint(e.To, "Too many current subscriptions.", "en");
479 return;
480 }
481
482 this.geoSubscriptions.Add(Subscription);
483
484 DateTime Elapses = Gateway.ScheduleEvent(this.SubscriptionTimeout,
485 DateTime.Now.AddSeconds(Ttl), Subscription);
486
487 Subscription.Elapses = Elapses;
488 }
489
490 StringBuilder Xml = new StringBuilder();
491
492 Xml.Append("<subscribed xmlns='");
493 Xml.Append(e.Query.NamespaceURI);
494 Xml.Append("' ttl='");
495 Xml.Append(Ttl);
496
497 if (string.IsNullOrEmpty(Id))
498 {
499 Xml.Append("' id='");
500 Xml.Append(XML.Encode(Subscription.BoxId));
501 }
502
503 Xml.Append("'/>");
504
505 await e.IqResult(Xml.ToString(), e.To);
506
507 References = this.geoPositions.Find(Subscription);
508
509 foreach (PersistedGeoSpatialObjectReference Obj in References)
510 {
511 if (PrevReferences is null || !PrevReferences.Remove(Obj.GeoId))
512 await Subscription.ObjectAdded(Obj, this);
513 }
514
515 if (!(PrevReferences is null))
516 {
517 foreach (PersistedGeoSpatialObjectReference Obj in PrevReferences.Values)
518 await Subscription.ObjectRemoved(Obj, this);
519 }
520 }
521
522 private Task ClientConnectionRemoved(object Sender, ClientConnectionEventArgs e)
523 {
524 try
525 {
526 XmppAddress Jid = new XmppAddress(e.JID);
527 ChunkedList<GeoSubscription> Removed = null;
529
530 lock (this.geoSubscriptionsByBareJid)
531 {
532 if (!this.geoSubscriptionsByBareJid.TryGetValue(Jid.BareJid,
533 out ChunkedList<GeoSubscription> Subscriptions))
534 {
535 return Task.CompletedTask;
536 }
537
538 foreach (GeoSubscription Subscription in Subscriptions)
539 {
540 if (Subscription.From.Address == e.JID)
541 {
542 Removed ??= new ChunkedList<GeoSubscription>();
543 Removed.Add(Subscription);
544 }
545 else
546 {
547 Kept ??= new ChunkedList<GeoSubscription>();
548 Kept.Add(Subscription);
549 }
550 }
551
552 if (Kept is null)
553 this.geoSubscriptionsByBareJid.Remove(Jid.BareJid);
554 else if (!(Removed is null))
555 this.geoSubscriptionsByBareJid[Jid.BareJid] = Kept;
556 }
557
558 if (!(Removed is null))
559 {
560 foreach (GeoSubscription Subscription in Removed)
561 {
562 Gateway.CancelScheduledEvent(Subscription.Elapses);
563 Subscription.Elapses = DateTime.MinValue;
564
565 this.geoSubscriptions.Remove(Subscription);
566 }
567 }
568 }
569 catch (Exception ex)
570 {
571 Log.Exception(ex);
572 }
573
574 return Task.CompletedTask;
575 }
576
577 private async Task SubscriptionTimeout(object State)
578 {
579 if (!(State is GeoSubscription Subscription))
580 return;
581
582 this.geoSubscriptions.Remove(Subscription);
583
584 lock (this.geoSubscriptionsByBareJid)
585 {
586 if (this.geoSubscriptionsByBareJid.TryGetValue(Subscription.From.BareJid,
587 out ChunkedList<GeoSubscription> Subscriptions) &&
588 Subscriptions.Remove(Subscription) &&
589 Subscriptions.Count == 0)
590 {
591 this.geoSubscriptionsByBareJid.Remove(Subscription.From.BareJid);
592 }
593 }
594
595 await this.Message(string.Empty, string.Empty, Subscription.From,
596 this.MainDomain, string.Empty, UnsubscribedXml(Subscription.BoxId), null);
597 }
598
599 private static string UnsubscribedXml(string Id)
600 {
601 StringBuilder Xml = new StringBuilder();
602
603 Xml.Append("<unsubscribed xmlns='");
605 Xml.Append("' id='");
606 Xml.Append(XML.Encode(Id));
607 Xml.Append("'/>");
608
609 return Xml.ToString();
610 }
611
612 private async Task UnsubscribeHandler(object Sender, IqEventArgs e)
613 {
614 string Id = XML.Attribute(e.Query, "id");
615
616 if (string.IsNullOrEmpty(Id))
617 {
618 await e.IqErrorBadRequest(e.To, "Missing subscription ID.", "en");
619 return;
620 }
621
622 if (!this.geoSubscriptions.TryGetBox(Id, out GeoSubscription Subscription))
623 {
624 await e.IqErrorItemNotFound(e.To, "Subscription ID not found.", "en");
625 return;
626 }
627
628 if (Subscription.From.BareJid != e.From.BareJid)
629 {
630 await e.IqErrorForbidden(e.To, "Subscription not created by sender.", "en");
631 return;
632 }
633
634 Gateway.CancelScheduledEvent(Subscription.Elapses);
635 Subscription.Elapses = DateTime.MinValue;
636
637 this.geoSubscriptions.Remove(Subscription);
638
639 await e.IqResult(UnsubscribedXml(Id), e.To);
640 }
641
642 private async Task PublishHandler(object Sender, IqEventArgs e)
643 {
644 string Id = XML.Attribute(e.Query, "id");
645 double Lat = XML.Attribute(e.Query, "lat", double.NaN);
646 double Lon = XML.Attribute(e.Query, "lon", double.NaN);
647 double? Alt = e.Query.HasAttribute("alt") ? XML.Attribute(e.Query, "alt", double.NaN) : (double?)null;
648 int? Ttl = e.Query.HasAttribute("ttl") ? XML.Attribute(e.Query, "ttl", 0) : (int?)null;
649 DateTime? From = e.Query.HasAttribute("from") ? XML.Attribute(e.Query, "from", DateTime.MinValue) : (DateTime?)null;
650 DateTime? To = e.Query.HasAttribute("to") ? XML.Attribute(e.Query, "to", DateTime.MinValue) : (DateTime?)null;
651
652 if (double.IsNaN(Lat) ||
653 double.IsNaN(Lon) ||
654 (Alt.HasValue && double.IsNaN(Alt.Value)))
655 {
656 await e.IqErrorBadRequest(e.To, "Invalid floating-point attribute values.", "en");
657 return;
658 }
659
660 if (Lat < -90 || Lat > 90)
661 {
662 await e.IqErrorBadRequest(e.To, "Valid latitude values reside in the range [-90,90].", "en");
663 return;
664 }
665
666 if (Lon < -180 || Lon > 180)
667 {
668 await e.IqErrorBadRequest(e.To, "Valid longitude values reside in the range [-180,180].", "en");
669 return;
670 }
671
672 if (Ttl.HasValue && Ttl.Value <= 0)
673 {
674 await e.IqErrorBadRequest(e.To, "Invalid time to live (TTL) value.", "en");
675 return;
676 }
677
678 if (From.HasValue)
679 {
680 if (From.Value == DateTime.MinValue)
681 {
682 await e.IqErrorBadRequest(e.To, "Invalid from timestamp.", "en");
683 return;
684 }
685
686 if (From.Value.Kind != DateTimeKind.Utc)
687 {
688 await e.IqErrorBadRequest(e.To, "From timestamp not in UTC.", "en");
689 return;
690 }
691 }
692
693 if (To.HasValue)
694 {
695 if (To.Value == DateTime.MinValue)
696 {
697 await e.IqErrorBadRequest(e.To, "Invalid to timestamp.", "en");
698 return;
699 }
700
701 if (To.Value.Kind != DateTimeKind.Utc)
702 {
703 await e.IqErrorBadRequest(e.To, "To timestamp not in UTC.", "en");
704 return;
705 }
706 }
707
708 int i = Id.IndexOf(':');
709 if (i > 0)
710 {
711 string Scheme = Id[..i];
712
713 switch (Scheme.ToLower())
714 {
715 case "iotdisco":
716 await e.IqErrorForbidden(e.To, "Geo-spatial references to devices must be managed via the Thing Registry.", "en");
717 return;
718
719 case "iotid":
720 string LegalId = Id[(i + 1)..];
721
722 LegalIdentity Identity = await LegalComponent.GetLocalLegalIdentity(LegalId);
723 if (Identity is null)
724 {
725 await e.IqErrorForbidden(e.To, "Geo-spatial references to legal identities only allowed for local identities.", "en");
726 return;
727 }
728
729 PersonalInformation Info = LegalComponent.GetPersonalInformation(Identity);
730
731 if (e.From.BareJid != Info.Jid)
732 {
733 await e.IqErrorForbidden(e.To, "You are only allowed to add geo-spatial references to your own legal identities.", "en");
734 return;
735 }
736 break;
737 }
738 }
739
740 Dictionary<string, GeoSubscription> PrevSubscriptions = null;
741 PersistedGeoSpatialObjectReference Ref = null;
742 bool NewRef = true;
743
744 if (string.IsNullOrEmpty(Id))
745 Id = Guid.NewGuid().ToString();
746 else
747 {
748 if (this.geoPositions.TryGetObject(Id, out Ref))
749 {
750 if (e.From.BareJid != Ref.Creator)
751 {
752 await e.IqErrorForbidden(e.To, "Object not created by sender.", "en");
753 return;
754 }
755
756 if (Ref.Expires.HasValue)
757 {
758 Gateway.CancelScheduledEvent(Ref.Expires.Value);
759 Ref.Expires = null;
760 }
761
762 NewRef = false;
763 PrevSubscriptions ??= new Dictionary<string, GeoSubscription>();
764
765 foreach (GeoSubscription Subscription in this.geoSubscriptions.Find(Ref.Location))
766 PrevSubscriptions[Subscription.BoxId] = Subscription;
767 }
768 else
769 Ref = null;
770 }
771
772 if (Ttl.HasValue)
773 {
774 if (Ref is null)
775 {
776 Ref = new PersistedGeoSpatialObjectReference()
777 {
778 GeoId = Id,
779 EphemeralLocation = false,
780 Expires = Gateway.ScheduleEvent(this.CheckItemExpired, DateTime.Now.AddSeconds(Ttl.Value), Id),
781 Creator = e.From.BareJid,
782 Location = new GeoPosition(Lat, Lon, Alt),
783 ContentXml = e.Query.InnerXml,
784 Created = DateTime.UtcNow,
785 From = From,
786 To = To
787 };
788 }
789 else
790 {
791 Ref.EphemeralLocation = false;
792 Ref.Expires = Gateway.ScheduleEvent(this.CheckItemExpired, DateTime.Now.AddSeconds(Ttl.Value), Id);
793 Ref.Location = new GeoPosition(Lat, Lon, Alt);
794 Ref.ContentXml = e.Query.InnerXml;
795 Ref.Updated = DateTime.UtcNow;
796 Ref.From = From;
797 Ref.To = To;
798 }
799 }
800 else
801 {
802 if (Ref is null)
803 {
804 Ref = new PersistedGeoSpatialObjectReference()
805 {
806 GeoId = Id,
807 EphemeralLocation = true,
808 Expires = null,
809 Creator = e.From.BareJid,
810 Location = new GeoPosition(Lat, Lon, Alt),
811 ContentXml = e.Query.InnerXml,
812 Created = DateTime.UtcNow,
813 From = From,
814 To = To
815 };
816 }
817 else
818 {
819 this.RemoveReference(Ref);
820
821 Ref.EphemeralLocation = true;
822 Ref.Expires = null;
823 Ref.Location = new GeoPosition(Lat, Lon, Alt);
824 Ref.ContentXml = e.Query.InnerXml;
825 Ref.Updated = DateTime.UtcNow;
826 Ref.From = From;
827 Ref.To = To;
828 }
829 }
830
831 if (NewRef)
832 {
833 if (!this.AddReference(Ref, true))
834 {
835 await e.IqErrorResourceConstraint(e.To, "Publication limit reached.", "en");
836 return;
837 }
838 }
839 else
840 this.geoPositions.Moved(Ref);
841
842 if (Ref.EphemeralLocation)
843 {
844 if (!string.IsNullOrEmpty(Ref.ObjectId))
845 {
846 await Database.Delete(Ref);
847 Ref.ObjectId = null;
848 }
849 }
850 else if (string.IsNullOrEmpty(Ref.ObjectId))
851 await Database.Insert(Ref);
852 else
853 await Database.Update(Ref);
854
855 StringBuilder Xml = new StringBuilder();
856
857 Xml.Append("<published xmlns='");
859 Xml.Append("' id='");
860 Xml.Append(XML.Encode(Id));
861
862 if (Ttl.HasValue)
863 {
864 Xml.Append("' ttl='");
865 Xml.Append(Ttl.Value);
866 }
867
868 Xml.Append("'/>");
869
870 await e.IqResult(Xml.ToString(), e.To);
871
872 GeoSubscription[] Subscriptions = this.geoSubscriptions.Find(Ref.Location);
873
874 foreach (GeoSubscription Subscription in Subscriptions)
875 {
876 try
877 {
878 if (PrevSubscriptions?.Remove(Subscription.BoxId) ?? false)
879 await Subscription.ObjectUpdated(Ref, this);
880 else
881 await Subscription.ObjectAdded(Ref, this);
882 }
883 catch (Exception ex)
884 {
885 Log.Exception(ex);
886 }
887 }
888
889 if ((PrevSubscriptions?.Count ?? 0) > 0)
890 {
891 foreach (GeoSubscription Subscription in PrevSubscriptions.Values)
892 {
893 try
894 {
895 await Subscription.ObjectRemoved(Ref, this);
896 }
897 catch (Exception ex)
898 {
899 Log.Exception(ex);
900 }
901 }
902 }
903 }
904
905 private async Task CheckItemExpired(object State)
906 {
907 if (!(State is string GeoId))
908 return;
909
910 if (!this.geoPositions.TryGetObject(GeoId, out PersistedGeoSpatialObjectReference Ref))
911 return;
912
913 if (!Ref.Expires.HasValue || Ref.Expires.Value > DateTime.Now)
914 return;
915
916 this.RemoveReference(GeoId);
917
918 if (!string.IsNullOrEmpty(Ref.ObjectId))
919 await Database.Delete(Ref);
920
921 foreach (GeoSubscription Subscription in this.geoSubscriptions.Find(Ref.Location))
922 {
923 try
924 {
925 await Subscription.ObjectRemoved(Ref, this);
926 }
927 catch (Exception ex)
928 {
929 Log.Exception(ex);
930 }
931 }
932 }
933
934 private async Task DeleteHandler(object Sender, IqEventArgs e)
935 {
936 string Id = XML.Attribute(e.Query, "id");
937
938 if (!this.geoPositions.TryGetObject(Id, out PersistedGeoSpatialObjectReference Ref))
939 {
940 await e.IqErrorItemNotFound(e.To, "Object not found.", "en");
941 return;
942 }
943
944 if (e.From.BareJid != Ref.Creator)
945 {
946 await e.IqErrorForbidden(e.To, "Object not created by sender.", "en");
947 return;
948 }
949
950 this.RemoveReference(Id);
951
952 if (!string.IsNullOrEmpty(Ref.ObjectId))
953 {
954 await Database.Delete(Ref);
955 Ref.ObjectId = null;
956 }
957
958 GeoSubscription[] Subscriptions = this.geoSubscriptions.Find(Ref.Location);
959
960 StringBuilder Xml = new StringBuilder();
961
962 Xml.Append("<deleted xmlns='");
964 Xml.Append("'/>");
965
966 await e.IqResult(Xml.ToString(), e.To);
967
968 foreach (GeoSubscription Subscription in Subscriptions)
969 {
970 try
971 {
972 await Subscription.ObjectRemoved(Ref, this);
973 }
974 catch (Exception ex)
975 {
976 Log.Exception(ex);
977 }
978 }
979 }
980
981 private async Task SearchHandler(object Sender, IqEventArgs e)
982 {
983 double MinLat = XML.Attribute(e.Query, "minLat", double.NaN);
984 double MaxLat = XML.Attribute(e.Query, "maxLat", double.NaN);
985 double MinLon = XML.Attribute(e.Query, "minLon", double.NaN);
986 double MaxLon = XML.Attribute(e.Query, "maxLon", double.NaN);
987 double? MinAlt = e.Query.HasAttribute("minAlt") ? XML.Attribute(e.Query, "minAlt", double.NaN) : (double?)null;
988 double? MaxAlt = e.Query.HasAttribute("maxAlt") ? XML.Attribute(e.Query, "maxAlt", double.NaN) : (double?)null;
989 string Pattern = XML.Attribute(e.Query, "pattern");
990 string Path = XML.Attribute(e.Query, "path");
991 int Offset = XML.Attribute(e.Query, "offset", 0);
992 int MaxCount = XML.Attribute(e.Query, "maxCount", maxSearchResults);
993 Dictionary<string, string> Prefixes = null;
994 Regex ParsedPattern;
995
996 if (double.IsNaN(MinLat) ||
997 double.IsNaN(MaxLat) ||
998 double.IsNaN(MinLon) ||
999 double.IsNaN(MaxLon) ||
1000 (MinAlt.HasValue && double.IsNaN(MinAlt.Value)) ||
1001 (MaxAlt.HasValue && double.IsNaN(MaxAlt.Value)))
1002 {
1003 await e.IqErrorBadRequest(e.To, "Invalid floating-point attribute values.", "en");
1004 return;
1005 }
1006
1007 if (MinLat < -90 || MinLat > 90 || MaxLat < -90 || MaxLat > 90)
1008 {
1009 await e.IqErrorBadRequest(e.To, "Valid latitude values reside in the range [-90,90].", "en");
1010 return;
1011 }
1012
1013 if (MinLon < -180 || MinLon > 180 || MaxLon < -180 || MaxLon > 180)
1014 {
1015 await e.IqErrorBadRequest(e.To, "Valid longitude values reside in the range [-180,180].", "en");
1016 return;
1017 }
1018
1019 if (MinLat > MaxLat)
1020 {
1021 await e.IqErrorBadRequest(e.To, "Minimum latitude cannot be larger than the maximum latitude.", "en");
1022 return;
1023 }
1024
1025 if (MinAlt.HasValue && MaxAlt.HasValue && MinAlt.Value > MaxAlt.Value)
1026 {
1027 await e.IqErrorBadRequest(e.To, "Minimum altitude cannot be larger than the maximum altitude.", "en");
1028 return;
1029 }
1030
1031 if (MaxCount <= 0)
1032 {
1033 await e.IqErrorBadRequest(e.To, "Invalid maximum number of items to return.", "en");
1034 return;
1035 }
1036
1037 if (Offset < 0)
1038 {
1039 await e.IqErrorBadRequest(e.To, "Invalid offset.", "en");
1040 return;
1041 }
1042
1043 if (string.IsNullOrEmpty(Pattern))
1044 ParsedPattern = null;
1045 else
1046 {
1047 try
1048 {
1049 ParsedPattern = new Regex(Pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant);
1050 }
1051 catch (Exception ex)
1052 {
1053 await e.IqErrorBadRequest(e.To, "Invalid regular expression pattern: " + ex.Message, "en");
1054 return;
1055 }
1056 }
1057
1058 if (string.IsNullOrEmpty(Path))
1059 Prefixes = null;
1060 else
1061 {
1062 Prefixes = new Dictionary<string, string>();
1063
1064 foreach (XmlNode N in e.Query.ChildNodes)
1065 {
1066 if (N is XmlElement E &&
1067 E.LocalName == "namespace" &&
1068 E.NamespaceURI == e.Query.NamespaceURI)
1069 {
1070 string Prefix = XML.Attribute(E, "prefix");
1071 string Value = XML.Attribute(E, "value");
1072
1073 Prefixes[Prefix] = Value;
1074 }
1075 }
1076 }
1077
1078 if (MaxCount > maxSearchResults)
1079 MaxCount = maxSearchResults;
1080
1082 new GeoPosition(MinLat, MinLon, MinAlt),
1083 new GeoPosition(MaxLat, MaxLon, MaxAlt));
1084
1085 Predicate<PersistedGeoSpatialObjectReference> XPathFilter;
1086
1087 if (Prefixes is null)
1088 XPathFilter = null;
1089 else
1090 {
1091 XPathFilter = (Obj) =>
1092 {
1093 if (string.IsNullOrEmpty(Obj.ContentXml))
1094 return false;
1095
1096 try
1097 {
1098 XmlDocument Doc = XML.ParseXml(Obj.ContentXml);
1099
1100 XmlNamespaceManager NamespaceManager = new XmlNamespaceManager(Doc.NameTable);
1101
1102 foreach (KeyValuePair<string, string> P in Prefixes)
1103 {
1104 if (!NamespaceManager.HasNamespace(P.Key))
1105 NamespaceManager.AddNamespace(P.Key, P.Value);
1106 }
1107
1108 return !(Doc.DocumentElement.SelectSingleNode(Path, NamespaceManager) is null);
1109 }
1110 catch (Exception)
1111 {
1112 return false;
1113 }
1114 };
1115 }
1116
1117 // Using semaphore to restrict searches to one at a time, per Bare JID.
1118 using Semaphore Semaphore = await Semaphores.BeginWrite("search:" + e.From.BareJid);
1119
1120 PersistedGeoSpatialObjectReference[] Result = this.geoPositions.Find(Box,
1121 Offset, MaxCount, ParsedPattern, XPathFilter);
1122
1123 StringBuilder Xml = new StringBuilder();
1124
1125 Xml.Append("<references xmlns='");
1126 Xml.Append(e.Query.NamespaceURI);
1127 Xml.Append("' maxCount='");
1128 Xml.Append(MaxCount);
1129 Xml.Append('>');
1130
1131 foreach (PersistedGeoSpatialObjectReference Ref in Result)
1132 Ref.Serialize(Xml);
1133
1134 Xml.Append("</references>");
1135
1136 await e.IqResult(Xml.ToString(), e.To);
1137 }
1138 }
1139}
Contains personal information found in a legal identity.
Helps with common XML-related tasks.
Definition: XML.cs:21
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Definition: XML.cs:1062
static string Encode(string s)
Encodes a string for use in XML.
Definition: XML.cs:29
static XmlDocument ParseXml(string Xml)
Parses an XML Document from its string representation.
Definition: XML.cs:713
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
Static class managing the runtime environment of the IoT Gateway.
Definition: Gateway.cs:147
static DateTime ScheduleEvent(Action< object > Callback, DateTime When, object State)
Schedules a one-time event.
Definition: Gateway.cs:4253
static bool CancelScheduledEvent(DateTime When)
Cancels a scheduled event.
Definition: Gateway.cs:4275
CaseInsensitiveString JID
JID of client connection.
Base class for components.
Definition: Component.cs:17
void RegisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Set handler.
Definition: Component.cs:162
CaseInsensitiveString Subdomain
Subdomain name.
Definition: Component.cs:77
void RegisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool PublishNamespaceAsFeature)
Registers an IQ-Get handler.
Definition: Component.cs:150
async Task< bool > Message(string Type, string Id, XmppAddress To, XmppAddress From, string Language, Stanza Stanza, ISender Sender)
Message stanza.
Definition: Component.cs:510
XmppServer Server
XMPP Server.
Definition: Component.cs:97
bool UnregisterIqGetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Get handler.
Definition: Component.cs:250
bool UnregisterIqSetHandler(string LocalName, string Namespace, EventHandlerAsync< IqEventArgs > Handler, bool RemoveNamespaceAsFeature)
Unregisters an IQ-Set handler.
Definition: Component.cs:263
Event arguments for IQ queries.
Definition: IqEventArgs.cs:12
XmppAddress From
From address attribute
Definition: IqEventArgs.cs:93
Task IqResult(string Xml, string From)
Returns a response to the current request.
Definition: IqEventArgs.cs:113
Task IqErrorResourceConstraint(XmppAddress From, string ErrorText, string Language)
Returns a resource-constraint error.
Definition: IqEventArgs.cs:178
Task IqErrorItemNotFound(XmppAddress From, string ErrorText, string Language)
Returns a item-not-found error.
Definition: IqEventArgs.cs:206
XmlElement Query
Query element, if found, null otherwise.
Definition: IqEventArgs.cs:70
XmppAddress To
To address attribute
Definition: IqEventArgs.cs:88
Task IqErrorBadRequest(XmppAddress From, string ErrorText, string Language)
Returns a bad-request error.
Definition: IqEventArgs.cs:164
Task IqErrorForbidden(XmppAddress From, string ErrorText, string Language)
Returns a forbidden error.
Definition: IqEventArgs.cs:234
Contains information about one XMPP address.
Definition: XmppAddress.cs:9
CaseInsensitiveString BareJid
Bare JID
Definition: XmppAddress.cs:45
Represents a case-insensitive string.
static bool IsNullOrEmpty(CaseInsensitiveString value)
Indicates whether the specified string is null or an CaseInsensitiveString.Empty string.
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static async Task Update(object Object)
Updates an object in the database.
Definition: Database.cs:1211
static async Task Delete(object Object)
Deletes an object in the database.
Definition: Database.cs:1291
static async Task Insert(object Object)
Inserts an object into the default collection of the database.
Definition: Database.cs:97
A chunked list is a linked list of chunks of objects of type T .
Definition: ChunkedList.cs:54
void Add(T Item)
Adds an item to the collection.
Definition: ChunkedList.cs:272
Contains information about a geo-spatial bounding box using the Mercator Projection.
In-memory thread-safe geo-spatial collection of bounding boxes.
In-memory thread-safe geo-spatial collection of points (positions).
Contains information about a position in a geo-spatial coordinate system.
Definition: GeoPosition.cs:17
override bool Equals(object obj)
Definition: GeoPosition.cs:475
Represents a named semaphore, i.e. an object, identified by a name, that allows single concurrent wri...
Definition: Semaphore.cs:19
Static class of application-wide semaphores that can be used to order access to editable objects.
Definition: Semaphores.cs:17
static async Task< Semaphore > BeginWrite(string Key)
Waits until the semaphore identified by Key is ready for writing. Each call to BeginWrite must be fo...
Definition: Semaphores.cs:91
static readonly string[] NamespacesGeoSpatial
Supported geo-spatial namespaces, ordered by preference
async Task Delete(string GeoId)
Deletes a geo-spatial object.
static bool IsNamespaceGeoSpatial(string Namespace)
If a namespace corresponds to a geo-spatial namespace.
async Task Publish(IGeoSpatialObject Object)
Publishes a new or updated geo-spatial object.
async Task Delete(IGeoSpatialObject Object)
Deletes a geo-spatial object.
const string NamespaceGeoSpatialNeuroFoundationV1
urn:nf:iot:geo:1.0
override bool SupportsAccounts
If the component supports accounts (true), or if the subdomain name is the only valid address.
GeoSpatialComponent(XmppServer Server, CaseInsensitiveString Subdomain, string Name)
Legal (digital identities, smart contracts) service component.
Represents a Geo-spatial subscription.
Task ObjectUpdated(PersistedGeoSpatialObjectReference Reference, GeoSpatialComponent Sender)
Geo-spatial object has been updated within the area defined by the subscription.
Task ObjectAdded(PersistedGeoSpatialObjectReference Reference, GeoSpatialComponent Sender)
Geo-spatial object has been added within the area defined by the subscription.
string BoxId
The ID of the geo-spatial bounding box.
Task ObjectRemoved(PersistedGeoSpatialObjectReference Reference, GeoSpatialComponent Sender)
Geo-spatial object has been removed within the area defined by the subscription.
static new async Task< PersistedGeoSpatialObjectReference > Create(IGeoSpatialObject Object)
Create a persisted or persistable geo-spatial object reference from a geo-spatial object.
Interface for objects with a geo-spatial location
bool EphemeralLocation
If the location of the geo-spatial object is ephemeral.
Task< GeoPosition > GetLocation()
Gets the geo-spatial location of the object.
bool HasGeoLocation
If the object has a geo-spatial location.
string GeoId
The ID of the geo-spatial object.
Definition: ImplTypes.g.cs:58
Prefix
SI prefixes. http://physics.nist.gov/cuu/Units/prefixes.html
Definition: Prefixes.cs:11