@tak-ps/node-cot 12.11.0 → 12.13.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,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import protobuf from 'protobufjs';
2
3
  import Err from '@openaddresses/batch-error';
3
4
  import { diff } from 'json-diff-ts';
@@ -9,6 +10,14 @@ import type {
9
10
  FeaturePropertyMission,
10
11
  FeaturePropertyMissionLayer,
11
12
  } from './types/feature.js';
13
+ import type {
14
+ MartiDest,
15
+ MartiDestAttributes,
16
+ Link,
17
+ LinkAttributes,
18
+ VideoAttributes,
19
+ VideoConnectionEntryAttributes,
20
+ } from './types/types.js'
12
21
  import {
13
22
  InputFeature,
14
23
  } from './types/feature.js';
@@ -19,7 +28,6 @@ import Ellipse from '@turf/ellipse';
19
28
  import Util from './utils/util.js';
20
29
  import Color from './utils/color.js';
21
30
  import JSONCoT, { Detail } from './types/types.js'
22
- import type { MartiDest, MartiDestAttributes, Link, LinkAttributes } from './types/types.js'
23
31
  import AJV from 'ajv';
24
32
  import fs from 'fs';
25
33
 
@@ -133,6 +141,21 @@ export default class CoT {
133
141
  return diffs.length > 0;
134
142
  }
135
143
 
144
+ /**
145
+ * Returns or sets the Callsign of the CoT
146
+ */
147
+ callsign(callsign?: string): string {
148
+ if (!this.raw.event.detail) this.raw.event.detail = {};
149
+
150
+ if (callsign && !this.raw.event.detail.contact) {
151
+ this.raw.event.detail.contact = { _attributes: { callsign } };
152
+ } else if (callsign && this.raw.event.detail.contact) {
153
+ this.raw.event.detail.contact._attributes.callsign = callsign;
154
+ }
155
+
156
+ return this.raw.event.detail.contact ? this.raw.event.detail.contact._attributes.callsign : 'UNKNOWN';
157
+ }
158
+
136
159
  /**
137
160
  * Returns or sets the UID of the CoT
138
161
  */
