@tak-ps/node-cot 13.5.0 → 14.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/parser.ts CHANGED
@@ -2,31 +2,19 @@ import protobuf from 'protobufjs';
2
2
  import Err from '@openaddresses/batch-error';
3
3
  import { xml2js, js2xml } from 'xml-js';
4
4
  import { diff } from 'json-diff-ts';
5
- import { v4 as randomUUID } from 'uuid';
6
5
  import type { Static } from '@sinclair/typebox';
6
+ import { from_geojson } from './parser/from_geojson.js';
7
+ import { normalize_geojson } from './parser/normalize_geojson.js';
8
+ import { to_geojson } from './parser/to_geojson.js';
7
9
  import type {
8
10
  Feature,
9
- Polygon,
10
- FeaturePropertyMission,
11
- FeaturePropertyMissionLayer,
12
11
  } from './types/feature.js';
13
12
  import type {
14
- MartiDest,
15
- MartiDestAttributes,
16
- Link,
17
- LinkAttributes,
18
- ColorAttributes,
19
- } from './types/types.js'
13
+ GeoJSONFeature,
14
+ } from './types/geojson.js';
20
15
  import {
21
16
  InputFeature,
22
17
  } from './types/feature.js';
23
- import type { AllGeoJSON } from "@turf/helpers";
24
- import Ellipse from '@turf/ellipse';
25
- import PointOnFeature from '@turf/point-on-feature';
26
- import Truncate from '@turf/truncate';
27
- import { destination } from '@turf/destination';
28
- import Util from './utils/util.js';
29
- import Color from './utils/color.js';
30
18
  import JSONCoT, { Detail } from './types/types.js'
31
19
  import CoT from './cot.js';
32
20
  import type { CoTOptions } from './cot.js';
@@ -35,9 +23,6 @@ import fs from 'fs';
35
23
  import path from 'node:path';
36
24
  import { fileURLToPath } from 'node:url';
37
25
 
38
- // GeoJSON Geospatial ops will truncate to the below
39
- const COORDINATE_PRECISION = 6;
40
-
41
26
  const protoPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'proto', 'takmessage.proto');
42
27
  const RootMessage = await protobuf.load(protoPath);
43
28
 
@@ -50,13 +35,6 @@ const checkXML = (new AJV({
50
35
  }))
51
36
  .compile(JSONCoT);
52
37
 
53
- const checkFeat = (new AJV({
54
- allErrors: true,
55
- coerceTypes: true,
56
- allowUnionTypes: true
57
- }))
58
- .compile(InputFeature);
59
-
60
38
  /**
61
39
  * Convert to and from an XML CoT message
62
40
  * @class
@@ -97,7 +75,7 @@ export class CoTParser {
97
75
  * Note: This diffs based on GeoJSON Representation of message
98
76
  * So if unknown properties are present they will be excluded from the diff
99
77
  */
