@tak-ps/node-cot 12.37.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cot.ts CHANGED
@@ -1,15 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
- import protobuf from 'protobufjs';
3
2
  import Err from '@openaddresses/batch-error';
4
- import { diff } from 'json-diff-ts';
5
- import xmljs from 'xml-js';
6
3
  import type { Static } from '@sinclair/typebox';
7
4
  import type {
8
- Feature,
9
5
  Polygon,
10
6
  Position,
11
- FeaturePropertyMission,
12
- FeaturePropertyMissionLayer,
13
7
  } from './types/feature.js';
14
8
  import type {
15
9
  MartiDest,
@@ -21,44 +15,18 @@ import type {
21
15
  SensorAttributes,
22
16
  VideoConnectionEntryAttributes,
23
17
  } from './types/types.js'
24
- import {
25
- InputFeature,
26
- } from './types/feature.js';
27
18
  import Sensor from './sensor.js';
28
- import type { AllGeoJSON } from "@turf/helpers";
29
- import PointOnFeature from '@turf/point-on-feature';
30
- import Truncate from '@turf/truncate';
31
- import { destination } from '@turf/destination';
32
- import Ellipse from '@turf/ellipse';
33
19
  import Util from './utils/util.js';
34
- import Color from './utils/color.js';
35
20
  import JSONCoT, { Detail } from './types/types.js'
36
- import AJV from 'ajv';
37
- import fs from 'fs';
38
- import path from 'node:path';
39
- import { fileURLToPath } from 'node:url';
40
-
41
- // GeoJSON Geospatial ops will truncate to the below
42
- const COORDINATE_PRECISION = 6;
43
-
44
- const protoPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'proto', 'takmessage.proto');
45
- const RootMessage = await protobuf.load(protoPath);
46
-
47
- const pkg = JSON.parse(String(fs.readFileSync(new URL('../package.json', import.meta.url))));
48
-
49
- const checkXML = (new AJV({
50
- allErrors: true,
51
- coerceTypes: true,
52
- allowUnionTypes: true
53
- }))
54
- .compile(JSONCoT);
55
21
 
56
- const checkFeat = (new AJV({
57
- allErrors: true,
58
- coerceTypes: true,
59
- allowUnionTypes: true
60
- }))
61
- .compile(InputFeature);
22
+ export type CoTOptions = {
23
+ creator?: CoT | {
24
+ uid: string,
25
+ type: string,
26
+ callsign: string,
27
+ time?: Date | string,
28
+ }
29
+ }
62
30
 