@@ -144,7 +167,7 @@ export default class CoT {
144
167
  /**
145
168
  * Add a given Dest tag to a CoT
146
169
  */
147
- addDest(dest: Static<typeof MartiDestAttributes>): void {
170
+ addDest(dest: Static<typeof MartiDestAttributes>): CoT {
148
171
  if (!this.raw.event.detail) this.raw.event.detail = {};
149
172
  if (!this.raw.event.detail.marti) this.raw.event.detail.marti = {};
150
173
 
@@ -158,9 +181,57 @@ export default class CoT {
158
181
  destArr.push({ _attributes: dest });
159
182
 
160
183
  this.raw.event.detail.marti.dest = destArr;
184
+
185
+ return this;
186
+ }
187
+
188
+ addVideo(
189
+ video: Static<typeof VideoAttributes>,
190
+ connection?: Static<typeof VideoConnectionEntryAttributes>
191
+ ): CoT {
192
+ if (!this.raw.event.detail) this.raw.event.detail = {};
193
+ if (this.raw.event.detail.__video) throw new Err(400, null, 'A video stream already exists on this CoT');
194
+
195
+ if (!video.url) throw new Err(400, null, 'A Video URL must be provided');
196
+
197
+ if (!video.uid && connection && connection.uid) {
198
+ video.uid = connection.uid
199
+ } else if (video.uid && connection && !connection.uid) {
200
+ connection.uid = video.uid;
201
+ } else if (!video.uid) {
202
+ video.uid = crypto.randomUUID();
203
+ }
204
+
205
+ this.raw.event.detail.__video = {
206
+ _attributes: video
207
+ };
208
+
209
+ if (connection) {
210
+ this.raw.event.detail.__video.ConnectionEntry = {
211
+ _attributes: connection
212
+ }
213
+ } else {
214
+ this.raw.event.detail.__video.ConnectionEntry = {
215
+ _attributes: {
216
+ uid: video.uid,
217
+ networkTimeout: 12000,
218
+ path: '',
219
+ protocol: 'raw',
220
+ bufferTime: -1,
221
+ address: video.url,
222
+ port: -1,
223
+ roverPort: -1,
224
+ rtspReliable: 0,
225
+ ignoreEmbeddedKLV: false,
226
+ alias: this.callsign()
227
+ }
228
+ }
229
+ }
230
+
231
+ return this;
161
232
  }
162
233
 
163
- addLink(link: Static<typeof LinkAttributes>): void {
234
+ addLink(link: Static<typeof LinkAttributes>): CoT {
164
235
  if (!this.raw.event.detail) this.raw.event.detail = {};
165
236
 
166
237
  let linkArr: Array<Static<typeof Link>> = [];
@@ -173,342 +244,581 @@ export default class CoT {
173
244
  linkArr.push({ _attributes: link });
174
245
 
175
246
  this.raw.event.detail.link = linkArr;
247
+
248
+ return this;
176
249
  }
177
250
 
178
251
  /**
179
- * Return an CoT Message given a GeoJSON Feature
180
- *
181
- * @param {Object} feature GeoJSON Point Feature
182
- *
183
- * @return {CoT}
252
+ * Return an ATAK Compliant Protobuf
184
253
  */
185
- static from_geojson(feature: Static<typeof InputFeature>): CoT {
186
- checkFeat(feature);
187
- if (checkFeat.errors) throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
254
+ to_proto(version = 1): Uint8Array {
255
+ if (version < 1 || version > 1) throw new Err(400, null, `Unsupported Proto Version: ${version}`);
256
+ const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
188
257
 
189
- const cot: Static<typeof JSONCoT> = {
190
- event: {
191
- _attributes: Util.cot_event_attr(
192
- feature.properties.type || 'a-f-G',
193
- feature.properties.how || 'm-g',
194
- feature.properties.time,
195
- feature.properties.start,
196
- feature.properties.stale
197
- ),
198
- point: Util.cot_point(),
199
- detail: Util.cot_event_detail(feature.properties.callsign)
258
+ const detail = this.raw.event.detail || {};
259
+
260
+ const msg: any = {
261
+ cotEvent: {
262
+ ...this.raw.event._attributes,
263
+ sendTime: new Date(this.raw.event._attributes.time).getTime(),
264
+ startTime: new Date(this.raw.event._attributes.start).getTime(),
265
+ staleTime: new Date(this.raw.event._attributes.stale).getTime(),
266
+ ...this.raw.event.point._attributes,
267
+ detail: {
268
+ xmlDetail: ''
269
+ }
200
270
  }
201
271
  };
202
272
 
203
- if (feature.id) cot.event._attributes.uid = String(feature.id);
204
- if (feature.properties.callsign && !feature.id) cot.event._attributes.uid = feature.properties.callsign;
205
- if (!cot.event.detail) cot.event.detail = {};
206
-
207
- if (feature.properties.droid) {
208
- cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
209
- }
210
-
211
- if (feature.properties.archived) {
212
- cot.event.detail.archive = { _attributes: { } };
273
+ let key: keyof Static<typeof Detail>;
274
+ for (key in detail) {
275
+ if(['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
276
+ msg.cotEvent.detail[key] = detail[key]._attributes;
277
+ delete detail[key]
278
+ }
213
279
  }
214
280
 
215
- if (feature.properties.links) {
216
- if (!cot.event.detail.link) cot.event.detail.link = [];
217
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link];
281
+ msg.cotEvent.detail.xmlDetail = xmljs.js2xml({
282
+ ...detail,
283
+ metadata: this.metadata
284
+ }, { compact: true });
218
285
 
219
- cot.event.detail.link.push(...feature.properties.links.map((link: Static<typeof LinkAttributes>) => {
220
- return { _attributes: link };
221
- }))
222
- }
286
+ return ProtoMessage.encode(msg).finish();
287
+ }
223
288
 
224
- if (feature.properties.dest) {
225
- const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
289
+ /**
290
+ * Return a GeoJSON Feature from an XML CoT message
291
+ */
292
+ to_geojson(): Static<typeof Feature> {
293
+ const raw: Static<typeof JSONCoT> = JSON.parse(JSON.stringify(this.raw));
294
+ if (!raw.event.detail) raw.event.detail = {};
295
+ if (!raw.event.detail.contact) raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
296
+ if (!raw.event.detail.contact._attributes) raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
226
297
 
227
- cot.event.detail.marti = {
228
- dest: dest.map((dest) => {
229
- return { _attributes: { ...dest } };
230
- })
298
+ const feat: Static<typeof Feature> = {
299
+ id: raw.event._attributes.uid,
300
+ type: 'Feature',
301
+ properties: {
302
+ callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
303
+ center: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ],
304
+ type: raw.event._attributes.type,
305
+ how: raw.event._attributes.how || '',
306
+ time: raw.event._attributes.time,
307
+ start: raw.event._attributes.start,
308
+ stale: raw.event._attributes.stale,
309
+ },
310
+ geometry: {
311
+ type: 'Point',
312
+ coordinates: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ]
231
313
  }
232
- }
314
+ };
233
315
 
234
- if (feature.properties.takv) {
235
- cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
316
+ const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
317
+ delete contact.callsign;
318
+ if (Object.keys(contact).length) {
319
+ feat.properties.contact = contact;
236
320
  }
237
321
 
238
- if (feature.properties.geofence) {
239
- cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
322
+ if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
323
+ feat.properties.remarks = raw.event.detail.remarks._text;
240
324
  }
241
325
 
242
- if (feature.properties.sensor) {
243
- cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
326
+ if (raw.event.detail.fileshare) {
327
+ feat.properties.fileshare = raw.event.detail.fileshare._attributes;
328
+ if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
329
+ feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes)
330
+ }
244
331
  }
245
332
 
246
- if (feature.properties.ackrequest) {
247
- cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
333
+ if (raw.event.detail.sensor) {
334
+ feat.properties.sensor = raw.event.detail.sensor._attributes;
248
335
  }
249
336
 
250
- if (feature.properties.video) {
251
- cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
252
- }
337
+ if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
338
+ feat.properties.video = raw.event.detail.__video._attributes;
253
339
 
254
- if (feature.properties.attachments) {
255
- cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
340
+ if (raw.event.detail.__video.ConnectionEntry) {
341
+ feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
342
+ }
256
343
  }
257
344
 
258
- if (feature.properties.contact) {
259
- cot.event.detail.contact = {
260
- _attributes: {
261
- callsign: feature.properties.callsign || 'UNKNOWN',
262
- ...feature.properties.contact
263
- }
264
- };
345
+ if (raw.event.detail.__geofence) {
346
+ feat.properties.geofence = raw.event.detail.__geofence._attributes;
265
347
  }
266
348
 
267
- if (feature.properties.fileshare) {
268
- cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
349
+ if (raw.event.detail.ackrequest) {
350
+ feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
269
351
  }
270
352
 
271
- if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
272
- cot.event.detail.track = {
273
- _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
274
- }
353
+ if (raw.event.detail.attachment_list) {
354
+ feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
275
355
  }
276
356
 
277
- if (feature.properties.group) {
278
- cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
279
- }
357
+ if (raw.event.detail.link) {
358
+ if (!Array.isArray(raw.event.detail.link)) raw.event.detail.link = [raw.event.detail.link];
280
359
 
281
- if (feature.properties.flow) {
282
- cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
360
+ feat.properties.links = raw.event.detail.link.filter((link: Static<typeof Link>) => {
361
+ return !!link._attributes.url
362
+ }).map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
363
+ return link._attributes;
364
+ });
365
+
366
+ if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
283
367
  }
284
368
 
285
- if (feature.properties.status) {
286
- cot.event.detail.status = { _attributes: { ...feature.properties.status } }
369
+ if (raw.event.detail.archive) {
370
+ feat.properties.archived = true;
287
371
  }
288
372
 
289
- if (feature.properties.precisionlocation) {
290
- cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
373
+ if (raw.event.detail.__chat) {
374
+ feat.properties.chat = {
375
+ ...raw.event.detail.__chat._attributes,
376
+ chatgrp: raw.event.detail.__chat.chatgrp
377
+ }
291
378
  }
292
379
 
293
- if (feature.properties.icon) {
294
- cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } }
380
+ if (raw.event.detail.track && raw.event.detail.track._attributes) {
381
+ if (raw.event.detail.track._attributes.course) feat.properties.course = Number(raw.event.detail.track._attributes.course);
382
+ if (raw.event.detail.track._attributes.slope) feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
383
+ if (raw.event.detail.track._attributes.course) feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
295
384
  }
296
385
 
297
- if (feature.properties.mission) {
298
- cot.event.detail.mission = {
299
- _attributes: {
300
- type: feature.properties.mission.type,
301
- guid: feature.properties.mission.guid,
302
- tool: feature.properties.mission.tool,
303
- name: feature.properties.mission.name,
304
- authorUid: feature.properties.mission.authorUid,
305
- }
306
- }
386
+ if (raw.event.detail.marti && raw.event.detail.marti.dest) {
387
+ if (!Array.isArray(raw.event.detail.marti.dest)) raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
307
388
 
308
- if (feature.properties.mission.missionLayer) {
309
- cot.event.detail.mission.missionLayer = {};
389
+ const dest: Array<Static<typeof MartiDestAttributes>> = raw.event.detail.marti.dest.map((d: Static<typeof MartiDest>) => {
390
+ return { ...d._attributes };
391
+ });
310
392
 
311
- if (feature.properties.mission.missionLayer.name) {
312
- cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
313
- }
393
+ feat.properties.dest = dest.length === 1 ? dest[0] : dest
394
+ }
314
395
 
315
- if (feature.properties.mission.missionLayer.parentUid) {
316
- cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
317
- }
396
+ if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
397
+ feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
398
+ }
318
399
 
319
- if (feature.properties.mission.missionLayer.type) {
320
- cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
321
- }
322
400
 
323
- if (feature.properties.mission.missionLayer.uid) {
324
- cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
325
- }
326
- }
401
+ if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
402
+ feat.properties.droid = raw.event.detail.uid._attributes.Droid;
327
403
  }
328
404
 
329
- cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
330
-
331
- if (!feature.geometry) {
332
- throw new Err(400, null, 'Must have Geometry');
333
- } else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
334
- throw new Err(400, null, 'Unsupported Geometry Type');
405
+ if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
406
+ feat.properties.takv = raw.event.detail.takv._attributes;
335
407
  }
336
408
 
337
- if (feature.geometry.type === 'Point') {
338
- cot.event.point._attributes.lon = String(feature.geometry.coordinates[0]);
339
- cot.event.point._attributes.lat = String(feature.geometry.coordinates[1]);
340
- cot.event.point._attributes.hae = String(feature.geometry.coordinates[2] || '0.0');
409
+ if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
410
+ feat.properties.group = raw.event.detail.__group._attributes;
411
+ }
341
412
 
413
+ if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
414
+ feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
415
+ }
342
416
 
343
- if (feature.properties['marker-color']) {
344
- const color = new Color(feature.properties['marker-color'] || -1761607936);
345
- color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
346
- cot.event.detail.color = { _attributes: { argb: String(color.as_32bit()) } };
347
- }
348
- } else if (feature.geometry.type === 'Polygon' && feature.properties.type === 'u-d-c-c') {
349
- if (!feature.properties.shape || !feature.properties.shape.ellipse) {
350
- throw new Err(400, null, 'u-d-c-c (Circle) must define a feature.properties.shape.ellipse property')
351
- }
352
- cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } }
417
+ if (raw.event.detail.status && raw.event.detail.status._attributes) {
418
+ feat.properties.status = raw.event.detail.status._attributes;
419
+ }
353
420
 
354
- if (feature.properties.center) {
355
- cot.event.point._attributes.lon = String(feature.properties.center[0]);
356
- cot.event.point._attributes.lat = String(feature.properties.center[1]);
357
- } else {
358
- const centre = PointOnFeature(feature as AllGeoJSON);
359
- cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
360
- cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
361
- cot.event.point._attributes.hae = '0.0';
362
- }
363
- } else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
364
- const stroke = new Color(feature.properties.stroke || -1761607936);
365
- stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
366
- cot.event.detail.strokeColor = { _attributes: { value: String(stroke.as_32bit()) } };
421
+ if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
422
+ const mission: Static<typeof FeaturePropertyMission> = {
423
+ ...raw.event.detail.mission._attributes
424
+ };
367
425
 
368
- if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
369
- cot.event.detail.strokeWeight = { _attributes: {
370
- value: String(feature.properties['stroke-width'])
371
- } };
426
+ if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
427
+ const changes =
428
+ Array.isArray(raw.event.detail.mission.MissionChanges)
429
+ ? raw.event.detail.mission.MissionChanges
430
+ : [ raw.event.detail.mission.MissionChanges ]
372
431
 
373
- if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
374
- cot.event.detail.strokeStyle = { _attributes: {
375
- value: feature.properties['stroke-style']
376
- } };
432
+ mission.missionChanges = []
433
+ for (const change of changes) {
434
+ mission.missionChanges.push({
435
+ contentUid: change.MissionChange.contentUid._text,
436
+ creatorUid: change.MissionChange.creatorUid._text,
437
+ isFederatedChange: change.MissionChange.isFederatedChange._text,
438
+ missionName: change.MissionChange.missionName._text,
439
+ timestamp: change.MissionChange.timestamp._text,
440
+ type: change.MissionChange.type._text,
441
+ details: {
442
+ ...change.MissionChange.details._attributes,
443
+ ...change.MissionChange.details.location
444
+ ? change.MissionChange.details.location._attributes
445
+ : {}
446
+ }
447
+ })
448
+ }
449
+ }
377
450
 
378
- if (feature.geometry.type === 'LineString') {
379
- cot.event._attributes.type = 'u-d-f';
380
451
 
381
- if (!cot.event.detail.link) cot.event.detail.link = [];
382
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
452
+ if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
453
+ const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};
383
454
 
384
- for (const coord of feature.geometry.coordinates) {
385
- cot.event.detail.link.push({
386
- _attributes: { point: `${coord[1]},${coord[0]}` }
387
- });
455
+ if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
456
+ missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
388
457
  }
389
- } else if (feature.geometry.type === 'Polygon') {
390
- cot.event._attributes.type = 'u-d-f';
391
-
392
- if (!cot.event.detail.link) cot.event.detail.link = [];
393
- else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
394
-
395
- // Inner rings are not yet supported
396
- for (const coord of feature.geometry.coordinates[0]) {
397
- cot.event.detail.link.push({
398
- _attributes: { point: `${coord[1]},${coord[0]}` }
399
- });
458
+ if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
459
+ missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
460
+ }
461
+ if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
462
+ missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
463
+ }
464
+ if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
465
+ missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
400
466
  }
401
467
 
402
- const fill = new Color(feature.properties.fill || -1761607936);
403
- fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
404
- cot.event.detail.fillColor = { _attributes: { value: String(fill.as_32bit()) } };
468
+ mission.missionLayer = missionLayer;
405
469
  }
406
470
 
407
- cot.event.detail.labels_on = { _attributes: { value: 'false' } };
408
- cot.event.detail.tog = { _attributes: { enabled: '0' } };
409
-
410
- if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
411
- cot.event.point._attributes.lon = String(feature.properties.center[0]);
412
- cot.event.point._attributes.lat = String(feature.properties.center[1]);
413
-
414
- if (feature.properties.center.length >= 3) {
415
- cot.event.point._attributes.hae = String(feature.properties.center[2] || '0.0');
416
- } else {
417
- cot.event.point._attributes.hae = '0.0';
418
- }
419
- } else {
420
- const centre = PointOnFeature(feature as AllGeoJSON);
421
- cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
422
- cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
423
- cot.event.point._attributes.hae = '0.0';
424
- }
471
+ feat.properties.mission = mission;
425
472
  }