100
- static isDiff(
78
+ static async isDiff(
101
79
  aCoT: CoT,
102
80
  bCoT: CoT,
103
81
  opts = {
@@ -106,9 +84,9 @@ export class CoTParser {
106
84
  diffDest: false,
107
85
  diffFlow: false
108
86
  }
109
- ): boolean {
110
- const a = this.to_geojson(aCoT) as Static<typeof InputFeature>;
111
- const b = this.to_geojson(bCoT) as Static<typeof InputFeature>;
87
+ ): Promise<boolean> {
88
+ const a = await this.to_geojson(aCoT) as Static<typeof InputFeature>;
89
+ const b = await this.to_geojson(bCoT) as Static<typeof InputFeature>;
112
90
 
113
91
  if (!opts.diffDest) {
114
92
  delete a.properties.dest;
@@ -159,7 +137,7 @@ export class CoTParser {
159
137
  /**
160
138
  * Return an ATAK Compliant Protobuf
161
139
  */
162
- static to_proto(cot: CoT, version = 1): Uint8Array {
140
+ static async to_proto(cot: CoT, version = 1): Promise<Uint8Array> {
163
141
  if (version < 1 || version > 1) throw new Err(400, null, `Unsupported Proto Version: ${version}`);
164
142
  const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
165
143
 
@@ -198,382 +176,18 @@ export class CoTParser {
198
176
  /**
199
177
  * Return a GeoJSON Feature from an XML CoT message
200
178
  */
201
- static to_geojson(cot: CoT): Static<typeof Feature> {
202
- const raw: Static<typeof JSONCoT> = JSON.parse(JSON.stringify(cot.raw));
203
- if (!raw.event.detail) raw.event.detail = {};
204
- if (!raw.event.detail.contact) raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
205
- if (!raw.event.detail.contact._attributes) raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
206
-
207
- const feat: Static<typeof Feature> = {
208
- id: raw.event._attributes.uid,
209
- type: 'Feature',
210
- properties: {
211
- callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
212
- center: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ],
213
- type: raw.event._attributes.type,
214
- how: raw.event._attributes.how || '',
215
- time: raw.event._attributes.time,
216
- start: raw.event._attributes.start,
217
- stale: raw.event._attributes.stale,
218
- },
219
- geometry: {
220
- type: 'Point',
221
- coordinates: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ]
222
- }
223
- };
224
-
225
- const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
226
- delete contact.callsign;
227
- if (Object.keys(contact).length) {
228
- feat.properties.contact = contact;
229
- }
230
-
231
- if (cot.creator()) {
232
- feat.properties.creator = cot.creator();
233
- }
234
-
235
- if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
236
- feat.properties.remarks = raw.event.detail.remarks._text;
237
- }
238
-
239
- if (raw.event.detail.fileshare) {
240
- feat.properties.fileshare = raw.event.detail.fileshare._attributes;
241
- if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
242
- feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes)
243
- }
244
- }
245
-
246
- if (raw.event.detail.__milsym) {
247
- feat.properties.milsym = {
248
- id: raw.event.detail.__milsym._attributes.id
249
- }
250
- }
251
-
252
- if (raw.event.detail.sensor) {
253
- feat.properties.sensor = raw.event.detail.sensor._attributes;
254
- }
255
-
256
- if (raw.event.detail.range) {
257
- feat.properties.range = raw.event.detail.range._attributes.value;
258
- }
259
-
260
- if (raw.event.detail.bearing) {
261
- feat.properties.bearing = raw.event.detail.bearing._attributes.value;
262
- }
263
-
264
- if (raw.event.detail.labels_on && raw.event.detail.labels_on._attributes && raw.event.detail.labels_on._attributes.value !== undefined) {
265
- feat.properties.labels = raw.event.detail.labels_on._attributes.value;
266
- }
267
-
268
- if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
269
- feat.properties.video = raw.event.detail.__video._attributes;
270
-
271
- if (raw.event.detail.__video.ConnectionEntry) {
272
- feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
273
- }
274
- }
275
-
276
- if (raw.event.detail.__geofence) {
277
- feat.properties.geofence = raw.event.detail.__geofence._attributes;
278
- }
279
-
280
- if (raw.event.detail.ackrequest) {
281
- feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
282
- }
283
-
284
- if (raw.event.detail.attachment_list) {
285
- feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
286
- }
287
-
288
- if (raw.event.detail.link) {
289
- if (!Array.isArray(raw.event.detail.link)) raw.event.detail.link = [raw.event.detail.link];
290
-
291
- feat.properties.links = raw.event.detail.link.filter((link: Static<typeof Link>) => {
292
- return !!link._attributes.url
293
- }).map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
294
- return link._attributes;
295
- });
296
-
297
- if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
298
- }
299
-
300
- if (raw.event.detail.archive) {
301
- feat.properties.archived = true;
302
- }
303
-
304
- if (raw.event.detail.__chat) {
305
- feat.properties.chat = {
306
- ...raw.event.detail.__chat._attributes,
307
- chatgrp: raw.event.detail.__chat.chatgrp
308
- }
309
- }
310
-
311
- if (raw.event.detail.track && raw.event.detail.track._attributes) {
312
- if (raw.event.detail.track._attributes.course) feat.properties.course = Number(raw.event.detail.track._attributes.course);
313
- if (raw.event.detail.track._attributes.slope) feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
314
- if (raw.event.detail.track._attributes.course) feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
315
- }
316
-
317
- if (raw.event.detail.marti && raw.event.detail.marti.dest) {
318
- if (!Array.isArray(raw.event.detail.marti.dest)) raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
319
-
320
- const dest: Array<Static<typeof MartiDestAttributes>> = raw.event.detail.marti.dest.map((d: Static<typeof MartiDest>) => {
321
- return { ...d._attributes };
322
- });
323
-
324
- feat.properties.dest = dest.length === 1 ? dest[0] : dest
325
- }
326
-
327
- if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
328
- feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
329
- }
330
-
331
-
332
- if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
333
- feat.properties.droid = raw.event.detail.uid._attributes.Droid;
334
- }
335
-
336
- if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
337
- feat.properties.takv = raw.event.detail.takv._attributes;
338
- }
339
-
340
- if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
341
- feat.properties.group = raw.event.detail.__group._attributes;
342
- }
343
-
344
- if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
345
- feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
346
- }
347
-
348
- if (raw.event.detail.status && raw.event.detail.status._attributes) {
349
- feat.properties.status = raw.event.detail.status._attributes;
350
- }
351
-
352
- if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
353
- const mission: Static<typeof FeaturePropertyMission> = {
354
- ...raw.event.detail.mission._attributes
355
- };
356
-
357
- if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
358
- const changes =
359
- Array.isArray(raw.event.detail.mission.MissionChanges)
360
- ? raw.event.detail.mission.MissionChanges
361
- : [ raw.event.detail.mission.MissionChanges ]
362
-
363
- mission.missionChanges = []
364
- for (const change of changes) {
365
- mission.missionChanges.push({
366
- contentUid: change.MissionChange.contentUid._text,
367
- creatorUid: change.MissionChange.creatorUid._text,
368
- isFederatedChange: change.MissionChange.isFederatedChange._text,
369
- missionName: change.MissionChange.missionName._text,
370
- timestamp: change.MissionChange.timestamp._text,
371
- type: change.MissionChange.type._text,
372
- details: {
373
- ...change.MissionChange.details._attributes,
374
- ...change.MissionChange.details.location
375
- ? change.MissionChange.details.location._attributes
376
- : {}
377
- }
378
- })
379
- }
380
- }
381
-
382
-
383
- if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
384
- const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};
385
-
386
- if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
387
- missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
388
- }
389
- if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
390
- missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
391
- }
392
- if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
393
- missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
394
- }
395
- if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
396
- missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
397
- }
398
-
399
- mission.missionLayer = missionLayer;
400
- }
401
-
402
- feat.properties.mission = mission;
403
- }
404
-
405
- if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
406
- feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
407
- }
408
-
409
- if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
410
- const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
411
- feat.properties.stroke = stroke.as_hex();
412
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
413
- }
414
-
415
- if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
416
- feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
417
- }
418
-
419
- if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
420
- feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
421
- }
422
-
423
- if (raw.event.detail.color) {
424
- let color: Static<typeof ColorAttributes> | null = null;
425
-
426
- if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length > 1) {
427
- color = raw.event.detail.color[0];
428
- if (!color._attributes) color._attributes = {};
429
-
430
- for (let i = raw.event.detail.color.length - 1; i >= 1; i--) {
431
- if (raw.event.detail.color[i]._attributes) {
432
- Object.assign(color._attributes, raw.event.detail.color[i]._attributes);
433
- }
434
- }
435
- } else if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length === 1) {
436
- color = raw.event.detail.color[0];
437
- } else if (!Array.isArray(raw.event.detail.color)) {
438
- color = raw.event.detail.color;
439
- }
440
-
441
- if (color && color._attributes && color._attributes.argb) {
442
- const parsedColor = new Color(Number(color._attributes.argb));
443
- feat.properties['marker-color'] = parsedColor.as_hex();
444
- feat.properties['marker-opacity'] = parsedColor.as_opacity() / 255;
445
- }
446
- }
447
-
448
- // Line, Polygon style types
449
- if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
450
- const coordinates = [];
451
-
452
- for (const l of raw.event.detail.link) {
453
- if (!l._attributes.point) continue;
454
- coordinates.push(l._attributes.point.split(',').map((p: string) => { return Number(p.trim()) }).splice(0, 2).reverse());
455
- }
456
-
457
- // Range & Bearing Line
458
- if (raw.event._attributes.type === 'u-rb-a') {
459
- const detail = cot.detail();
460
-
461
- if (!detail.range) throw new Error('Range value not provided')
462
- if (!detail.bearing) throw new Error('Bearing value not provided')
463
-
464
- // TODO Support inclination
465
- const dest = destination(
466
- cot.position(),
467
- detail.range._attributes.value / 1000,
468
- detail.bearing._attributes.value
469
- ).geometry.coordinates;
470
-
471
- feat.geometry = {
472
- type: 'LineString',
473
- coordinates: [cot.position(), dest]
474
- };
475
- } 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])) {
476
- if (raw.event._attributes.type === 'u-d-r') {
477
- // CoT rectangles are only 4 points - GeoJSON needs to be closed
478
- coordinates.push(coordinates[0])
479
- }
480
-
481
- feat.geometry = {
482
- type: 'Polygon',
483
- coordinates: [coordinates]
484
- }
485
-
486
- if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
487
- const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
488
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
489
- feat.properties['fill'] = fill.as_hex();
490
- }
491
- } else {
492
- feat.geometry = {
493
- type: 'LineString',
494
- coordinates
495
- }
496
- }
497
- } else if (raw.event._attributes.type.startsWith('u-d-c-c') || raw.event._attributes.type.startsWith('u-r-b-c-c')) {
498
- if (!raw.event.detail.shape) throw new Err(400, null, `${raw.event._attributes.type} (Circle) must define shape value`)
499
- if (
500
- !raw.event.detail.shape.ellipse
501
- || !raw.event.detail.shape.ellipse._attributes
502
- ) throw new Err(400, null, `${raw.event._attributes.type} (Circle) must define ellipse shape value`)
503
-
504
- const ellipse = {
505
- major: Number(raw.event.detail.shape.ellipse._attributes.major),
506
- minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
507
- angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
508
- }
509
-
510
- feat.geometry = Truncate(Ellipse(
511
- feat.geometry.coordinates as number[],
512
- Number(ellipse.major) / 1000,
513
- Number(ellipse.minor) / 1000,
514
- {
515
- angle: ellipse.angle
516
- }
517
- ), {
518
- precision: COORDINATE_PRECISION,
519
- mutate: true
520
- }).geometry as Static<typeof Polygon>;
521
-
522
- feat.properties.shape = {};
523
- feat.properties.shape.ellipse = ellipse;
524
- } else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
525
- // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
526
- if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
527
- const coordinates = [];
528
-
529
- const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
530
- for (const v of vertices) {
531
- coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
532
- }
533
-
534
- if (coordinates.length === 1) {
535
- feat.geometry = { type: 'Point', coordinates: coordinates[0] }
536
- } else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
537
- coordinates.push(coordinates[0]);
538
- feat.geometry = { type: 'Polygon', coordinates: [coordinates] }
539
- } else {
540
- feat.geometry = { type: 'LineString', coordinates }
541
- }
542
- }
543
-
544
- if (
545
- raw.event.detail.shape
546
- && raw.event.detail.shape.polyline
547
- && raw.event.detail.shape.polyline._attributes
548
- ) {
549
- if (raw.event.detail.shape.polyline._attributes.fillColor) {
550
- const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
551
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
552
- feat.properties['fill'] = fill.as_hex();
553
- }
554
-
555
- if (raw.event.detail.shape.polyline._attributes.color) {
556
- const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
557
- feat.properties.stroke = stroke.as_hex();
558
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
559
- }
560
- }
561
- }
562
-
563
- feat.properties.metadata = cot.metadata;
564
- feat.path = cot.path;
565
-
566
- return feat;
179
+ static async to_geojson(cot: CoT): Promise<Static<typeof Feature>> {
180
+ return await to_geojson(cot);
567
181
  }