63
31
  /**
64
32
  * Convert to and from an XML CoT message
@@ -80,36 +48,19 @@ export default class CoT {
80
48
  path: string;
81
49
 
82
50
  constructor(
83
- cot: Buffer | Static<typeof JSONCoT> | object | string,
84
- opts: {
85
- creator?: CoT | {
86
- uid: string,
87
- type: string,
88
- callsign: string,
89
- time?: Date | string,
90
- }
91
- } = {}
51
+ cot: Static<typeof JSONCoT>,
52
+ opts: CoTOptions = {}
92
53
  ) {
93
- if (typeof cot === 'string' || cot instanceof Buffer) {
94
- const raw = xmljs.xml2js(String(cot), { compact: true });
95
- this.raw = raw as Static<typeof JSONCoT>;
96
- } else {
97
- this.raw = cot as Static<typeof JSONCoT>;
98
- }
54
+ this.raw = cot;
99
55
 
100
56
  this.metadata = {};
101
57
  this.path = '/';
102
58
 
103
- if (!this.raw.event._attributes.uid) this.raw.event._attributes.uid = Util.cot_uuid();
104
-
105
- if (process.env.DEBUG_COTS) console.log(JSON.stringify(this.raw))
106
-
107
- checkXML(this.raw);
108
- if (checkXML.errors) throw new Err(400, null, `${checkXML.errors[0].message} (${checkXML.errors[0].instancePath})`);
59
+ if (!this.raw.event._attributes.uid) {
60
+ this.raw.event._attributes.uid = Util.cot_uuid();
61
+ }
109
62
 
110
63
  if (!this.raw.event.detail) this.raw.event.detail = {};
111
- if (!this.raw.event.detail['_flow-tags_']) this.raw.event.detail['_flow-tags_'] = {};
112
- this.raw.event.detail['_flow-tags_'][`NodeCoT-${pkg.version}`] = new Date().toISOString()
113
64
 
114
65
  if (this.raw.event.detail.archive && Object.keys(this.raw.event.detail.archive).length === 0) {
115
66
  this.raw.event.detail.archive = { _attributes: {} };
@@ -123,52 +74,8 @@ export default class CoT {
123
74
  time: opts.creator instanceof CoT ? new Date() : opts.creator.time
124
75
  });
125
76
  }
126
- }
127
-
128
- /**
129
- * Detect difference between CoT messages
130
- * Note: This diffs based on GeoJSON Representation of message
131
- * So if unknown properties are present they will be excluded from the diff
132
- */
133
- isDiff(
134
- cot: CoT,
135
- opts = {
136
- diffMetadata: false,
137
- diffStaleStartTime: false,
138
- diffDest: false,
139
- diffFlow: false
140
- }
141
- ): boolean {
142
- const a = this.to_geojson() as Static<typeof InputFeature>;
143
- const b = cot.to_geojson() as Static<typeof InputFeature>;
144
-
145
- if (!opts.diffDest) {
146
- delete a.properties.dest;
147
- delete b.properties.dest;
148
- }
149
-
150
- if (!opts.diffMetadata) {
151
- delete a.properties.metadata;
152
- delete b.properties.metadata;
153
- }
154
-
155
- if (!opts.diffFlow) {
156
- delete a.properties.flow;
157
- delete b.properties.flow;
158
- }
159
-
160
- if (!opts.diffStaleStartTime) {
161
- delete a.properties.time;
162
- delete a.properties.stale;
163
- delete a.properties.start;
164
- delete b.properties.time;
165
- delete b.properties.stale;
166
- delete b.properties.start;
167
- }
168
77
 
169
- const diffs = diff(a, b);
170
-
171
- return diffs.length > 0;
78
+ if (process.env.DEBUG_COTS) console.log(JSON.stringify(this.raw))
172
79
  }
173
80
 