426
473
 
427
- const newcot = new CoT(cot);
428
-
429
- if (feature.properties.metadata) {
430
- newcot.metadata = feature.properties.metadata
474
+ if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
475
+ feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
431
476
  }
432
477
 
433
- return newcot;
434
- }
478
+ if (['u-d-f', 'u-d-r', 'b-m-r'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
479
+ const coordinates = [];
435
480
 
436
- /**
437
- * Return an ATAK Compliant Protobuf
438
- */
439
- to_proto(version = 1): Uint8Array {
440
- if (version < 1 || version > 1) throw new Err(400, null, `Unsupported Proto Version: ${version}`);
441
- const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
481
+ for (const l of raw.event.detail.link) {
482
+ if (!l._attributes.point) continue;
483
+ coordinates.push(l._attributes.point.split(',').map((p: string) => { return Number(p.trim()) }).splice(0, 2).reverse());
484
+ }
442
485
 
443
- const detail = this.raw.event.detail || {};
486
+ if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
487
+ const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
488
+ feat.properties.stroke = stroke.as_hex();
489
+ feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
490
+ }
444
491
 
445
- const msg: any = {
446
- cotEvent: {
447
- ...this.raw.event._attributes,
448
- sendTime: new Date(this.raw.event._attributes.time).getTime(),
449
- startTime: new Date(this.raw.event._attributes.start).getTime(),
450
- staleTime: new Date(this.raw.event._attributes.stale).getTime(),
451
- ...this.raw.event.point._attributes,
452
- detail: {
453
- xmlDetail: ''
454
- }
492
+ if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
493
+ feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
455
494
  }
456
- };
457
495
 
458
- let key: keyof Static<typeof Detail>;
459
- for (key in detail) {
460
- if(['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
461
- msg.cotEvent.detail[key] = detail[key]._attributes;
462
- delete detail[key]
496
+ if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
497
+ feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
463
498
  }
464
- }
465
499
 
466
- msg.cotEvent.detail.xmlDetail = xmljs.js2xml({
467
- ...detail,
468
- metadata: this.metadata
469
- }, { compact: true });
500
+ if (raw.event._attributes.type === 'u-d-r' || (coordinates[0][0] === coordinates[coordinates.length -1][0] && coordinates[0][1] === coordinates[coordinates.length -1][1])) {
501
+ if (raw.event._attributes.type === 'u-d-r') {
502
+ // CoT rectangles are only 4 points - GeoJSON needs to be closed
503
+ coordinates.push(coordinates[0])
504
+ }
470
505
 
471
- return ProtoMessage.encode(msg).finish();
472
- }
506
+ feat.geometry = {
507
+ type: 'Polygon',
508
+ coordinates: [coordinates]
509
+ }
473
510
 
474
- /**
475
- * Parse an ATAK compliant Protobuf to a JS Object
476
- */
477
- static from_proto(raw: Uint8Array, version = 1): CoT {
478
- const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
511
+ if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
512
+ const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
513
+ feat.properties['fill-opacity'] = fill.as_opacity() / 255;
514
+ feat.properties['fill'] = fill.as_hex();
515
+ }
516
+ } else {
517
+ feat.geometry = {
518
+ type: 'LineString',
519
+ coordinates
520
+ }
521
+ }
522
+ } else if (raw.event._attributes.type.startsWith('u-d-c-c')) {
523
+ if (!raw.event.detail.shape) throw new Err(400, null, 'u-d-c-c (Circle) must define shape value')
524
+ if (
525
+ !raw.event.detail.shape.ellipse
526
+ || !raw.event.detail.shape.ellipse._attributes
527
+ ) throw new Err(400, null, 'u-d-c-c (Circle) must define ellipse shape value')
479
528
 
480
- // TODO Type this
481
- const msg: any = ProtoMessage.decode(raw);
529
+ const ellipse = {
530
+ major: Number(raw.event.detail.shape.ellipse._attributes.major),
531
+ minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
532
+ angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
533
+ }
482
534
 
483
- if (!msg.cotEvent) throw new Err(400, null, 'No cotEvent Data');
535
+ feat.geometry = Truncate(Ellipse(
536
+ feat.geometry.coordinates as number[],
537
+ Number(ellipse.major) / 1000,
538
+ Number(ellipse.minor) / 1000,
539
+ {
540
+ angle: ellipse.angle
541
+ }
542
+ ), {
543
+ precision: COORDINATE_PRECISION,
544
+ mutate: true
545
+ }).geometry as Static<typeof Polygon>;
484
546
 
485
- const detail: Record<string, any> = {};
486
- const metadata: Record<string, unknown> = {};
487
- for (const key in msg.cotEvent.detail) {
488
- if (key === 'xmlDetail') {
489
- const parsed: any = xmljs.xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });
490
- Object.assign(detail, parsed.detail);
547
+ feat.properties.shape = {};
548
+ feat.properties.shape.ellipse = ellipse;
549
+ } else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
550
+ // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
551
+ if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
552
+ const coordinates = [];
491
553
 
492
- if (detail.metadata) {
493
- for (const key in detail.metadata) {
494
- metadata[key] = detail.metadata[key]._text;
495
- }
496
- delete detail.metadata;
497
- }
498
- } else if (key === 'group') {
499
- if (msg.cotEvent.detail[key]) {
500
- detail.__group = { _attributes: msg.cotEvent.detail[key] };
554
+ const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
555
+ for (const v of vertices) {
556
+ coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
501
557
  }
502
- } else if (['contact', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
503
- if (msg.cotEvent.detail[key]) {
504
- detail[key] = { _attributes: msg.cotEvent.detail[key] };
558
+
559
+ if (coordinates.length === 1) {
560
+ feat.geometry = { type: 'Point', coordinates: coordinates[0] }
561
+ } else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
562
+ coordinates.push(coordinates[0]);
563
+ feat.geometry = { type: 'Polygon', coordinates: [coordinates] }
564
+ } else {
565
+ feat.geometry = { type: 'LineString', coordinates }
505
566
  }
506
567
  }
507
- }
508
568
 
509
- const cot = new CoT({
510
- event: {
511
- _attributes: {
569
+ if (
570
+ raw.event.detail.shape
571
+ && raw.event.detail.shape.polyline
572
+ && raw.event.detail.shape.polyline._attributes
573
+ && raw.event.detail.shape.polyline._attributes
574
+ ) {
575
+ if (raw.event.detail.shape.polyline._attributes.fillColor) {
576
+ const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
577
+ feat.properties['fill-opacity'] = fill.as_opacity() / 255;
578
+ feat.properties['fill'] = fill.as_hex();
579
+ }
580
+
581
+ if (raw.event.detail.shape.polyline._attributes.color) {
582
+ const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
583
+ feat.properties.stroke = stroke.as_hex();
584
+ feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
585
+ }
586
+ }
587
+ }
588
+
589
+ if (raw.event.detail.color && raw.event.detail.color._attributes && raw.event.detail.color._attributes.argb) {
590
+ const color = new Color(Number(raw.event.detail.color._attributes.argb));
591
+ feat.properties['marker-color'] = color.as_hex();
592
+ feat.properties['marker-opacity'] = color.as_opacity() / 255;
593
+ }
594
+
595
+ feat.properties.metadata = this.metadata;
596
+
597
+ return feat;
598
+ }
599
+
600
+ to_xml(): string {
601
+ return xmljs.js2xml(this.raw, { compact: true });
602
+ }
603
+
604
+ /**
605
+ * Determines if the CoT message represents a Tasking Message
606
+ *
607
+ * @return {boolean}
608
+ */
609
+ is_tasking(): boolean {
610
+ return !!this.raw.event._attributes.type.match(/^t-/)
611
+ }
612
+
613
+ /**
614
+ * Determines if the CoT message represents a Chat Message
615
+ *
616
+ * @return {boolean}
617
+ */
618
+ is_chat(): boolean {
619
+ return !!(this.raw.event.detail && this.raw.event.detail.__chat);
620
+ }
621
+
622
+ /**
623
+ * Determines if the CoT message represents a Friendly Element
624
+ *
625
+ * @return {boolean}
626
+ */
627
+ is_friend(): boolean {
628
+ return !!this.raw.event._attributes.type.match(/^a-f-/)
629
+ }
630
+
631
+ /**
632
+ * Determines if the CoT message represents a Hostile Element
633
+ *
634
+ * @return {boolean}
635
+ */
636
+ is_hostile(): boolean {
637
+ return !!this.raw.event._attributes.type.match(/^a-h-/)
638
+ }
639
+
640
+ /**
641
+ * Determines if the CoT message represents a Unknown Element
642
+ *
643
+ * @return {boolean}
644
+ */
645
+ is_unknown(): boolean {
646
+ return !!this.raw.event._attributes.type.match(/^a-u-/)
647
+ }
648
+
649
+ /**
650
+ * Determines if the CoT message represents a Pending Element
651
+ *
652
+ * @return {boolean}
653
+ */
654
+ is_pending(): boolean {
655
+ return !!this.raw.event._attributes.type.match(/^a-p-/)
656
+ }
657
+
658
+ /**
659
+ * Determines if the CoT message represents an Assumed Element
660
+ *
661
+ * @return {boolean}
662
+ */
663
+ is_assumed(): boolean {
664
+ return !!this.raw.event._attributes.type.match(/^a-a-/)
665
+ }
666
+
667
+ /**
668
+ * Determines if the CoT message represents a Neutral Element
669
+ *
670
+ * @return {boolean}
671
+ */
672
+ is_neutral(): boolean {
673
+ return !!this.raw.event._attributes.type.match(/^a-n-/)
674
+ }
675
+
676
+ /**
677
+ * Determines if the CoT message represents a Suspect Element
678
+ *
679
+ * @return {boolean}
680
+ */
681
+ is_suspect(): boolean {
682
+ return !!this.raw.event._attributes.type.match(/^a-s-/)
683
+ }
684
+
685
+ /**
686
+ * Determines if the CoT message represents a Joker Element
687
+ *
688
+ * @return {boolean}
689
+ */
690
+ is_joker(): boolean {
691
+ return !!this.raw.event._attributes.type.match(/^a-j-/)
692
+ }
693
+
694
+ /**
695
+ * Determines if the CoT message represents a Faker Element
696
+ *
697
+ * @return {boolean}
698
+ */
699
+ is_faker(): boolean {
700
+ return !!this.raw.event._attributes.type.match(/^a-k-/)
701
+ }
702
+
703
+ /**
704
+ * Determines if the CoT message represents an Element
705
+ *
706
+ * @return {boolean}
707
+ */
708
+ is_atom(): boolean {
709
+ return !!this.raw.event._attributes.type.match(/^a-/)
710
+ }
711
+
712
+ /**
713
+ * Determines if the CoT message represents an Airborne Element
714
+ *
715
+ * @return {boolean}
716
+ */
717
+ is_airborne(): boolean {
718
+ return !!this.raw.event._attributes.type.match(/^a-.-A/)
719
+ }
720
+
721
+ /**
722
+ * Determines if the CoT message represents a Ground Element
723
+ *
724
+ * @return {boolean}
725
+ */
726
+ is_ground(): boolean {
727
+ return !!this.raw.event._attributes.type.match(/^a-.-G/)
728
+ }
729
+
730
+ /**
731
+ * Determines if the CoT message represents an Installation
732
+ *
733
+ * @return {boolean}
734
+ */
735
+ is_installation(): boolean {
736
+ return !!this.raw.event._attributes.type.match(/^a-.-G-I/)
737
+ }
738
+
739
+ /**
740
+ * Determines if the CoT message represents a Vehicle
741
+ *
742
+ * @return {boolean}
743
+ */
744
+ is_vehicle(): boolean {
745
+ return !!this.raw.event._attributes.type.match(/^a-.-G-E-V/)
746
+ }
747
+
748
+ /**
749
+ * Determines if the CoT message represents Equipment
750
+ *
751
+ * @return {boolean}
752
+ */
753
+ is_equipment(): boolean {
754
+ return !!this.raw.event._attributes.type.match(/^a-.-G-E/)
755
+ }
756
+
757
+ /**
758
+ * Determines if the CoT message represents a Surface Element
759
+ *
760
+ * @return {boolean}
761
+ */
762
+ is_surface(): boolean {
763
+ return !!this.raw.event._attributes.type.match(/^a-.-S/)
764
+ }
765
+
766
+ /**
767
+ * Determines if the CoT message represents a Subsurface Element
768
+ *
769
+ * @return {boolean}
770
+ */
771
+ is_subsurface(): boolean {
772
+ return !!this.raw.event._attributes.type.match(/^a-.-U/)
773
+ }
774
+
775
+ /**
776
+ * Determines if the CoT message represents a UAV Element
777
+ *
778
+ * @return {boolean}
779
+ */
780
+ is_uav(): boolean {
781
+ return !!this.raw.event._attributes.type.match(/^a-f-A-M-F-Q-r/)
782
+ }
783
+
784
+ /**
785
+ * Parse an ATAK compliant Protobuf to a JS Object
786
+ */
787
+ static from_proto(raw: Uint8Array, version = 1): CoT {
788
+ const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)
789
+
790
+ // TODO Type this
791
+ const msg: any = ProtoMessage.decode(raw);
792
+
793
+ if (!msg.cotEvent) throw new Err(400, null, 'No cotEvent Data');
794
+
795
+ const detail: Record<string, any> = {};
796
+ const metadata: Record<string, unknown> = {};
797
+ for (const key in msg.cotEvent.detail) {
798
+ if (key === 'xmlDetail') {
799
+ const parsed: any = xmljs.xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });
800
+ Object.assign(detail, parsed.detail);
801
+
802
+ if (detail.metadata) {
803
+ for (const key in detail.metadata) {
804
+ metadata[key] = detail.metadata[key]._text;
805
+ }
806
+ delete detail.metadata;
807
+ }
808
+ } else if (key === 'group') {
809
+ if (msg.cotEvent.detail[key]) {
810
+ detail.__group = { _attributes: msg.cotEvent.detail[key] };
811
+ }
812
+ } else if (['contact', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
813
+ if (msg.cotEvent.detail[key]) {
814
+ detail[key] = { _attributes: msg.cotEvent.detail[key] };
815
+ }
816
+ }
817
+ }
818
+
819
+ const cot = new CoT({
820
+ event: {
821
+ _attributes: {
512
822
  version: '2.0',
513
823
  uid: msg.cotEvent.uid, type: msg.cotEvent.type, how: msg.cotEvent.how,
514
824
  qos: msg.cotEvent.qos, opex: msg.cotEvent.opex, access: msg.cotEvent.access,
@@ -535,506 +845,288 @@ export default class CoT {
535
845
  }
536
846
 
537
847
  /**
538
- * Return a GeoJSON Feature from an XML CoT message
848
+ * Return a CoT Message
539
849
  */
540
- to_geojson(): Static<typeof Feature> {
541
- const raw: Static<typeof JSONCoT> = JSON.parse(JSON.stringify(this.raw));
542
- if (!raw.event.detail) raw.event.detail = {};
543
- if (!raw.event.detail.contact) raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
544
- if (!raw.event.detail.contact._attributes) raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
545
-
546
- const feat: Static<typeof Feature> = {
547
- id: raw.event._attributes.uid,
548
- type: 'Feature',
549
- properties: {
550
- callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
551
- center: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ],
552
- type: raw.event._attributes.type,
553
- how: raw.event._attributes.how || '',
554
- time: raw.event._attributes.time,
555
- start: raw.event._attributes.start,
556
- stale: raw.event._attributes.stale,
557
- },
558
- geometry: {
559
- type: 'Point',
560
- coordinates: [ Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae) ]
850
+ static ping(): CoT {
851
+ return new CoT({
852
+ event: {
853
+ _attributes: Util.cot_event_attr('t-x-c-t', 'h-g-i-g-o'),
854
+ detail: {},
855
+ point: Util.cot_point()
561
856
  }
562
- };
563
-
564
- const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
565
- delete contact.callsign;
566
- if (Object.keys(contact).length) {
567
- feat.properties.contact = contact;
568
- }
857
+ });
858
+ }
569
859
 
570
- if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
571
- feat.properties.remarks = raw.event.detail.remarks._text;
572
- }
860
+ /**
861
+ * Return an CoT Message given a GeoJSON Feature
862
+ *
863
+ * @param {Object} feature GeoJSON Point Feature
864
+ *
865
+ * @return {CoT}
866
+ */
867
+ static from_geojson(feature: Static<typeof InputFeature>): CoT {
868
+ checkFeat(feature);
869
+ if (checkFeat.errors) throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
573
870
 
574
- if (raw.event.detail.fileshare) {
575
- feat.properties.fileshare = raw.event.detail.fileshare._attributes;
576
- if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
577
- feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes)
871
+ const cot: Static<typeof JSONCoT> = {
872
+ event: {
873
+ _attributes: Util.cot_event_attr(
874
+ feature.properties.type || 'a-f-G',
875
+ feature.properties.how || 'm-g',
876
+ feature.properties.time,
877
+ feature.properties.start,
878
+ feature.properties.stale
879
+ ),
880
+ point: Util.cot_point(),
881
+ detail: Util.cot_event_detail(feature.properties.callsign)
578
882
  }
579
- }
883
+ };
580
884
 
581
- if (raw.event.detail.sensor) {
582
- feat.properties.sensor = raw.event.detail.sensor._attributes;
583
- }
885
+ if (feature.id) cot.event._attributes.uid = String(feature.id);
886
+ if (feature.properties.callsign && !feature.id) cot.event._attributes.uid = feature.properties.callsign;
887
+ if (!cot.event.detail) cot.event.detail = {};
584
888
 
585
- if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
586
- feat.properties.video = raw.event.detail.__video._attributes;
889
+ if (feature.properties.droid) {
890
+ cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
587
891
  }
588
892
 
589
- if (raw.event.detail.__geofence) {
590
- feat.properties.geofence = raw.event.detail.__geofence._attributes;
893
+ if (feature.properties.archived) {
894
+ cot.event.detail.archive = { _attributes: { } };
591
895
  }
592
896
 
593
- if (raw.event.detail.ackrequest) {
594
- feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
595
- }
897
+ if (feature.properties.links) {
898
+ if (!cot.event.detail.link) cot.event.detail.link = [];
899
+ else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link];
596
900
 
597
- if (raw.event.detail.attachment_list) {
598
- feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
901
+ cot.event.detail.link.push(...feature.properties.links.map((link: Static<typeof LinkAttributes>) => {
902
+ return { _attributes: link };
903
+ }))
599
904
  }
600
905
 
601
- if (raw.event.detail.link) {
602
- if (!Array.isArray(raw.event.detail.link)) raw.event.detail.link = [raw.event.detail.link];
906
+ if (feature.properties.dest) {
907
+ const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
603
908
 
604
- feat.properties.links = raw.event.detail.link.filter((link: Static<typeof Link>) => {
605
- return !!link._attributes.url
606
- }).map((link: Static<typeof Link>): Static<typeof LinkAttributes> => {
607
- return link._attributes;
608
- });
909
+ cot.event.detail.marti = {
910
+ dest: dest.map((dest) => {
911
+ return { _attributes: { ...dest } };
912
+ })
913
+ }
914
+ }
609
915
 
610
- if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
916
+ if (feature.properties.takv) {
917
+ cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
611
918
  }
612
919
 
613
- if (raw.event.detail.archive) {
614
- feat.properties.archived = true;
920
+ if (feature.properties.geofence) {
921
+ cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
615
922
  }
616
923
 
617
- if (raw.event.detail.__chat) {
618
- feat.properties.chat = {
619
- ...raw.event.detail.__chat._attributes,
620
- chatgrp: raw.event.detail.__chat.chatgrp
621
- }
924
+ if (feature.properties.sensor) {
925
+ cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
622
926
  }
623
927
 
624
- if (raw.event.detail.track && raw.event.detail.track._attributes) {
625
- if (raw.event.detail.track._attributes.course) feat.properties.course = Number(raw.event.detail.track._attributes.course);
626
- if (raw.event.detail.track._attributes.slope) feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
627
- if (raw.event.detail.track._attributes.course) feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
928
+ if (feature.properties.ackrequest) {
929
+ cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
628
930
  }
629
931
 
630
- if (raw.event.detail.marti && raw.event.detail.marti.dest) {
631
- if (!Array.isArray(raw.event.detail.marti.dest)) raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
932
+ if (feature.properties.video) {
933
+ if (feature.properties.video.connection) {
934
+ const video = JSON.parse(JSON.stringify(feature.properties.video));
632
935
 
633
- const dest: Array<Static<typeof MartiDestAttributes>> = raw.event.detail.marti.dest.map((d: Static<typeof MartiDest>) => {
634
- return { ...d._attributes };
635
- });
936
+ const connection = video.connection;
937
+ delete video.connection;
636
938
 
637
- feat.properties.dest = dest.length === 1 ? dest[0] : dest
939
+ cot.event.detail.__video = {
940
+ _attributes: { ...video },
941
+ ConnectionEntry: {
942
+ _attributes: connection
943
+ }
944
+ }
945
+ } else {
946
+ cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
947
+ }
638
948
  }
639
949
 
640
- if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
641
- feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
950
+ if (feature.properties.attachments) {
951
+ cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
642
952
  }
643
953
 
954
+ if (feature.properties.contact) {
955
+ cot.event.detail.contact = {
956
+ _attributes: {
957
+ callsign: feature.properties.callsign || 'UNKNOWN',
958
+ ...feature.properties.contact
959
+ }
960
+ };
961
+ }
644
962
 
645
- if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
646
- feat.properties.droid = raw.event.detail.uid._attributes.Droid;
963
+ if (feature.properties.fileshare) {
964
+ cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
647
965
  }
648
966
 
649
- if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
650
- feat.properties.takv = raw.event.detail.takv._attributes;
967
+ if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
968
+ cot.event.detail.track = {
969
+ _attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
970
+ }
651
971
  }
652
972
 
653
- if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
654
- feat.properties.group = raw.event.detail.__group._attributes;
973
+ if (feature.properties.group) {
974
+ cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
655
975
  }
656
976
 
657
- if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
658
- feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
977
+ if (feature.properties.flow) {
978
+ cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
659
979
  }
660
980
 
661
- if (raw.event.detail.status && raw.event.detail.status._attributes) {
662
- feat.properties.status = raw.event.detail.status._attributes;
981
+ if (feature.properties.status) {
982
+ cot.event.detail.status = { _attributes: { ...feature.properties.status } }
663
983
  }
664
984
 
665
- if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
666
- const mission: Static<typeof FeaturePropertyMission> = {
667
- ...raw.event.detail.mission._attributes
668
- };
985
+ if (feature.properties.precisionlocation) {
986
+ cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
987
+ }
669
988
 
670
- if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
671
- const changes =
672
- Array.isArray(raw.event.detail.mission.MissionChanges)
673
- ? raw.event.detail.mission.MissionChanges
674
- : [ raw.event.detail.mission.MissionChanges ]
989
+ if (feature.properties.icon) {
990
+ cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } }
991
+ }
675
992
 
676
- mission.missionChanges = []
677
- for (const change of changes) {
678
- mission.missionChanges.push({
679
- contentUid: change.MissionChange.contentUid._text,
680
- creatorUid: change.MissionChange.creatorUid._text,
681
- isFederatedChange: change.MissionChange.isFederatedChange._text,
682
- missionName: change.MissionChange.missionName._text,
683
- timestamp: change.MissionChange.timestamp._text,
684
- type: change.MissionChange.type._text,
685
- details: {
686
- ...change.MissionChange.details._attributes,
687
- ...change.MissionChange.details.location
688
- ? change.MissionChange.details.location._attributes
689
- : {}
690
- }
691
- })
993
+ if (feature.properties.mission) {
994
+ cot.event.detail.mission = {
995
+ _attributes: {
996
+ type: feature.properties.mission.type,
997
+ guid: feature.properties.mission.guid,
998
+ tool: feature.properties.mission.tool,
999
+ name: feature.properties.mission.name,
1000
+ authorUid: feature.properties.mission.authorUid,
692
1001
  }
693
1002
  }
694
1003
 
1004
+ if (feature.properties.mission.missionLayer) {
1005
+ cot.event.detail.mission.missionLayer = {};
695
1006
 
696
- if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
697
- const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};
698
-
699
- if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
700
- missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
701
- }
702
- if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
703
- missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
1007
+ if (feature.properties.mission.missionLayer.name) {
1008
+ cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
704
1009
  }
705
- if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
706
- missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
1010
+
1011
+ if (feature.properties.mission.missionLayer.parentUid) {
1012
+ cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
707
1013
  }
708
- if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
709
- missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
1014
+
1015
+ if (feature.properties.mission.missionLayer.type) {
1016
+ cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
710
1017
  }
711
1018
 
712
- mission.missionLayer = missionLayer;
1019
+ if (feature.properties.mission.missionLayer.uid) {
1020
+ cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
1021
+ }
713
1022
  }
714
-
715
- feat.properties.mission = mission;
716
1023
  }
717
1024
 
718
- if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
719
- feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
1025
+ cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
1026
+
1027
+ if (!feature.geometry) {
1028
+ throw new Err(400, null, 'Must have Geometry');
1029
+ } else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
1030
+ throw new Err(400, null, 'Unsupported Geometry Type');
720
1031
  }
721
1032
 
722
- if (['u-d-f', 'u-d-r', 'b-m-r'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
723
- const coordinates = [];
1033
+ if (feature.geometry.type === 'Point') {
1034
+ cot.event.point._attributes.lon = String(feature.geometry.coordinates[0]);
1035
+ cot.event.point._attributes.lat = String(feature.geometry.coordinates[1]);
1036
+ cot.event.point._attributes.hae = String(feature.geometry.coordinates[2] || '0.0');
724
1037
 
725
- for (const l of raw.event.detail.link) {
726
- if (!l._attributes.point) continue;
727
- coordinates.push(l._attributes.point.split(',').map((p: string) => { return Number(p.trim()) }).splice(0, 2).reverse());
728
- }
729
1038
 
730
- if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
731
- const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
732
- feat.properties.stroke = stroke.as_hex();
733
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
1039
+ if (feature.properties['marker-color']) {
1040
+ const color = new Color(feature.properties['marker-color'] || -1761607936);
1041
+ color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
1042
+ cot.event.detail.color = { _attributes: { argb: String(color.as_32bit()) } };
734
1043
  }
735
-
736
- if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
737
- feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
1044
+ } else if (feature.geometry.type === 'Polygon' && feature.properties.type === 'u-d-c-c') {
1045
+ if (!feature.properties.shape || !feature.properties.shape.ellipse) {
1046
+ throw new Err(400, null, 'u-d-c-c (Circle) must define a feature.properties.shape.ellipse property')
738
1047
  }
1048
+ cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } }
739
1049
 
740
- if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
741
- feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
1050
+ if (feature.properties.center) {
1051
+ cot.event.point._attributes.lon = String(feature.properties.center[0]);
1052
+ cot.event.point._attributes.lat = String(feature.properties.center[1]);
1053
+ } else {
1054
+ const centre = PointOnFeature(feature as AllGeoJSON);
1055
+ cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
1056
+ cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
1057
+ cot.event.point._attributes.hae = '0.0';
742
1058
  }
1059
+ } else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
1060
+ const stroke = new Color(feature.properties.stroke || -1761607936);
1061
+ stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
1062
+ cot.event.detail.strokeColor = { _attributes: { value: String(stroke.as_32bit()) } };
743
1063
 
744
- if (raw.event._attributes.type === 'u-d-r' || (coordinates[0][0] === coordinates[coordinates.length -1][0] && coordinates[0][1] === coordinates[coordinates.length -1][1])) {
745
- if (raw.event._attributes.type === 'u-d-r') {
746
- // CoT rectangles are only 4 points - GeoJSON needs to be closed
747
- coordinates.push(coordinates[0])
748
- }
1064
+ if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
1065
+ cot.event.detail.strokeWeight = { _attributes: {
1066
+ value: String(feature.properties['stroke-width'])
1067
+ } };
749
1068
 
750
- feat.geometry = {
751
- type: 'Polygon',
752
- coordinates: [coordinates]
753
- }
1069
+ if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
1070
+ cot.event.detail.strokeStyle = { _attributes: {
1071
+ value: feature.properties['stroke-style']
1072
+ } };
754
1073
 
755
- if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
756
- const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
757
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
758
- feat.properties['fill'] = fill.as_hex();
759
- }
760
- } else {
761
- feat.geometry = {
762
- type: 'LineString',
763
- coordinates
764
- }
765
- }
766
- } else if (raw.event._attributes.type.startsWith('u-d-c-c')) {
767
- if (!raw.event.detail.shape) throw new Err(400, null, 'u-d-c-c (Circle) must define shape value')
768
- if (
769
- !raw.event.detail.shape.ellipse
770
- || !raw.event.detail.shape.ellipse._attributes
771
- ) throw new Err(400, null, 'u-d-c-c (Circle) must define ellipse shape value')
1074
+ if (feature.geometry.type === 'LineString') {
1075
+ cot.event._attributes.type = 'u-d-f';
772
1076
 
773
- const ellipse = {
774
- major: Number(raw.event.detail.shape.ellipse._attributes.major),
775
- minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
776
- angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
777
- }
1077
+ if (!cot.event.detail.link) cot.event.detail.link = [];
1078
+ else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
778
1079
 
779
- feat.geometry = Truncate(Ellipse(
780
- feat.geometry.coordinates as number[],
781
- Number(ellipse.major) / 1000,
782
- Number(ellipse.minor) / 1000,
783
- {
784
- angle: ellipse.angle
1080
+ for (const coord of feature.geometry.coordinates) {
1081
+ cot.event.detail.link.push({
1082
+ _attributes: { point: `${coord[1]},${coord[0]}` }
1083
+ });
785
1084
  }
786
- ), {
787
- precision: COORDINATE_PRECISION,
788
- mutate: true
789
- }).geometry as Static<typeof Polygon>;
1085
+ } else if (feature.geometry.type === 'Polygon') {
1086
+ cot.event._attributes.type = 'u-d-f';
790
1087
 
791
- feat.properties.shape = {};
792
- feat.properties.shape.ellipse = ellipse;
793
- } else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
794
- // TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
795
- if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
796
- const coordinates = [];
1088
+ if (!cot.event.detail.link) cot.event.detail.link = [];
1089
+ else if (!Array.isArray(cot.event.detail.link)) cot.event.detail.link = [cot.event.detail.link]
797
1090
 
798
- const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
799
- for (const v of vertices) {
800
- coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
1091
+ // Inner rings are not yet supported
1092
+ for (const coord of feature.geometry.coordinates[0]) {
1093
+ cot.event.detail.link.push({
1094
+ _attributes: { point: `${coord[1]},${coord[0]}` }
1095
+ });
801
1096
  }
802
1097
 
803
- if (coordinates.length === 1) {
804
- feat.geometry = { type: 'Point', coordinates: coordinates[0] }
805
- } else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
806
- coordinates.push(coordinates[0]);
807
- feat.geometry = { type: 'Polygon', coordinates: [coordinates] }
808
- } else {
809
- feat.geometry = { type: 'LineString', coordinates }
810
- }
1098
+ const fill = new Color(feature.properties.fill || -1761607936);
1099
+ fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
1100
+ cot.event.detail.fillColor = { _attributes: { value: String(fill.as_32bit()) } };
811
1101
  }
812
1102
 
813
- if (
814
- raw.event.detail.shape
815
- && raw.event.detail.shape.polyline
816
- && raw.event.detail.shape.polyline._attributes
817
- && raw.event.detail.shape.polyline._attributes
818
- ) {
819
- if (raw.event.detail.shape.polyline._attributes.fillColor) {
820
- const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
821
- feat.properties['fill-opacity'] = fill.as_opacity() / 255;
822
- feat.properties['fill'] = fill.as_hex();
823
- }
1103
+ cot.event.detail.labels_on = { _attributes: { value: 'false' } };
1104
+ cot.event.detail.tog = { _attributes: { enabled: '0' } };
824
1105
 
825
- if (raw.event.detail.shape.polyline._attributes.color) {
826
- const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
827
- feat.properties.stroke = stroke.as_hex();
828
- feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
1106
+ if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
1107
+ cot.event.point._attributes.lon = String(feature.properties.center[0]);
1108
+ cot.event.point._attributes.lat = String(feature.properties.center[1]);
1109
+
1110
+ if (feature.properties.center.length >= 3) {
1111
+ cot.event.point._attributes.hae = String(feature.properties.center[2] || '0.0');
1112
+ } else {
1113
+ cot.event.point._attributes.hae = '0.0';
829
1114
  }
1115
+ } else {
1116
+ const centre = PointOnFeature(feature as AllGeoJSON);
1117
+ cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
1118
+ cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
1119
+ cot.event.point._attributes.hae = '0.0';
830
1120
  }
831
1121
  }
832
1122
 
833
- if (raw.event.detail.color && raw.event.detail.color._attributes && raw.event.detail.color._attributes.argb) {
834
- const color = new Color(Number(raw.event.detail.color._attributes.argb));
835
- feat.properties['marker-color'] = color.as_hex();
836
- feat.properties['marker-opacity'] = color.as_opacity() / 255;
837
- }
838
-
839
- feat.properties.metadata = this.metadata;
840
-
841
- return feat;
842
- }
843
-
844
- to_xml(): string {
845
- return xmljs.js2xml(this.raw, { compact: true });
846
- }
847
-
848
- /**
849
- * Return a CoT Message
850
- */
851
- static ping(): CoT {
852
- return new CoT({
853
- event: {
854
- _attributes: Util.cot_event_attr('t-x-c-t', 'h-g-i-g-o'),
855
- detail: {},
856
- point: Util.cot_point()
857
- }
858
- });
859
- }
860
-
861
- /**
862
- * Determines if the CoT message represents a Tasking Message
863
- *
864
- * @return {boolean}
865
- */
866
- is_tasking(): boolean {
867
- return !!this.raw.event._attributes.type.match(/^t-/)
868
- }
869
-
870
- /**
871
- * Determines if the CoT message represents a Chat Message
872
- *
873
- * @return {boolean}
874
- */
875
- is_chat(): boolean {
876
- return !!(this.raw.event.detail && this.raw.event.detail.__chat);
877
- }
878
-
879
- /**
880
- * Determines if the CoT message represents a Friendly Element
881
- *
882
- * @return {boolean}
883
- */
884
- is_friend(): boolean {
885
- return !!this.raw.event._attributes.type.match(/^a-f-/)
886
- }
887
-
888
- /**
889
- * Determines if the CoT message represents a Hostile Element
890
- *
891
- * @return {boolean}
892
- */
893
- is_hostile(): boolean {
894
- return !!this.raw.event._attributes.type.match(/^a-h-/)
895
- }
896
-
897
- /**
898
- * Determines if the CoT message represents a Unknown Element
899
- *
900
- * @return {boolean}
901
- */
902
- is_unknown(): boolean {
903
- return !!this.raw.event._attributes.type.match(/^a-u-/)
904
- }
905
-
906
- /**
907
- * Determines if the CoT message represents a Pending Element
908
- *
909
- * @return {boolean}
910
- */
911
- is_pending(): boolean {
912
- return !!this.raw.event._attributes.type.match(/^a-p-/)
913
- }
914
-
915
- /**
916
- * Determines if the CoT message represents an Assumed Element
917
- *
918
- * @return {boolean}
919
- */
920
- is_assumed(): boolean {
921
- return !!this.raw.event._attributes.type.match(/^a-a-/)
922
- }
923
-
924
- /**
925
- * Determines if the CoT message represents a Neutral Element
926
- *
927
- * @return {boolean}
928
- */
929
- is_neutral(): boolean {
930
- return !!this.raw.event._attributes.type.match(/^a-n-/)
931
- }
932
-
933
- /**
934
- * Determines if the CoT message represents a Suspect Element
935
- *
936
- * @return {boolean}
937
- */
938
- is_suspect(): boolean {
939
- return !!this.raw.event._attributes.type.match(/^a-s-/)
940
- }
941
-
942
- /**
943
- * Determines if the CoT message represents a Joker Element
944
- *
945
- * @return {boolean}
946
- */
947
- is_joker(): boolean {
948
- return !!this.raw.event._attributes.type.match(/^a-j-/)
949
- }
950
-
951
- /**
952
- * Determines if the CoT message represents a Faker Element
953
- *
954
- * @return {boolean}
955
- */
956
- is_faker(): boolean {
957
- return !!this.raw.event._attributes.type.match(/^a-k-/)
958
- }
959
-
960
- /**
961
- * Determines if the CoT message represents an Element
962
- *
963
- * @return {boolean}
964
- */
965
- is_atom(): boolean {
966
- return !!this.raw.event._attributes.type.match(/^a-/)
967
- }
968
-
969
- /**
970
- * Determines if the CoT message represents an Airborne Element
971
- *
972
- * @return {boolean}
973
- */
974
- is_airborne(): boolean {
975
- return !!this.raw.event._attributes.type.match(/^a-.-A/)
976
- }
977
-
978
- /**
979
- * Determines if the CoT message represents a Ground Element
980
- *
981
- * @return {boolean}
982
- */
983
- is_ground(): boolean {
984
- return !!this.raw.event._attributes.type.match(/^a-.-G/)
985
- }
986
-
987
- /**
988
- * Determines if the CoT message represents an Installation
989
- *
990
- * @return {boolean}
991
- */
992
- is_installation(): boolean {
993
- return !!this.raw.event._attributes.type.match(/^a-.-G-I/)
994
- }
995
-
996
- /**
997
- * Determines if the CoT message represents a Vehicle
998
- *
999
- * @return {boolean}
1000
- */
1001
- is_vehicle(): boolean {
1002
- return !!this.raw.event._attributes.type.match(/^a-.-G-E-V/)
1003
- }
1004
-
1005
- /**
1006
- * Determines if the CoT message represents Equipment
1007
- *
1008
- * @return {boolean}
1009
- */
1010
- is_equipment(): boolean {
1011
- return !!this.raw.event._attributes.type.match(/^a-.-G-E/)
1012
- }
1123
+ const newcot = new CoT(cot);
1013
1124
 
1014
- /**
1015
- * Determines if the CoT message represents a Surface Element
1016
- *
1017
- * @return {boolean}
1018
- */
1019
- is_surface(): boolean {
1020
- return !!this.raw.event._attributes.type.match(/^a-.-S/)
1021
- }
1125
+ if (feature.properties.metadata) {
1126
+ newcot.metadata = feature.properties.metadata
1127
+ }
1022
1128
 
1023
- /**
1024
- * Determines if the CoT message represents a Subsurface Element
1025
- *
1026
- * @return {boolean}
1027
- */
1028
- is_subsurface(): boolean {
1029
- return !!this.raw.event._attributes.type.match(/^a-.-U/)
1129
+ return newcot;
1030
1130
  }
1031
1131
 
1032
- /**
1033
- * Determines if the CoT message represents a UAV Element
1034
- *
1035
- * @return {boolean}
1036
- */
1037
- is_uav(): boolean {
1038
- return !!this.raw.event._attributes.type.match(/^a-f-A-M-F-Q-r/)
1039
- }
1040
1132
  }