@tak-ps/node-cot 12.36.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.
@@ -0,0 +1,742 @@
1
+ import protobuf from 'protobufjs';
2
+ import Err from '@openaddresses/batch-error';
3
+ import { xml2js, js2xml } from 'xml-js';
4
+ import { diff } from 'json-diff-ts';
5
+ import { InputFeature, } from './types/feature.js';
6
+ import Ellipse from '@turf/ellipse';
7
+ import PointOnFeature from '@turf/point-on-feature';
8
+ import Truncate from '@turf/truncate';
9
+ import { destination } from '@turf/destination';
10
+ import Util from './utils/util.js';
11
+ import Color from './utils/color.js';
12
+ import JSONCoT, { Detail } from './types/types.js';
13
+ import CoT from './cot.js';
14
+ import AJV from 'ajv';
15
+ import fs from 'fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ // GeoJSON Geospatial ops will truncate to the below
19
+ const COORDINATE_PRECISION = 6;
20
+ const protoPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'proto', 'takmessage.proto');
21
+ const RootMessage = await protobuf.load(protoPath);
22
+ const pkg = JSON.parse(String(fs.readFileSync(new URL('../package.json', import.meta.url))));
23
+ const checkXML = (new AJV({
24
+ allErrors: true,
25
+ coerceTypes: true,
26
+ allowUnionTypes: true
27
+ }))
28
+ .compile(JSONCoT);
29
+ const checkFeat = (new AJV({
30
+ allErrors: true,
31
+ coerceTypes: true,
32
+ allowUnionTypes: true
33
+ }))
34
+ .compile(InputFeature);
35
+ /**
36
+ * Convert to and from an XML CoT message
37
+ * @class
38
+ *
39
+ * @param cot A string/buffer containing the XML representation or the xml-js object tree
40
+ *
41
+ * @prop raw Raw XML-JS representation of CoT
42
+ */
43
+ export class CoTParser {
44
+ static validate(cot, opts = {
45
+ flow: true
46
+ }) {
47
+ if (opts.flow === undefined)
48
+ opts.flow = true;
49
+ checkXML(cot.raw);
50
+ if (checkXML.errors)
51
+ throw new Err(400, null, `${checkXML.errors[0].message} (${checkXML.errors[0].instancePath})`);
52
+ if (opts.flow) {
53
+ if (!cot.raw.event.detail)
54
+ cot.raw.event.detail = {};
55
+ if (!cot.raw.event.detail['_flow-tags_']) {
56
+ cot.raw.event.detail['_flow-tags_'] = {};
57
+ }
58
+ cot.raw.event.detail['_flow-tags_'][`NodeCoT-${pkg.version}`] = new Date().toISOString();
59
+ }
60
+ return cot;
61
+ }
62
+ /**
63
+ * Detect difference between CoT messages
64
+ * Note: This diffs based on GeoJSON Representation of message
65
+ * So if unknown properties are present they will be excluded from the diff
66
+ */
67
+ static isDiff(aCoT, bCoT, opts = {
68
+ diffMetadata: false,
69
+ diffStaleStartTime: false,
70
+ diffDest: false,
71
+ diffFlow: false
72
+ }) {
73
+ const a = this.to_geojson(aCoT);
74
+ const b = this.to_geojson(bCoT);
75
+ if (!opts.diffDest) {
76
+ delete a.properties.dest;
77
+ delete b.properties.dest;
78
+ }
79
+ if (!opts.diffMetadata) {
80
+ delete a.properties.metadata;
81
+ delete b.properties.metadata;
82
+ }
83
+ if (!opts.diffFlow) {
84
+ delete a.properties.flow;
85
+ delete b.properties.flow;
86
+ }
87
+ if (!opts.diffStaleStartTime) {
88
+ delete a.properties.time;
89
+ delete a.properties.stale;
90
+ delete a.properties.start;
91
+ delete b.properties.time;
92
+ delete b.properties.stale;
93
+ delete b.properties.start;
94
+ }
95
+ const diffs = diff(a, b);
96
+ return diffs.length > 0;
97
+ }
98
+ static from_xml(raw, opts = {}) {
99
+ const cot = new CoT(xml2js(String(raw), { compact: true }), opts);
100
+ return this.validate(cot);
101
+ }
102
+ static to_xml(cot) {
103
+ return js2xml(cot.raw, { compact: true });
104
+ }
105
+ /**
106
+ * Return an ATAK Compliant Protobuf
107
+ */
108
+ static to_proto(cot, version = 1) {
109
+ if (version < 1 || version > 1)
110
+ throw new Err(400, null, `Unsupported Proto Version: ${version}`);
111
+ const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`);
112
+ // The spread operator is important to make sure the delete doesn't modify the underlying detail object
113
+ const detail = { ...cot.raw.event.detail };
114
+ const msg = {
115
+ cotEvent: {
116
+ ...cot.raw.event._attributes,
117
+ sendTime: new Date(cot.raw.event._attributes.time).getTime(),
118
+ startTime: new Date(cot.raw.event._attributes.start).getTime(),
119
+ staleTime: new Date(cot.raw.event._attributes.stale).getTime(),
120
+ ...cot.raw.event.point._attributes,
121
+ detail: {
122
+ xmlDetail: ''
123
+ }
124
+ }
125
+ };
126
+ let key;
127
+ for (key in detail) {
128
+ if (['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
129
+ msg.cotEvent.detail[key] = detail[key]._attributes;
130
+ delete detail[key];
131
+ }
132
+ }
133
+ msg.cotEvent.detail.xmlDetail = js2xml({
134
+ ...detail,
135
+ metadata: cot.metadata
136
+ }, { compact: true });
137
+ return ProtoMessage.encode(msg).finish();
138
+ }
139
+ /**
140
+ * Return a GeoJSON Feature from an XML CoT message
141
+ */
142
+ static to_geojson(cot) {
143
+ const raw = JSON.parse(JSON.stringify(cot.raw));
144
+ if (!raw.event.detail)
145
+ raw.event.detail = {};
146
+ if (!raw.event.detail.contact)
147
+ raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
148
+ if (!raw.event.detail.contact._attributes)
149
+ raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
150
+ const feat = {
151
+ id: raw.event._attributes.uid,
152
+ type: 'Feature',
153
+ properties: {
154
+ callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
155
+ center: [Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae)],
156
+ type: raw.event._attributes.type,
157
+ how: raw.event._attributes.how || '',
158
+ time: raw.event._attributes.time,
159
+ start: raw.event._attributes.start,
160
+ stale: raw.event._attributes.stale,
161
+ },
162
+ geometry: {
163
+ type: 'Point',
164
+ coordinates: [Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae)]
165
+ }
166
+ };
167
+ const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
168
+ delete contact.callsign;
169
+ if (Object.keys(contact).length) {
170
+ feat.properties.contact = contact;
171
+ }
172
+ if (cot.creator()) {
173
+ feat.properties.creator = cot.creator();
174
+ }
175
+ if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
176
+ feat.properties.remarks = raw.event.detail.remarks._text;
177
+ }
178
+ if (raw.event.detail.fileshare) {
179
+ feat.properties.fileshare = raw.event.detail.fileshare._attributes;
180
+ if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
181
+ feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes);
182
+ }
183
+ }
184
+ if (raw.event.detail.__milsym) {
185
+ feat.properties.milsym = {
186
+ id: raw.event.detail.__milsym._attributes.id
187
+ };
188
+ }
189
+ if (raw.event.detail.sensor) {
190
+ feat.properties.sensor = raw.event.detail.sensor._attributes;
191
+ }
192
+ if (raw.event.detail.range) {
193
+ feat.properties.range = raw.event.detail.range._attributes.value;
194
+ }
195
+ if (raw.event.detail.bearing) {
196
+ feat.properties.bearing = raw.event.detail.bearing._attributes.value;
197
+ }
198
+ if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
199
+ feat.properties.video = raw.event.detail.__video._attributes;
200
+ if (raw.event.detail.__video.ConnectionEntry) {
201
+ feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
202
+ }
203
+ }
204
+ if (raw.event.detail.__geofence) {
205
+ feat.properties.geofence = raw.event.detail.__geofence._attributes;
206
+ }
207
+ if (raw.event.detail.ackrequest) {
208
+ feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
209
+ }
210
+ if (raw.event.detail.attachment_list) {
211
+ feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
212
+ }
213
+ if (raw.event.detail.link) {
214
+ if (!Array.isArray(raw.event.detail.link))
215
+ raw.event.detail.link = [raw.event.detail.link];
216
+ feat.properties.links = raw.event.detail.link.filter((link) => {
217
+ return !!link._attributes.url;
218
+ }).map((link) => {
219
+ return link._attributes;
220
+ });
221
+ if (!feat.properties.links || !feat.properties.links.length)
222
+ delete feat.properties.links;
223
+ }
224
+ if (raw.event.detail.archive) {
225
+ feat.properties.archived = true;
226
+ }
227
+ if (raw.event.detail.__chat) {
228
+ feat.properties.chat = {
229
+ ...raw.event.detail.__chat._attributes,
230
+ chatgrp: raw.event.detail.__chat.chatgrp
231
+ };
232
+ }
233
+ if (raw.event.detail.track && raw.event.detail.track._attributes) {
234
+ if (raw.event.detail.track._attributes.course)
235
+ feat.properties.course = Number(raw.event.detail.track._attributes.course);
236
+ if (raw.event.detail.track._attributes.slope)
237
+ feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
238
+ if (raw.event.detail.track._attributes.course)
239
+ feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
240
+ }
241
+ if (raw.event.detail.marti && raw.event.detail.marti.dest) {
242
+ if (!Array.isArray(raw.event.detail.marti.dest))
243
+ raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
244
+ const dest = raw.event.detail.marti.dest.map((d) => {
245
+ return { ...d._attributes };
246
+ });
247
+ feat.properties.dest = dest.length === 1 ? dest[0] : dest;
248
+ }
249
+ if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
250
+ feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
251
+ }
252
+ if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
253
+ feat.properties.droid = raw.event.detail.uid._attributes.Droid;
254
+ }
255
+ if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
256
+ feat.properties.takv = raw.event.detail.takv._attributes;
257
+ }
258
+ if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
259
+ feat.properties.group = raw.event.detail.__group._attributes;
260
+ }
261
+ if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
262
+ feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
263
+ }
264
+ if (raw.event.detail.status && raw.event.detail.status._attributes) {
265
+ feat.properties.status = raw.event.detail.status._attributes;
266
+ }
267
+ if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
268
+ const mission = {
269
+ ...raw.event.detail.mission._attributes
270
+ };
271
+ if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
272
+ const changes = Array.isArray(raw.event.detail.mission.MissionChanges)
273
+ ? raw.event.detail.mission.MissionChanges
274
+ : [raw.event.detail.mission.MissionChanges];
275
+ mission.missionChanges = [];
276
+ for (const change of changes) {
277
+ mission.missionChanges.push({
278
+ contentUid: change.MissionChange.contentUid._text,
279
+ creatorUid: change.MissionChange.creatorUid._text,
280
+ isFederatedChange: change.MissionChange.isFederatedChange._text,
281
+ missionName: change.MissionChange.missionName._text,
282
+ timestamp: change.MissionChange.timestamp._text,
283
+ type: change.MissionChange.type._text,
284
+ details: {
285
+ ...change.MissionChange.details._attributes,
286
+ ...change.MissionChange.details.location
287
+ ? change.MissionChange.details.location._attributes
288
+ : {}
289
+ }
290
+ });
291
+ }
292
+ }
293
+ if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
294
+ const missionLayer = {};
295
+ if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
296
+ missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
297
+ }
298
+ if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
299
+ missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
300
+ }
301
+ if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
302
+ missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
303
+ }
304
+ if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
305
+ missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
306
+ }
307
+ mission.missionLayer = missionLayer;
308
+ }
309
+ feat.properties.mission = mission;
310
+ }
311
+ if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
312
+ feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
313
+ }
314
+ // Line or Polygon style types
315
+ if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
316
+ const coordinates = [];
317
+ for (const l of raw.event.detail.link) {
318
+ if (!l._attributes.point)
319
+ continue;
320
+ coordinates.push(l._attributes.point.split(',').map((p) => { return Number(p.trim()); }).splice(0, 2).reverse());
321
+ }
322
+ if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
323
+ const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
324
+ feat.properties.stroke = stroke.as_hex();
325
+ feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
326
+ }
327
+ if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
328
+ feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
329
+ }
330
+ if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
331
+ feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
332
+ }
333
+ // Range & Bearing Line
334
+ if (raw.event._attributes.type === 'u-rb-a') {
335
+ const detail = cot.detail();
336
+ if (!detail.range)
337
+ throw new Error('Range value not provided');
338
+ if (!detail.bearing)
339
+ throw new Error('Bearing value not provided');
340
+ // TODO Support inclination
341
+ const dest = destination(cot.position(), detail.range._attributes.value / 1000, detail.bearing._attributes.value).geometry.coordinates;
342
+ feat.geometry = {
343
+ type: 'LineString',
344
+ coordinates: [cot.position(), dest]
345
+ };
346
+ }
347
+ 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])) {
348
+ if (raw.event._attributes.type === 'u-d-r') {
349
+ // CoT rectangles are only 4 points - GeoJSON needs to be closed
350
+ coordinates.push(coordinates[0]);
351
+ }
352
+ feat.geometry = {
353
+ type: 'Polygon',
354
+ coordinates: [coordinates]
355
+ };
356
+ if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
357
+ const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
358
+ feat.properties['fill-opacity'] = fill.as_opacity() / 255;
359
+ feat.properties['fill'] = fill.as_hex();
360
+ }
361
+ }
362
+ else {
363
+ feat.geometry = {
364
+ type: 'LineString',
365
+ coordinates
366
+ };
367
+ }
368
+ }
369
+ else if (raw.event._attributes.type.startsWith('u-d-c-c')) {
370
+ if (!raw.event.detail.shape)
371
+ throw new Err(400, null, 'u-d-c-c (Circle) must define shape value');
372
+ if (!raw.event.detail.shape.ellipse
373
+ || !raw.event.detail.shape.ellipse._attributes)
374
+ throw new Err(400, null, 'u-d-c-c (Circle) must define ellipse shape value');
375
+ const ellipse = {
376
+ major: Number(raw.event.detail.shape.ellipse._attributes.major),
377
+ minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
378
+ angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
379
+ };
380
+ feat.geometry = Truncate(Ellipse(feat.geometry.coordinates, Number(ellipse.major) / 1000, Number(ellipse.minor) / 1000, {
381
+ angle: ellipse.angle
382
+ }), {
383
+ precision: COORDINATE_PRECISION,
384
+ mutate: true
385
+ }).geometry;
386
+ feat.properties.shape = {};
387
+ feat.properties.shape.ellipse = ellipse;
388
+ }
389
+ else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
390
+ // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
391
+ if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
392
+ const coordinates = [];
393
+ const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
394
+ for (const v of vertices) {
395
+ coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
396
+ }
397
+ if (coordinates.length === 1) {
398
+ feat.geometry = { type: 'Point', coordinates: coordinates[0] };
399
+ }
400
+ else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
401
+ coordinates.push(coordinates[0]);
402
+ feat.geometry = { type: 'Polygon', coordinates: [coordinates] };
403
+ }
404
+ else {
405
+ feat.geometry = { type: 'LineString', coordinates };
406
+ }
407
+ }
408
+ if (raw.event.detail.shape
409
+ && raw.event.detail.shape.polyline
410
+ && raw.event.detail.shape.polyline._attributes
411
+ && raw.event.detail.shape.polyline._attributes) {
412
+ if (raw.event.detail.shape.polyline._attributes.fillColor) {
413
+ const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
414
+ feat.properties['fill-opacity'] = fill.as_opacity() / 255;
415
+ feat.properties['fill'] = fill.as_hex();
416
+ }
417
+ if (raw.event.detail.shape.polyline._attributes.color) {
418
+ const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
419
+ feat.properties.stroke = stroke.as_hex();
420
+ feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
421
+ }
422
+ }
423
+ }
424
+ if (raw.event.detail.color && raw.event.detail.color._attributes && raw.event.detail.color._attributes.argb) {
425
+ const color = new Color(Number(raw.event.detail.color._attributes.argb));
426
+ feat.properties['marker-color'] = color.as_hex();
427
+ feat.properties['marker-opacity'] = color.as_opacity() / 255;
428
+ }
429
+ feat.properties.metadata = cot.metadata;
430
+ feat.path = cot.path;
431
+ return feat;
432
+ }
433
+ /**
434
+ * Parse an ATAK compliant Protobuf to a JS Object
435
+ */
436
+ static from_proto(raw, version = 1, opts = {}) {
437
+ const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`);
438
+ // TODO Type this
439
+ const msg = ProtoMessage.decode(raw);
440
+ if (!msg.cotEvent)
441
+ throw new Err(400, null, 'No cotEvent Data');
442
+ const detail = {};
443
+ const metadata = {};
444
+ for (const key in msg.cotEvent.detail) {
445
+ if (key === 'xmlDetail') {
446
+ const parsed = xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });
447
+ Object.assign(detail, parsed.detail);
448
+ if (detail.metadata) {
449
+ for (const key in detail.metadata) {
450
+ metadata[key] = detail.metadata[key]._text;
451
+ }
452
+ delete detail.metadata;
453
+ }
454
+ }
455
+ else if (key === 'group') {
456
+ if (msg.cotEvent.detail[key]) {
457
+ detail.__group = { _attributes: msg.cotEvent.detail[key] };
458
+ }
459
+ }
460
+ else if (['contact', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
461
+ if (msg.cotEvent.detail[key]) {
462
+ detail[key] = { _attributes: msg.cotEvent.detail[key] };
463
+ }
464
+ }
465
+ }
466
+ const cot = new CoT({
467
+ event: {
468
+ _attributes: {
469
+ version: '2.0',
470
+ uid: msg.cotEvent.uid, type: msg.cotEvent.type, how: msg.cotEvent.how,
471
+ qos: msg.cotEvent.qos, opex: msg.cotEvent.opex, access: msg.cotEvent.access,
472
+ time: new Date(msg.cotEvent.sendTime.toNumber()).toISOString(),
473
+ start: new Date(msg.cotEvent.startTime.toNumber()).toISOString(),
474
+ stale: new Date(msg.cotEvent.staleTime.toNumber()).toISOString(),
475
+ },
476
+ detail,
477
+ point: {
478
+ _attributes: {
479
+ lat: msg.cotEvent.lat,
480
+ lon: msg.cotEvent.lon,
481
+ hae: msg.cotEvent.hae,
482
+ le: msg.cotEvent.le,
483
+ ce: msg.cotEvent.ce,
484
+ },
485
+ }
486
+ }
487
+ }, opts);
488
+ cot.metadata = metadata;
489
+ return this.validate(cot);
490
+ }
491
+ /**
492
+ * Return an CoT Message given a GeoJSON Feature
493
+ *
494
+ * @param {Object} feature GeoJSON Point Feature
495
+ *
496
+ * @return {CoT}
497
+ */
498
+ static from_geojson(feature, opts = {}) {
499
+ checkFeat(feature);
500
+ if (checkFeat.errors)
501
+ throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
502
+ const cot = {
503
+ event: {
504
+ _attributes: Util.cot_event_attr(feature.properties.type || 'a-f-G', feature.properties.how || 'm-g', feature.properties.time, feature.properties.start, feature.properties.stale),
505
+ point: Util.cot_point(),
506
+ detail: Util.cot_event_detail(feature.properties.callsign)
507
+ }
508
+ };
509
+ if (feature.id)
510
+ cot.event._attributes.uid = String(feature.id);
511
+ if (feature.properties.callsign && !feature.id)
512
+ cot.event._attributes.uid = feature.properties.callsign;
513
+ if (!cot.event.detail)
514
+ cot.event.detail = {};
515
+ if (feature.properties.droid) {
516
+ cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
517
+ }
518
+ if (feature.properties.archived) {
519
+ cot.event.detail.archive = { _attributes: {} };
520
+ }
521
+ if (feature.properties.links) {
522
+ if (!cot.event.detail.link)
523
+ cot.event.detail.link = [];
524
+ else if (!Array.isArray(cot.event.detail.link))
525
+ cot.event.detail.link = [cot.event.detail.link];
526
+ cot.event.detail.link.push(...feature.properties.links.map((link) => {
527
+ return { _attributes: link };
528
+ }));
529
+ }
530
+ if (feature.properties.dest) {
531
+ const dest = !Array.isArray(feature.properties.dest) ? [feature.properties.dest] : feature.properties.dest;
532
+ cot.event.detail.marti = {
533
+ dest: dest.map((dest) => {
534
+ return { _attributes: { ...dest } };
535
+ })
536
+ };
537
+ }
538
+ if (feature.properties.takv) {
539
+ cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
540
+ }
541
+ if (feature.properties.creator) {
542
+ cot.event.detail.creator = { _attributes: { ...feature.properties.creator } };
543
+ }
544
+ if (feature.properties.range !== undefined) {
545
+ cot.event.detail.range = { _attributes: { value: feature.properties.range } };
546
+ }
547
+ if (feature.properties.bearing !== undefined) {
548
+ cot.event.detail.bearing = { _attributes: { value: feature.properties.bearing } };
549
+ }
550
+ if (feature.properties.geofence) {
551
+ cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
552
+ }
553
+ if (feature.properties.milsym) {
554
+ cot.event.detail.__milsym = { _attributes: { id: feature.properties.milsym.id } };
555
+ }
556
+ if (feature.properties.sensor) {
557
+ cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
558
+ }
559
+ if (feature.properties.ackrequest) {
560
+ cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
561
+ }
562
+ if (feature.properties.video) {
563
+ if (feature.properties.video.connection) {
564
+ const video = JSON.parse(JSON.stringify(feature.properties.video));
565
+ const connection = video.connection;
566
+ delete video.connection;
567
+ cot.event.detail.__video = {
568
+ _attributes: { ...video },
569
+ ConnectionEntry: {
570
+ _attributes: connection
571
+ }
572
+ };
573
+ }
574
+ else {
575
+ cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
576
+ }
577
+ }
578
+ if (feature.properties.attachments) {
579
+ cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
580
+ }
581
+ if (feature.properties.contact) {
582
+ cot.event.detail.contact = {
583
+ _attributes: {
584
+ ...feature.properties.contact,
585
+ callsign: feature.properties.callsign || 'UNKNOWN',
586
+ }
587
+ };
588
+ }
589
+ if (feature.properties.fileshare) {
590
+ cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
591
+ }
592
+ if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
593
+ cot.event.detail.track = {
594
+ _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
595
+ };
596
+ }
597
+ if (feature.properties.group) {
598
+ cot.event.detail.__group = { _attributes: { ...feature.properties.group } };
599
+ }
600
+ if (feature.properties.flow) {
601
+ cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } };
602
+ }
603
+ if (feature.properties.status) {
604
+ cot.event.detail.status = { _attributes: { ...feature.properties.status } };
605
+ }
606
+ if (feature.properties.precisionlocation) {
607
+ cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } };
608
+ }
609
+ if (feature.properties.icon) {
610
+ cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } };
611
+ }
612
+ if (feature.properties.mission) {
613
+ cot.event.detail.mission = {
614
+ _attributes: {
615
+ type: feature.properties.mission.type,
616
+ guid: feature.properties.mission.guid,
617
+ tool: feature.properties.mission.tool,
618
+ name: feature.properties.mission.name,
619
+ authorUid: feature.properties.mission.authorUid,
620
+ }
621
+ };
622
+ if (feature.properties.mission.missionLayer) {
623
+ cot.event.detail.mission.missionLayer = {};
624
+ if (feature.properties.mission.missionLayer.name) {
625
+ cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
626
+ }
627
+ if (feature.properties.mission.missionLayer.parentUid) {
628
+ cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
629
+ }
630
+ if (feature.properties.mission.missionLayer.type) {
631
+ cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
632
+ }
633
+ if (feature.properties.mission.missionLayer.uid) {
634
+ cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
635
+ }
636
+ }
637
+ }
638
+ cot.event.detail.remarks = { _attributes: {}, _text: feature.properties.remarks || '' };
639
+ if (!feature.geometry) {
640
+ throw new Err(400, null, 'Must have Geometry');
641
+ }
642
+ else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
643
+ throw new Err(400, null, 'Unsupported Geometry Type');
644
+ }
645
+ if (feature.geometry.type === 'Point') {
646
+ cot.event.point._attributes.lon = feature.geometry.coordinates[0];
647
+ cot.event.point._attributes.lat = feature.geometry.coordinates[1];
648
+ cot.event.point._attributes.hae = feature.geometry.coordinates[2] || 0.0;
649
+ if (feature.properties['marker-color']) {
650
+ const color = new Color(feature.properties['marker-color'] || -1761607936);
651
+ color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
652
+ cot.event.detail.color = { _attributes: { argb: color.as_32bit() } };
653
+ }
654
+ }
655
+ else if (feature.geometry.type === 'Polygon' && feature.properties.type === 'u-d-c-c') {
656
+ if (!feature.properties.shape || !feature.properties.shape.ellipse) {
657
+ throw new Err(400, null, 'u-d-c-c (Circle) must define a feature.properties.shape.ellipse property');
658
+ }
659
+ cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } };
660
+ if (feature.properties.center) {
661
+ cot.event.point._attributes.lon = feature.properties.center[0];
662
+ cot.event.point._attributes.lat = feature.properties.center[1];
663
+ }
664
+ else {
665
+ const centre = PointOnFeature(feature);
666
+ cot.event.point._attributes.lon = centre.geometry.coordinates[0];
667
+ cot.event.point._attributes.lat = centre.geometry.coordinates[1];
668
+ cot.event.point._attributes.hae = 0.0;
669
+ }
670
+ }
671
+ else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
672
+ const stroke = new Color(feature.properties.stroke || -1761607936);
673
+ stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
674
+ cot.event.detail.strokeColor = { _attributes: { value: stroke.as_32bit() } };
675
+ if (!feature.properties['stroke-width'])
676
+ feature.properties['stroke-width'] = 3;
677
+ cot.event.detail.strokeWeight = { _attributes: {
678
+ value: feature.properties['stroke-width']
679
+ } };
680
+ if (!feature.properties['stroke-style'])
681
+ feature.properties['stroke-style'] = 'solid';
682
+ cot.event.detail.strokeStyle = { _attributes: {
683
+ value: feature.properties['stroke-style']
684
+ } };
685
+ if (feature.geometry.type === 'LineString') {
686
+ cot.event._attributes.type = 'u-d-f';
687
+ if (!cot.event.detail.link)
688
+ cot.event.detail.link = [];
689
+ else if (!Array.isArray(cot.event.detail.link))
690
+ cot.event.detail.link = [cot.event.detail.link];
691
+ for (const coord of feature.geometry.coordinates) {
692
+ cot.event.detail.link.push({
693
+ _attributes: { point: `${coord[1]},${coord[0]}` }
694
+ });
695
+ }
696
+ }
697
+ else if (feature.geometry.type === 'Polygon') {
698
+ cot.event._attributes.type = 'u-d-f';
699
+ if (!cot.event.detail.link)
700
+ cot.event.detail.link = [];
701
+ else if (!Array.isArray(cot.event.detail.link))
702
+ cot.event.detail.link = [cot.event.detail.link];
703
+ // Inner rings are not yet supported
704
+ for (const coord of feature.geometry.coordinates[0]) {
705
+ cot.event.detail.link.push({
706
+ _attributes: { point: `${coord[1]},${coord[0]}` }
707
+ });
708
+ }
709
+ const fill = new Color(feature.properties.fill || -1761607936);
710
+ fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
711
+ cot.event.detail.fillColor = { _attributes: { value: fill.as_32bit() } };
712
+ }
713
+ cot.event.detail.labels_on = { _attributes: { value: false } };
714
+ cot.event.detail.tog = { _attributes: { enabled: '0' } };
715
+ if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
716
+ cot.event.point._attributes.lon = feature.properties.center[0];
717
+ cot.event.point._attributes.lat = feature.properties.center[1];
718
+ if (feature.properties.center.length >= 3) {
719
+ cot.event.point._attributes.hae = feature.properties.center[2] || 0.0;
720
+ }
721
+ else {
722
+ cot.event.point._attributes.hae = 0.0;
723
+ }
724
+ }
725
+ else {
726
+ const centre = PointOnFeature(feature);
727
+ cot.event.point._attributes.lon = centre.geometry.coordinates[0];
728
+ cot.event.point._attributes.lat = centre.geometry.coordinates[1];
729
+ cot.event.point._attributes.hae = 0.0;
730
+ }
731
+ }
732
+ const newcot = new CoT(cot, opts);
733
+ if (feature.properties.metadata) {
734
+ newcot.metadata = feature.properties.metadata;
735
+ }
736
+ if (feature.path) {
737
+ newcot.path = feature.path;
738
+ }
739
+ return this.validate(newcot);
740
+ }
741
+ }
742
+ //# sourceMappingURL=parser.js.map