174
81
  /**
@@ -306,8 +213,8 @@ export default class CoT {
306
213
 
307
214
  position(position?: Static<typeof Position>): Static<typeof Position> {
308
215
  if (position) {
309
- this.raw.event.point._attributes.lon = String(position[0]);
310
- this.raw.event.point._attributes.lat = String(position[1]);
216
+ this.raw.event.point._attributes.lon = position[0];
217
+ this.raw.event.point._attributes.lat = position[1];
311
218
  }
312
219
 
313
220
  return [
@@ -386,398 +293,6 @@ export default class CoT {
386
293
  return this;
387
294
  }
388
295
 
389
- /**
390
- * Return an ATAK Compliant Protobuf
391
- */
392
- to_proto(version = 1): Uint8Array {
393
- if (version < 1 || version > 1) throw new Err(400, null, `Unsupported Proto Version: ${version}`);
394
- const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
395
-
396
- // The spread operator is important to make sure the delete doesn't modify the underlying detail object
397
- const detail = { ...this.raw.event.detail };
398
-
399
- const msg: any = {
400
- cotEvent: {
401
- ...this.raw.event._attributes,
402
- sendTime: new Date(this.raw.event._attributes.time).getTime(),
403
- startTime: new Date(this.raw.event._attributes.start).getTime(),
404
- staleTime: new Date(this.raw.event._attributes.stale).getTime(),
405
- ...this.raw.event.point._attributes,
406
- detail: {
407
- xmlDetail: ''
408
- }
409
- }
410
- };
411
-
412
- let key: keyof Static<typeof Detail>;
413
- for (key in detail) {
414
- if(['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
415
- msg.cotEvent.detail[key] = detail[key]._attributes;
416
- delete detail[key]
417
- }
418
- }
419
-
420
- msg.cotEvent.detail.xmlDetail = xmljs.js2xml({
421
- ...detail,
422
- metadata: this.metadata
423
- }, { compact: true });
424
-
425
- return ProtoMessage.encode(msg).finish();
426
- }
427
-
428
- /**
429
- * Return a GeoJSON Feature from an XML CoT message
430
- */
431
- to_geojson(): Static<typeof Feature> {
432
- const raw: Static<typeof JSONCoT> = JSON.parse(JSON.stringify(this.raw));
433
- if (!raw.event.detail) raw.event.detail = {};
434
- if (!raw.event.detail.contact) raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
435
- if (!raw.event.detail.contact._attributes) raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
436
-
437
- const feat: Static<typeof Feature> = {
438
- id: raw.event._attributes.uid,
439
- type: 'Feature',
440
- properties: {
441
- callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
442
- center: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ],
443
- type: raw.event._attributes.type,
444
- how: raw.event._attributes.how || '',
445
- time: raw.event._attributes.time,
446
- start: raw.event._attributes.start,
447
- stale: raw.event._attributes.stale,
448
- },
449
- geometry: {
450
- type: 'Point',
451
- coordinates: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ]
452
- }
453
- };
454
-
455
- const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
456
- delete contact.callsign;
457
- if (Object.keys(contact).length) {
458
- feat.properties.contact = contact;
459
- }
460
-
461
- if (this.creator()) {
462
- feat.properties.creator = this.creator();
463
- }
464
-
465
- if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
466
- feat.properties.remarks = raw.event.detail.remarks._text;
467
- }
468
-
469
- if (raw.event.detail.fileshare) {
470
- feat.properties.fileshare = raw.event.detail.fileshare._attributes;
471
- if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
472
- feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes)
473
- }
474
- }
475
-
476
- if (raw.event.detail.__milsym) {
477
- feat.properties.milsym = {
478
- id: raw.event.detail.__milsym._attributes.id
479
- }
480
- }
481
-
482
- if (raw.event.detail.sensor) {
483
- feat.properties.sensor = raw.event.detail.sensor._attributes;
484
- }
485
-
486
- if (raw.event.detail.range) {
487
- feat.properties.range = raw.event.detail.range._attributes.value;
488
- }
489
-
490
- if (raw.event.detail.bearing) {
491
- feat.properties.bearing = raw.event.detail.bearing._attributes.value;
492
- }
493
-
494
- if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
495
- feat.properties.video = raw.event.detail.__video._attributes;
496
-
497
- if (raw.event.detail.__video.ConnectionEntry) {
498
- feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
499
- }
500
- }
501
-
502
- if (raw.event.detail.__geofence) {
503
- feat.properties.geofence = raw.event.detail.__geofence._attributes;
504
- }
505
-
506
- if (raw.event.detail.ackrequest) {
507
- feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
508
- }
509
-
510
- if (raw.event.detail.attachment_list) {
511
- feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
512
- }
513
-
514
- if (raw.event.detail.link) {
515
- if (!Array.isArray(raw.event.detail.link)) raw.event.detail.link = [raw.event.detail.link];
516
-
517
- feat.properties.links = raw.event.detail.link.filter((link: Static<typeof Link>) => {
518
- return !!link._attributes.url
519
- }).map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
520
- return link._attributes;
521
- });
522
-
523
- if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
524
- }
525
-
526
- if (raw.event.detail.archive) {
527
- feat.properties.archived = true;
528
- }
529
-
530
- if (raw.event.detail.__chat) {
531
- feat.properties.chat = {
532
- ...raw.event.detail.__chat._attributes,
533
- chatgrp: raw.event.detail.__chat.chatgrp
534
- }
535
- }
536
-
537
- if (raw.event.detail.track && raw.event.detail.track._attributes) {
538
- if (raw.event.detail.track._attributes.course) feat.properties.course = Number(raw.event.detail.track._attributes.course);
539
- if (raw.event.detail.track._attributes.slope) feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
540
- if (raw.event.detail.track._attributes.course) feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
541
- }
542
-
543
- if (raw.event.detail.marti && raw.event.detail.marti.dest) {
544
- if (!Array.isArray(raw.event.detail.marti.dest)) raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
545
-
546
- const dest: Array<Static<typeof MartiDestAttributes>> = raw.event.detail.marti.dest.map((d: Static<typeof MartiDest>) => {
547
- return { ...d._attributes };
548
- });
549
-
550
- feat.properties.dest = dest.length === 1 ? dest[0] : dest
551
- }
552
-
553
- if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
554
- feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
555
- }
556
-
557
-
558
- if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
559
- feat.properties.droid = raw.event.detail.uid._attributes.Droid;
560
- }
561
-
562
- if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
563
- feat.properties.takv = raw.event.detail.takv._attributes;
564
- }
565
-
566
- if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
567
- feat.properties.group = raw.event.detail.__group._attributes;
568
- }
569
-
570
- if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
571
- feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
572
- }
573
-
574
- if (raw.event.detail.status && raw.event.detail.status._attributes) {
575
- feat.properties.status = raw.event.detail.status._attributes;
576
- }
577
-
578
- if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
579
- const mission: Static<typeof FeaturePropertyMission> = {
580
- ...raw.event.detail.mission._attributes
581
- };
582
-
583
- if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
584
- const changes =
585
- Array.isArray(raw.event.detail.mission.MissionChanges)
586
- ? raw.event.detail.mission.MissionChanges
587
- : [ raw.event.detail.mission.MissionChanges ]
588
-
589
- mission.missionChanges = []
590
- for (const change of changes) {
591
- mission.missionChanges.push({
592
- contentUid: change.MissionChange.contentUid._text,
593
- creatorUid: change.MissionChange.creatorUid._text,
594
- isFederatedChange: change.MissionChange.isFederatedChange._text,
595
- missionName: change.MissionChange.missionName._text,
596
- timestamp: change.MissionChange.timestamp._text,
597
- type: change.MissionChange.type._text,
598
- details: {
599
- ...change.MissionChange.details._attributes,
600
- ...change.MissionChange.details.location
601
- ? change.MissionChange.details.location._attributes
602
- : {}
603
- }
604
- })
605
- }
606
- }
607
-
608
-
609
- if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
610
- const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};
611
-
612
- if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
613
- missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
614
- }
615
- if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
616
- missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
617
- }
618
- if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
619
- missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
620
- }
621
- if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
622
- missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
623
- }
624
-
625
- mission.missionLayer = missionLayer;
626
- }
627
-
628
- feat.properties.mission = mission;
629
- }
630
-
631
- if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
632
- feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
633
- }
634
-
635
- // Line or Polygon style types
636
- if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
637
- const coordinates = [];
638
-
639
- for (const l of raw.event.detail.link) {
640
- if (!l._attributes.point) continue;
641
- coordinates.push(l._attributes.point.split(',').map((p: string) => { return Number(p.trim()) }).splice(0, 2).reverse());
642
- }
643
-
644
- if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
645
- const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
646
- feat.properties.stroke = stroke.as_hex();
647
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
648
- }
649
-
650
- if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
651
- feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
652
- }
653
-
654
- if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
655
- feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
656
- }
657
-
658
- // Range & Bearing Line
659
- if (raw.event._attributes.type === 'u-rb-a') {
660
- const detail = this.detail();
661
-
662
- if (!detail.range) throw new Error('Range value not provided')
663
- if (!detail.bearing) throw new Error('Bearing value not provided')
664
-
665
- // TODO Support inclination
666
- const dest = destination(
667
- this.position(),
668
- detail.range._attributes.value / 1000,
669
- detail.bearing._attributes.value
670
- ).geometry.coordinates;
671
-
672
- feat.geometry = {
673
- type: 'LineString',
674
- coordinates: [this.position(), dest]
675
- };
676
- } else if (raw.event._attributes.type === 'u-d-r' || (coordinates[0][0] === coordinates[coordinates.length -1][0] && coordinates[0][1] === coordinates[coordinates.length -1][1])) {
677
- if (raw.event._attributes.type === 'u-d-r') {
678
- // CoT rectangles are only 4 points - GeoJSON needs to be closed
679
- coordinates.push(coordinates[0])
680
- }
681
-
682
- feat.geometry = {
683
- type: 'Polygon',
684
- coordinates: [coordinates]
685
- }
686
-
687
- if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
688
- const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
689
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
690
- feat.properties['fill'] = fill.as_hex();
691
- }
692
- } else {
693
- feat.geometry = {
694
- type: 'LineString',
695
- coordinates
696
- }
697
- }
698
- } else if (raw.event._attributes.type.startsWith('u-d-c-c')) {
699
- if (!raw.event.detail.shape) throw new Err(400, null, 'u-d-c-c (Circle) must define shape value')
700
- if (
701
- !raw.event.detail.shape.ellipse
702
- || !raw.event.detail.shape.ellipse._attributes
703
- ) throw new Err(400, null, 'u-d-c-c (Circle) must define ellipse shape value')
704
-
705
- const ellipse = {
706
- major: Number(raw.event.detail.shape.ellipse._attributes.major),
707
- minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
708
- angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
709
- }
710
-
711
- feat.geometry = Truncate(Ellipse(
712
- feat.geometry.coordinates as number[],
713
- Number(ellipse.major) / 1000,
714
- Number(ellipse.minor) / 1000,
715
- {
716
- angle: ellipse.angle
717
- }
718
- ), {
719
- precision: COORDINATE_PRECISION,
720
- mutate: true
721
- }).geometry as Static<typeof Polygon>;
722
-
723
- feat.properties.shape = {};
724
- feat.properties.shape.ellipse = ellipse;
725
- } else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
726
- // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
727
- if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
728
- const coordinates = [];
729
-
730
- const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
731
- for (const v of vertices) {
732
- coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
733
- }
734
-
735
- if (coordinates.length === 1) {
736
- feat.geometry = { type: 'Point', coordinates: coordinates[0] }
737
- } else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
738
- coordinates.push(coordinates[0]);
739
- feat.geometry = { type: 'Polygon', coordinates: [coordinates] }
740
- } else {
741
- feat.geometry = { type: 'LineString', coordinates }
742
- }
743
- }
744
-
745
- if (
746
- raw.event.detail.shape
747
- && raw.event.detail.shape.polyline
748
- && raw.event.detail.shape.polyline._attributes
749
- && raw.event.detail.shape.polyline._attributes
750
- ) {
751
- if (raw.event.detail.shape.polyline._attributes.fillColor) {
752
- const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
753
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
754
- feat.properties['fill'] = fill.as_hex();
755
- }
756
-
757
- if (raw.event.detail.shape.polyline._attributes.color) {
758
- const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
759
- feat.properties.stroke = stroke.as_hex();
760
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
761
- }
762
- }
763
- }
764
-
765
- if (raw.event.detail.color && raw.event.detail.color._attributes && raw.event.detail.color._attributes.argb) {
766
- const color = new Color(Number(raw.event.detail.color._attributes.argb));
767
- feat.properties['marker-color'] = color.as_hex();
768
- feat.properties['marker-opacity'] = color.as_opacity() / 255;
769
- }
770
-
771
- feat.properties.metadata = this.metadata;
772
- feat.path = this.path;
773
-
774
- return feat;
775
- }
776
-
777
- to_xml(): string {
778
- return xmljs.js2xml(this.raw, { compact: true });
779
- }
780
-
781
296
  is_stale(): boolean {
782
297
  return new Date(this.raw.event._attributes.stale) < new Date();
783
298
  }