568
182
 
569
183
  /**
570
184
  * Parse an ATAK compliant Protobuf to a JS Object
571
185
  */
572
- static from_proto(
186
+ static async from_proto(
573
187
  raw: Uint8Array,
574
188
  version = 1,
575
189
  opts: CoTOptions = {}
576
- ): CoT {
190
+ ): Promise<CoT> {
577
191
  const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
578
192
 
579
193
  // TODO Type this
@@ -633,6 +247,13 @@ export class CoTParser {
633
247
  return this.validate(cot);
634
248
  }
635
249
 
250
+ static async normalize_geojson(
251
+ feature: Static<typeof GeoJSONFeature>
252
+ ): Promise<Static<typeof Feature>> {
253
+ const feat = await normalize_geojson(feature);
254
+ return feat;
255
+ }
256
+
636
257
  /**
637
258
  * Return an CoT Message given a GeoJSON Feature
638
259
  *
@@ -640,349 +261,12 @@ export class CoTParser {
640
261
  *
641
262
  * @return {CoT}
642
263
  */
643
- static from_geojson(
264
+ static async from_geojson(
644
265
  feature: Static<typeof InputFeature>,
645
266
  opts: CoTOptions = {}
646
- ): CoT {
647
- checkFeat(feature);
648
- if (checkFeat.errors) throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
649
-
650
- const cot: Static<typeof JSONCoT> = {
651
- event: {
652
- _attributes: Util.cot_event_attr(
653
- feature.properties.type || 'a-f-G',
654
- feature.properties.how || 'm-g',
655
- feature.properties.time,
656
- feature.properties.start,
657
- feature.properties.stale
658
- ),
659
- point: Util.cot_point(),
660
- detail: Util.cot_event_detail(feature.properties.callsign)
661
- }
662
- };
663
-
664
- if (feature.id) cot.event._attributes.uid = String(feature.id);
665
- if (feature.properties.callsign && !feature.id) cot.event._attributes.uid = feature.properties.callsign;
666
- if (!cot.event.detail) cot.event.detail = {};
667
-
668
- if (feature.properties.droid) {
669
- cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
670
- }
671
-
672
- if (feature.properties.archived) {
673
- cot.event.detail.archive = { _attributes: { } };
674
- }
675
-
676
- if (feature.properties.links) {
677
- if (!cot.event.detail.link) cot.event.detail.link = [];
678
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link];
679
-
680
- cot.event.detail.link.push(...feature.properties.links.map((link: Static<typeof LinkAttributes>) => {
681
- return { _attributes: link };
682
- }))
683
- }
684
-
685
- if (feature.properties.dest) {
686
- const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
687
-
688
- cot.event.detail.marti = {
689
- dest: dest.map((dest) => {
690
- return { _attributes: { ...dest } };
691
- })
692
- }
693
- }
694
-
695
- if (feature.properties.type === 'b-a-o-tbl') {
696
- cot.event.detail.emergency = {
697
- _attributes: { type: '911 Alert' },
698
- _text: feature.properties.callsign || 'UNKNOWN'
699
- }
700
- } else if (feature.properties.type === 'b-a-o-can') {
701
- cot.event.detail.emergency = {
702
- _attributes: { cancel: true },
703
- _text: feature.properties.callsign || 'UNKNOWN'
704
- }
705
- } else if (feature.properties.type === 'b-a-g') {
706
- cot.event.detail.emergency = {
707
- _attributes: { type: 'Geo-fence Breached' },
708
- _text: feature.properties.callsign || 'UNKNOWN'
709
- }
710
- } else if (feature.properties.type === 'b-a-o-pan') {
711
- cot.event.detail.emergency = {
712
- _attributes: { type: 'Ring The Bell' },
713
- _text: feature.properties.callsign || 'UNKNOWN'
714
- }
715
- } else if (feature.properties.type === 'b-a-o-opn') {
716
- cot.event.detail.emergency = {
717
- _attributes: { type: 'Troops In Contact' },
718
- _text: feature.properties.callsign || 'UNKNOWN'
719
- }
720
- }
721
-
722
- if (feature.properties.takv) {
723
- cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
724
- }
725
-
726
- if (feature.properties.creator) {
727
- cot.event.detail.creator = { _attributes: { ...feature.properties.creator } };
728
- }
729
-
730
- if (feature.properties.range !== undefined) {
731
- cot.event.detail.range = { _attributes: { value: feature.properties.range } }
732
- }
733
-
734
- if (feature.properties.bearing !== undefined) {
735
- cot.event.detail.bearing = { _attributes: { value: feature.properties.bearing } }
736
- }
737
-
738
- if (feature.properties.geofence) {
739
- cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
740
- }
741
-
742
- if (feature.properties.milsym) {
743
- cot.event.detail.__milsym = { _attributes: { id: feature.properties.milsym.id} };
744
- }
745
-
746
- if (feature.properties.sensor) {
747
- cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
748
- }
749
-
750
- if (feature.properties.ackrequest) {
751
- cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
752
- }
753
-
754
- if (feature.properties.video) {
755
- if (feature.properties.video.connection) {
756
- const video = JSON.parse(JSON.stringify(feature.properties.video));
757
-
758
- const connection = video.connection;
759
- delete video.connection;
760
-
761
- cot.event.detail.__video = {
762
- _attributes: { ...video },
763
- ConnectionEntry: {
764
- _attributes: connection
765
- }
766
- }
767
- } else {
768
- cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
769
- }
770
- }
771
-
772
- if (feature.properties.attachments) {
773
- cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
774
- }
775
-
776
- if (feature.properties.contact) {
777
- cot.event.detail.contact = {
778
- _attributes: {
779
- ...feature.properties.contact,
780
- callsign: feature.properties.callsign || 'UNKNOWN',
781
- }
782
- };
783
- }
784
-
785
- if (feature.properties.fileshare) {
786
- cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
787
- }
788
-
789
- if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
790
- cot.event.detail.track = {
791
- _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
792
- }
793
- }
794
-
795
- if (feature.properties.group) {
796
- cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
797
- }
798
-
799
- if (feature.properties.flow) {
800
- cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
801
- }
802
-
803
- if (feature.properties.status) {
804
- cot.event.detail.status = { _attributes: { ...feature.properties.status } }
805
- }
806
-
807
- if (feature.properties.precisionlocation) {
808
- cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
809
- }
810
-
811
- if (feature.properties.icon) {
812
- cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } }
813
- }
814
-
815
- if (feature.properties.mission) {
816
- cot.event.detail.mission = {
817
- _attributes: {
818
- type: feature.properties.mission.type,
819
- guid: feature.properties.mission.guid,
820
- tool: feature.properties.mission.tool,
821
- name: feature.properties.mission.name,
822
- authorUid: feature.properties.mission.authorUid,
823
- }
824
- }
825
-
826
- if (feature.properties.mission.missionLayer) {
827
- cot.event.detail.mission.missionLayer = {};
828
-
829
- if (feature.properties.mission.missionLayer.name) {
830
- cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
831
- }
832
-
833
- if (feature.properties.mission.missionLayer.parentUid) {
834
- cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
835
- }
836
-
837
- if (feature.properties.mission.missionLayer.type) {
838
- cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
839
- }
840
-
841
- if (feature.properties.mission.missionLayer.uid) {
842
- cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
843
- }
844
- }
845
- }
846
-
847
- cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
848
-
849
- if (!feature.geometry) {
850
- throw new Err(400, null, 'Must have Geometry');
851
- } else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
852
- throw new Err(400, null, 'Unsupported Geometry Type');
853
- }
854
-
855
- // This isn't specific to point as the color can apply to the centroid point
856
- if (feature.properties['marker-color']) {
857
- const color = new Color(feature.properties['marker-color'] || -1761607936);
858
- color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
859
-
860
- cot.event.detail.color = {
861
- _attributes: {
862
- argb: color.as_32bit(),
863
- value: color.as_32bit()
864
- }
865
- };
866
- }
867
-
868
- if (feature.geometry.type === 'Point') {
869
- cot.event.point._attributes.lon = feature.geometry.coordinates[0];
870
- cot.event.point._attributes.lat = feature.geometry.coordinates[1];
871
- cot.event.point._attributes.hae = feature.geometry.coordinates[2] || 0.0;
872
- } else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
873
- const stroke = new Color(feature.properties.stroke || -1761607936);
874
- stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
875
- cot.event.detail.strokeColor = { _attributes: { value: stroke.as_32bit() } };
876
-
877
- if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
878
- cot.event.detail.strokeWeight = { _attributes: {
879
- value: feature.properties['stroke-width']
880
- } };
881
-
882
- if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
883
- cot.event.detail.strokeStyle = { _attributes: {
884
- value: feature.properties['stroke-style']
885
- } };
886
-
887
-
888
- if (feature.geometry.type === 'Polygon' && feature.properties.type && ['u-d-c-c', 'u-r-b-c-c'].includes(feature.properties.type)) {
889
- if (!feature.properties.shape || !feature.properties.shape.ellipse) {
890
- throw new Err(400, null, `${feature.properties.type} (Circle) must define a feature.properties.shape.ellipse property`)
891
- }
267
+ ): Promise<CoT> {
268
+ const cot = await from_geojson(feature, opts);
892
269
 
893
- cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } }
894
- } else if (feature.geometry.type === 'LineString' && feature.properties.type === 'b-m-r') {
895
- cot.event._attributes.type = 'b-m-r';
896
-
897
- if (!cot.event.detail.link) {
898
- cot.event.detail.link = [];
899
- } else if (!Array.isArray(cot.event.detail.link)) {
900
- cot.event.detail.link = [cot.event.detail.link]
901
- }
902
-
903
- cot.event.detail.__routeinfo = {
904
- __navcues: {
905
- __navcue: []
906
- }
907
- }
908
-
909
- for (const coord of feature.geometry.coordinates) {
910
- cot.event.detail.link.push({
911
- _attributes: {
912
- type: 'b-m-p-c',
913
- uid: randomUUID(),
914
- callsign: "",
915
- point: `${coord[1]},${coord[0]}`
916
- }
917
- });
918
- }
919
- } else if (feature.geometry.type === 'LineString') {
920
- cot.event._attributes.type = 'u-d-f';
921
-
922
- if (!cot.event.detail.link) {
923
- cot.event.detail.link = [];
924
- } else if (!Array.isArray(cot.event.detail.link)) {
925
- cot.event.detail.link = [cot.event.detail.link]
926
- }
927
-
928
- for (const coord of feature.geometry.coordinates) {
929
- cot.event.detail.link.push({
930
- _attributes: { point: `${coord[1]},${coord[0]}` }
931
- });
932
- }
933
- } else if (feature.geometry.type === 'Polygon') {
934
- cot.event._attributes.type = 'u-d-f';
935
-
936
- if (!cot.event.detail.link) cot.event.detail.link = [];
937
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
938
-
939
- // Inner rings are not yet supported
940
- for (const coord of feature.geometry.coordinates[0]) {
941
- cot.event.detail.link.push({
942
- _attributes: { point: `${coord[1]},${coord[0]}` }
943
- });
944
- }
945
-
946
- const fill = new Color(feature.properties.fill || -1761607936);
947
- fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
948
- cot.event.detail.fillColor = { _attributes: { value: fill.as_32bit() } };
949
- }
950
-
951
- if (feature.properties.labels) {
952
- cot.event.detail.labels_on = { _attributes: { value: feature.properties.labels } };
953
- } else {
954
- cot.event.detail.labels_on = { _attributes: { value: false } };
955
- }
956
-
957
- cot.event.detail.tog = { _attributes: { enabled: '0' } };
958
-
959
- if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
960
- cot.event.point._attributes.lon = feature.properties.center[0];
961
- cot.event.point._attributes.lat = feature.properties.center[1];
962
-
963
- if (feature.properties.center.length >= 3) {
964
- cot.event.point._attributes.hae = feature.properties.center[2] || 0.0;
965
- } else {
966
- cot.event.point._attributes.hae = 0.0;
967
- }
968
- } else {
969
- const centre = PointOnFeature(feature as AllGeoJSON);
970
- cot.event.point._attributes.lon = centre.geometry.coordinates[0];
971
- cot.event.point._attributes.lat = centre.geometry.coordinates[1];
972
- cot.event.point._attributes.hae = 0.0;
973
- }
974
- }
975
-
976
- const newcot = new CoT(cot, opts);
977
-
978
- if (feature.properties.metadata) {
979
- newcot.metadata = feature.properties.metadata
980
- }
981
-
982
- if (feature.path) {
983
- newcot.path = feature.path
984
- }
985
-
986
- return this.validate(newcot);
270
+ return this.validate(cot);
987
271
  }
988
272
  }