@@ -962,69 +477,6 @@ export default class CoT {
962
477
  return !!this.raw.event._attributes.type.match(/^a-f-A-M-F-Q-r/)
963
478
  }
964
479
 
965
- /**
966
- * Parse an ATAK compliant Protobuf to a JS Object
967
- */
968
- static from_proto(raw: Uint8Array, version = 1): CoT {
969
- const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
970
-
971
- // TODO Type this
972
- const msg: any = ProtoMessage.decode(raw);
973
-
974
- if (!msg.cotEvent) throw new Err(400, null, 'No cotEvent Data');
975
-
976
- const detail: Record<string, any> = {};
977
- const metadata: Record<string, unknown> = {};
978
- for (const key in msg.cotEvent.detail) {
979
- if (key === 'xmlDetail') {
980
- const parsed: any = xmljs.xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });
981
- Object.assign(detail, parsed.detail);
982
-
983
- if (detail.metadata) {
984
- for (const key in detail.metadata) {
985
- metadata[key] = detail.metadata[key]._text;
986
- }
987
- delete detail.metadata;
988
- }
989
- } else if (key === 'group') {
990
- if (msg.cotEvent.detail[key]) {
991
- detail.__group = { _attributes: msg.cotEvent.detail[key] };
992
- }
993
- } else if (['contact', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
994
- if (msg.cotEvent.detail[key]) {
995
- detail[key] = { _attributes: msg.cotEvent.detail[key] };
996
- }
997
- }
998
- }
999
-
1000
- const cot = new CoT({
1001
- event: {
1002
- _attributes: {
1003
- version: '2.0',
1004
- uid: msg.cotEvent.uid, type: msg.cotEvent.type, how: msg.cotEvent.how,
1005
- qos: msg.cotEvent.qos, opex: msg.cotEvent.opex, access: msg.cotEvent.access,
1006
- time: new Date(msg.cotEvent.sendTime.toNumber()).toISOString(),
1007
- start: new Date(msg.cotEvent.startTime.toNumber()).toISOString(),
1008
- stale: new Date(msg.cotEvent.staleTime.toNumber()).toISOString(),
1009
- },
1010
- detail,
1011
- point: {
1012
- _attributes: {
1013
- lat: msg.cotEvent.lat,
1014
- lon: msg.cotEvent.lon,
1015
- hae: msg.cotEvent.hae,
1016
- le: msg.cotEvent.le,
1017
- ce: msg.cotEvent.ce,
1018
- },
1019
- }
1020
- }
1021
- });
1022
-
1023
- cot.metadata = metadata;
1024
-
1025
- return cot;
1026
- }
1027
-
1028
480
  /**
1029
481
  * Return a CoT Message
1030
482
  */
@@ -1037,297 +489,4 @@ export default class CoT {
1037
489
  }
1038
490
  });
1039
491
  }
1040
-
1041
- /**
1042
- * Return an CoT Message given a GeoJSON Feature
1043
- *
1044
- * @param {Object} feature GeoJSON Point Feature
1045
- *
1046
- * @return {CoT}
1047
- */
1048
- static from_geojson(feature: Static<typeof InputFeature>): CoT {
1049
- checkFeat(feature);
1050
- if (checkFeat.errors) throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
1051
-
1052
- const cot: Static<typeof JSONCoT> = {
1053
- event: {
1054
- _attributes: Util.cot_event_attr(
1055
- feature.properties.type || 'a-f-G',
1056
- feature.properties.how || 'm-g',
1057
- feature.properties.time,
1058
- feature.properties.start,
1059
- feature.properties.stale
1060
- ),
1061
- point: Util.cot_point(),
1062
- detail: Util.cot_event_detail(feature.properties.callsign)
1063
- }
1064
- };
1065
-
1066
- if (feature.id) cot.event._attributes.uid = String(feature.id);
1067
- if (feature.properties.callsign && !feature.id) cot.event._attributes.uid = feature.properties.callsign;
1068
- if (!cot.event.detail) cot.event.detail = {};
1069
-
1070
- if (feature.properties.droid) {
1071
- cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
1072
- }
1073
-
1074
- if (feature.properties.archived) {
1075
- cot.event.detail.archive = { _attributes: { } };
1076
- }
1077
-
1078
- if (feature.properties.links) {
1079
- if (!cot.event.detail.link) cot.event.detail.link = [];
1080
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link];
1081
-
1082
- cot.event.detail.link.push(...feature.properties.links.map((link: Static<typeof LinkAttributes>) => {
1083
- return { _attributes: link };
1084
- }))
1085
- }
1086
-
1087
- if (feature.properties.dest) {
1088
- const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
1089
-
1090
- cot.event.detail.marti = {
1091
- dest: dest.map((dest) => {
1092
- return { _attributes: { ...dest } };
1093
- })
1094
- }
1095
- }
1096
-
1097
- if (feature.properties.takv) {
1098
- cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
1099
- }
1100
-
1101
- if (feature.properties.creator) {
1102
- cot.event.detail.creator = { _attributes: { ...feature.properties.creator } };
1103
- }
1104
-
1105
- if (feature.properties.range !== undefined) {
1106
- cot.event.detail.range = { _attributes: { value: feature.properties.range } }
1107
- }
1108
-
1109
- if (feature.properties.bearing !== undefined) {
1110
- cot.event.detail.bearing = { _attributes: { value: feature.properties.bearing } }
1111
- }
1112
-
1113
- if (feature.properties.geofence) {
1114
- cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
1115
- }
1116
-
1117
- if (feature.properties.milsym) {
1118
- cot.event.detail.__milsym = { _attributes: { id: feature.properties.milsym.id} };
1119
- }
1120
-
1121
- if (feature.properties.sensor) {
1122
- cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
1123
- }
1124
-
1125
- if (feature.properties.ackrequest) {
1126
- cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
1127
- }
1128
-
1129
- if (feature.properties.video) {
1130
- if (feature.properties.video.connection) {
1131
- const video = JSON.parse(JSON.stringify(feature.properties.video));
1132
-
1133
- const connection = video.connection;
1134
- delete video.connection;
1135
-
1136
- cot.event.detail.__video = {
1137
- _attributes: { ...video },
1138
- ConnectionEntry: {
1139
- _attributes: connection
1140
- }
1141
- }
1142
- } else {
1143
- cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
1144
- }
1145
- }
1146
-
1147
- if (feature.properties.attachments) {
1148
- cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
1149
- }
1150
-
1151
- if (feature.properties.contact) {
1152
- cot.event.detail.contact = {
1153
- _attributes: {
1154
- ...feature.properties.contact,
1155
- callsign: feature.properties.callsign || 'UNKNOWN',
1156
- }
1157
- };
1158
- }
1159
-
1160
- if (feature.properties.fileshare) {
1161
- cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
1162
- }
1163
-
1164
- if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
1165
- cot.event.detail.track = {
1166
- _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
1167
- }
1168
- }
1169
-
1170
- if (feature.properties.group) {
1171
- cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
1172
- }
1173
-
1174
- if (feature.properties.flow) {
1175
- cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
1176
- }
1177
-
1178
- if (feature.properties.status) {
1179
- cot.event.detail.status = { _attributes: { ...feature.properties.status } }
1180
- }
1181
-
1182
- if (feature.properties.precisionlocation) {
1183
- cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
1184
- }
1185
-
1186
- if (feature.properties.icon) {
1187
- cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } }
1188
- }
1189
-
1190
- if (feature.properties.mission) {
1191
- cot.event.detail.mission = {
1192
- _attributes: {
1193
- type: feature.properties.mission.type,
1194
- guid: feature.properties.mission.guid,
1195
- tool: feature.properties.mission.tool,
1196
- name: feature.properties.mission.name,
1197
- authorUid: feature.properties.mission.authorUid,
1198
- }
1199
- }
1200
-
1201
- if (feature.properties.mission.missionLayer) {
1202
- cot.event.detail.mission.missionLayer = {};
1203
-
1204
- if (feature.properties.mission.missionLayer.name) {
1205
- cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
1206
- }
1207
-
1208
- if (feature.properties.mission.missionLayer.parentUid) {
1209
- cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
1210
- }
1211
-
1212
- if (feature.properties.mission.missionLayer.type) {
1213
- cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
1214
- }
1215
-
1216
- if (feature.properties.mission.missionLayer.uid) {
1217
- cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
1218
- }
1219
- }
1220
- }
1221
-
1222
- cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
1223
-
1224
- if (!feature.geometry) {
1225
- throw new Err(400, null, 'Must have Geometry');
1226
- } else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
1227
- throw new Err(400, null, 'Unsupported Geometry Type');
1228
- }
1229
-
1230
- if (feature.geometry.type === 'Point') {
1231
- cot.event.point._attributes.lon = String(feature.geometry.coordinates[0]);
1232
- cot.event.point._attributes.lat = String(feature.geometry.coordinates[1]);
1233
- cot.event.point._attributes.hae = String(feature.geometry.coordinates[2] || '0.0');
1234
-
1235
-
1236
- if (feature.properties['marker-color']) {
1237
- const color = new Color(feature.properties['marker-color'] || -1761607936);
1238
- color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
1239
- cot.event.detail.color = { _attributes: { argb: String(color.as_32bit()) } };
1240
- }
1241
- } else if (feature.geometry.type === 'Polygon' && feature.properties.type === 'u-d-c-c') {
1242
- if (!feature.properties.shape || !feature.properties.shape.ellipse) {
1243
- throw new Err(400, null, 'u-d-c-c (Circle) must define a feature.properties.shape.ellipse property')
1244
- }
1245
- cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } }
1246
-
1247
- if (feature.properties.center) {
1248
- cot.event.point._attributes.lon = String(feature.properties.center[0]);
1249
- cot.event.point._attributes.lat = String(feature.properties.center[1]);
1250
- } else {
1251
- const centre = PointOnFeature(feature as AllGeoJSON);
1252
- cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
1253
- cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
1254
- cot.event.point._attributes.hae = '0.0';
1255
- }
1256
- } else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
1257
- const stroke = new Color(feature.properties.stroke || -1761607936);
1258
- stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
1259
- cot.event.detail.strokeColor = { _attributes: { value: String(stroke.as_32bit()) } };
1260
-
1261
- if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
1262
- cot.event.detail.strokeWeight = { _attributes: {
1263
- value: String(feature.properties['stroke-width'])
1264
- } };
1265
-
1266
- if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
1267
- cot.event.detail.strokeStyle = { _attributes: {
1268
- value: feature.properties['stroke-style']
1269
- } };
1270
-
1271
- if (feature.geometry.type === 'LineString') {
1272
- cot.event._attributes.type = 'u-d-f';
1273
-
1274
- if (!cot.event.detail.link) cot.event.detail.link = [];
1275
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
1276
-
1277
- for (const coord of feature.geometry.coordinates) {
1278
- cot.event.detail.link.push({
1279
- _attributes: { point: `${coord[1]},${coord[0]}` }
1280
- });
1281
- }
1282
- } else if (feature.geometry.type === 'Polygon') {
1283
- cot.event._attributes.type = 'u-d-f';
1284
-
1285
- if (!cot.event.detail.link) cot.event.detail.link = [];
1286
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
1287
-
1288
- // Inner rings are not yet supported
1289
- for (const coord of feature.geometry.coordinates[0]) {
1290
- cot.event.detail.link.push({
1291
- _attributes: { point: `${coord[1]},${coord[0]}` }
1292
- });
1293
- }
1294
-
1295
- const fill = new Color(feature.properties.fill || -1761607936);
1296
- fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
1297
- cot.event.detail.fillColor = { _attributes: { value: String(fill.as_32bit()) } };
1298
- }
1299
-
1300
- cot.event.detail.labels_on = { _attributes: { value: 'false' } };
1301
- cot.event.detail.tog = { _attributes: { enabled: '0' } };
1302
-
1303
- if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
1304
- cot.event.point._attributes.lon = String(feature.properties.center[0]);
1305
- cot.event.point._attributes.lat = String(feature.properties.center[1]);
1306
-
1307
- if (feature.properties.center.length >= 3) {
1308
- cot.event.point._attributes.hae = String(feature.properties.center[2] || '0.0');
1309
- } else {
1310
- cot.event.point._attributes.hae = '0.0';
1311
- }
1312
- } else {
1313
- const centre = PointOnFeature(feature as AllGeoJSON);
1314
- cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
1315
- cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
1316
- cot.event.point._attributes.hae = '0.0';
1317
- }
1318
- }
1319
-
1320
- const newcot = new CoT(cot);
1321
-
1322
- if (feature.properties.metadata) {
1323
- newcot.metadata = feature.properties.metadata
1324
- }
1325
-
1326
- if (feature.path) {
1327
- newcot.path = feature.path
1328
- }
1329
-
1330
- return newcot;
1331
- }
1332
-
1333
492
  }