@tak-ps/node-cot 3.5.4 → 4.1.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.
@@ -80,14 +80,14 @@ export interface JSONCoT {
80
80
  *
81
81
  * @prop raw Raw XML-JS representation of CoT
82
82
  */
83
- export default class XMLCot {
83
+ export default class CoT {
84
84
  raw: JSONCoT;
85
85
 
86
86
  constructor(cot: Buffer | JSONCoT | string) {
87
87
  if (typeof cot === 'string' || cot instanceof Buffer) {
88
88
  if (cot instanceof Buffer) cot = String(cot);
89
89
 
90
- const raw: any = xmljs.xml2js(cot, { compact: true });
90
+ const raw = xmljs.xml2js(cot, { compact: true });
91
91
  this.raw = raw as JSONCoT;
92
92
  } else {
93
93
  this.raw = cot;
@@ -100,11 +100,11 @@ export default class XMLCot {
100
100
  }
101
101
 
102
102
  /**
103
- * Return an XMLCot Message
103
+ * Return an CoT Message
104
104
  *
105
105
  * @param {Object} feature GeoJSON Point Feature
106
106
  *
107
- * @return {XMLCot}
107
+ * @return {CoT}
108
108
  */
109
109
  static from_geojson(feature: Feature) {
110
110
  if (feature.type !== 'Feature') throw new Error('Must be GeoJSON Feature');
@@ -141,6 +141,8 @@ export default class XMLCot {
141
141
  }
142
142
  }
143
143
 
144
+ cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
145
+
144
146
  if (!feature.geometry) throw new Error('Must have Geometry');
145
147
  if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) throw new Error('Unsupported Geometry Type');
146
148
 
@@ -192,14 +194,12 @@ export default class XMLCot {
192
194
  cot.event.detail.labels_on = { _attributes: { value: 'false' } };
193
195
  cot.event.detail.tog = { _attributes: { enabled: '0' } };
194
196
 
195
- cot.event.detail.remarks = { _attributes: { }, _text: feature.properties.remarks || '' };
196
-
197
197
  const centre = PointOnFeature(feature as AllGeoJSON);
198
198
  cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
199
199
  cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
200
200
  }
201
201
 
202
- return new XMLCot(cot);
202
+ return new CoT(cot);
203
203
  }
204
204
 
205
205
  /**
@@ -220,7 +220,7 @@ export default class XMLCot {
220
220
  how: raw.event._attributes.how,
221
221
  time: raw.event._attributes.time,
222
222
  start: raw.event._attributes.start,
223
- stale: raw.event._attributes.stale
223
+ stale: raw.event._attributes.stale,
224
224
  },
225
225
  geometry: {
226
226
  type: 'Point',
@@ -232,22 +232,33 @@ export default class XMLCot {
232
232
  }
233
233
  };
234
234
 
235
+ if (!geojson.properties) geojson.properties = {};
236
+
237
+ if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
238
+ geojson.properties.remarks = raw.event.detail.remarks._text;
239
+ }
240
+
241
+ if (raw.event.detail.track && raw.event.detail.track._attributes) {
242
+ if (raw.event.detail.track._attributes.course) geojson.properties.course = Number(raw.event.detail.track._attributes.course);
243
+ if (raw.event.detail.track._attributes.course) geojson.properties.speed = Number(raw.event.detail.track._attributes.speed);
244
+ }
245
+
246
+ if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
247
+ geojson.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
248
+ }
249
+
235
250
  return geojson;
236
251
  }
237
252
 
238
- to_xml() {
239
- return xmljs.js2xml(this.raw, {
240
- compact: true
241
- });
253
+ to_xml(): string {
254
+ return xmljs.js2xml(this.raw, { compact: true });
242
255
  }
243
256
 
244
257
  /**
245
258
  * Return a CoT Message
246
- *
247
- * @returns {XMLCot}
248
259
  */
249
- static ping() {
250
- return new XMLCot({
260
+ static ping(): CoT {
261
+ return new CoT({
251
262
  event: {
252
263
  _attributes: Util.cot_event_attr('t-x-c-t', 'h-g-i-g-o'),
253
264
  detail: {},
@@ -255,4 +266,166 @@ export default class XMLCot {
255
266
  }
256
267
  });
257
268
  }
269
+
270
+ /**
271
+ * Determines if the CoT message represents a Friendly Element
272
+ *
273
+ * @return {boolean}
274
+ */
275
+ is_friend(): boolean {
276
+ return !!this.raw.event._attributes.type.match(/^a-f-/)
277
+ }
278
+
279
+ /**
280
+ * Determines if the CoT message represents a Hostile Element
281
+ *
282
+ * @return {boolean}
283
+ */
284
+ is_hostile(): boolean {
285
+ return !!this.raw.event._attributes.type.match(/^a-h-/)
286
+ }
287
+
288
+ /**
289
+ * Determines if the CoT message represents a Unknown Element
290
+ *
291
+ * @return {boolean}
292
+ */
293
+ is_unknown(): boolean {
294
+ return !!this.raw.event._attributes.type.match(/^a-u-/)
295
+ }
296
+
297
+ /**
298
+ * Determines if the CoT message represents a Pending Element
299
+ *
300
+ * @return {boolean}
301
+ */
302
+ is_pending(): boolean {
303
+ return !!this.raw.event._attributes.type.match(/^a-p-/)
304
+ }
305
+
306
+ /**
307
+ * Determines if the CoT message represents an Assumed Element
308
+ *
309
+ * @return {boolean}
310
+ */
311
+ is_assumed(): boolean {
312
+ return !!this.raw.event._attributes.type.match(/^a-a-/)
313
+ }
314
+
315
+ /**
316
+ * Determines if the CoT message represents a Neutral Element
317
+ *
318
+ * @return {boolean}
319
+ */
320
+ is_neutral(): boolean {
321
+ return !!this.raw.event._attributes.type.match(/^a-n-/)
322
+ }
323
+
324
+ /**
325
+ * Determines if the CoT message represents a Suspect Element
326
+ *
327
+ * @return {boolean}
328
+ */
329
+ is_suspect(): boolean {
330
+ return !!this.raw.event._attributes.type.match(/^a-s-/)
331
+ }
332
+
333
+ /**
334
+ * Determines if the CoT message represents a Joker Element
335
+ *
336
+ * @return {boolean}
337
+ */
338
+ is_joker(): boolean {
339
+ return !!this.raw.event._attributes.type.match(/^a-j-/)
340
+ }
341
+
342
+ /**
343
+ * Determines if the CoT message represents a Faker Element
344
+ *
345
+ * @return {boolean}
346
+ */
347
+ is_faker(): boolean {
348
+ return !!this.raw.event._attributes.type.match(/^a-k-/)
349
+ }
350
+
351
+ /**
352
+ * Determines if the CoT message represents an Element
353
+ *
354
+ * @return {boolean}
355
+ */
356
+ is_atom(): boolean {
357
+ return !!this.raw.event._attributes.type.match(/^a-/)
358
+ }
359
+
360
+ /**
361
+ * Determines if the CoT message represents an Airborne Element
362
+ *
363
+ * @return {boolean}
364
+ */
365
+ is_airborne(): boolean {
366
+ return !!this.raw.event._attributes.type.match(/^a-.-A/)
367
+ }
368
+
369
+ /**
370
+ * Determines if the CoT message represents a Ground Element
371
+ *
372
+ * @return {boolean}
373
+ */
374
+ is_ground(): boolean {
375
+ return !!this.raw.event._attributes.type.match(/^a-.-G/)
376
+ }
377
+
378
+ /**
379
+ * Determines if the CoT message represents an Installation
380
+ *
381
+ * @return {boolean}
382
+ */
383
+ is_installation(): boolean {
384
+ return !!this.raw.event._attributes.type.match(/^a-.-G-I/)
385
+ }
386
+
387
+ /**
388
+ * Determines if the CoT message represents a Vehicle
389
+ *
390
+ * @return {boolean}
391
+ */
392
+ is_vehicle(): boolean {
393
+ return !!this.raw.event._attributes.type.match(/^a-.-G-E-V/)
394
+ }
395
+
396
+ /**
397
+ * Determines if the CoT message represents Equipment
398
+ *
399
+ * @return {boolean}
400
+ */
401
+ is_equipment(): boolean {
402
+ return !!this.raw.event._attributes.type.match(/^a-.-G-E/)
403
+ }
404
+
405
+ /**
406
+ * Determines if the CoT message represents a Surface Element
407
+ *
408
+ * @return {boolean}
409
+ */
410
+ is_surface(): boolean {
411
+ return !!this.raw.event._attributes.type.match(/^a-.-S/)
412
+ }
413
+
414
+ /**
415
+ * Determines if the CoT message represents a Subsurface Element
416
+ *
417
+ * @return {boolean}
418
+ */
419
+ is_subsurface(): boolean {
420
+ return !!this.raw.event._attributes.type.match(/^a-.-U/)
421
+ }
422
+
423
+ /**
424
+ * Determines if the CoT message represents a UAV Element
425
+ *
426
+ * @return {boolean}
427
+ */
428
+ is_uav(): boolean {
429
+ return !!this.raw.event._attributes.type.match(/^a-f-A-M-F-Q-r/)
430
+ }
258
431
  }
package/lib/util.ts CHANGED
@@ -4,7 +4,7 @@ import type {
4
4
  TrackAttributes,
5
5
  Detail,
6
6
  Point
7
- } from './xml.js';
7
+ } from './cot.js';
8
8
 
9
9
  /**
10
10
  * Helper functions for generating CoT data
@@ -120,7 +120,7 @@ export default class Util {
120
120
  start: (new Date(start || now)).toISOString(),
121
121
  stale: (new Date(new Date(start || now).getTime() + 20 * 1000)).toISOString()
122
122
  };
123
- } else if (!isNaN(parseInt(String(stale)))) {
123
+ } else if (typeof stale === 'number') {
124
124
  return {
125
125
  time: (new Date(time || now)).toISOString(),
126
126
  start: (new Date(start || now)).toISOString(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tak-ps/node-cot",
3
3
  "type": "module",
4
- "version": "3.5.4",
4
+ "version": "4.1.0",
5
5
  "description": "Lightweight JavaScript library for parsing and manipulating TAK messages",
6
6
  "author": "Nick Ingalls <nick@ingalls.ca>",
7
7
  "main": "dist/index.js",
@@ -0,0 +1,65 @@
1
+ import test from 'tape';
2
+ import CoT from '../index.js';
3
+
4
+ test('Decode iTAK COT message', (t) => {
5
+ const packet = '<event version="2.0" uid="C94B9215-9BD4-4DBE-BDE1-83625F09153F" type="a-f-G-E-V-C" time="2023-07-18T15:23:09.00Z" start="2023-07-18T15:23:09.00Z" stale="2023-07-18T15:25:09.00Z" how="m-g"><point lat="41.52309645" lon="-107.72376567" hae="1681.23725821" ce="9999999" le="9999999" /><detail><contact callsign="DFPC-iSchmidt" phone="7204258729" endpoint="*:-1:stcp" /><uid Droid="DFPC-iSchmidt" /><__group name="Yellow" role="Team Member" /><precisionlocation geopointsrc="GPS" altsrc="???" /><status battery="100" /><takv device="iPhone" platform="iTAK" os="16.5.1" version="2.7.0.609" /><track speed="0.00000000" course="137.23542786" /></detail></event>';
6
+
7
+ t.deepEquals((new CoT(packet)).raw, {
8
+ 'event': {
9
+ '_attributes': {
10
+ 'version': '2.0',
11
+ 'uid': 'C94B9215-9BD4-4DBE-BDE1-83625F09153F',
12
+ 'type': 'a-f-G-E-V-C',
13
+ 'time': '2023-07-18T15:23:09.00Z',
14
+ 'start': '2023-07-18T15:23:09.00Z',
15
+ 'stale': '2023-07-18T15:25:09.00Z',
16
+ 'how': 'm-g',
17
+ },
18
+ 'point': {
19
+ '_attributes': {
20
+ 'lat': '41.52309645',
21
+ 'lon': '-107.72376567',
22
+ 'hae': '1681.23725821',
23
+ 'ce': '9999999',
24
+ 'le': '9999999'
25
+ }
26
+ },
27
+ detail: {
28
+ contact: {
29
+ _attributes: {
30
+ callsign: 'DFPC-iSchmidt',
31
+ phone: '7204258729',
32
+ endpoint: '*:-1:stcp'
33
+ }
34
+ },
35
+ uid: { _attributes: { Droid: 'DFPC-iSchmidt' } },
36
+ __group: { _attributes: { name: 'Yellow', role: 'Team Member' } },
37
+ precisionlocation: { _attributes: { geopointsrc: 'GPS', altsrc: '???' } },
38
+ status: { _attributes: { battery: '100' } },
39
+ takv: { _attributes: { device: 'iPhone', platform: 'iTAK', os: '16.5.1', version: '2.7.0.609' } },
40
+ track: { _attributes: { speed: '0.00000000', course: '137.23542786' } }
41
+ }
42
+ }
43
+ });
44
+
45
+ t.deepEquals((new CoT(packet)).to_geojson(), {
46
+ id: 'C94B9215-9BD4-4DBE-BDE1-83625F09153F',
47
+ type: 'Feature',
48
+ properties: {
49
+ callsign: 'DFPC-iSchmidt',
50
+ type: 'a-f-G-E-V-C',
51
+ how: 'm-g',
52
+ time: '2023-07-18T15:23:09.00Z',
53
+ start: '2023-07-18T15:23:09.00Z',
54
+ stale: '2023-07-18T15:25:09.00Z',
55
+ course: 137.23542786,
56
+ speed: 0
57
+ },
58
+ geometry: {
59
+ type: 'Point',
60
+ coordinates: [ -107.72376567, 41.52309645, 1681.23725821 ]
61
+ }
62
+ });
63
+
64
+ t.end();
65
+ });
package/test/cot.test.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import test from 'tape';
2
- import { XML } from '../index.js';
2
+ import CoT from '../index.js';
3
3
 
4
4
  test('Decode COT message', (t) => {
5
5
  const packet = '<event version="2.0" uid="ANDROID-deadbeef" type="a-f-G-U-C" how="m-g" time="2021-02-27T20:32:24.771Z" start="2021-02-27T20:32:24.771Z" stale="2021-02-27T20:38:39.771Z"><point lat="1.234567" lon="-3.141592" hae="-25.7" ce="9.9" le="9999999.0"/><detail><takv os="29" version="4.0.0.0 (deadbeef).1234567890-CIV" device="Some Android Device" platform="ATAK-CIV"/><contact xmppUsername="xmpp@host.com" endpoint="*:-1:stcp" callsign="JENNY"/><uid Droid="JENNY"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/><__group role="Team Member" name="Cyan"/><status battery="78"/><track course="80.24833892285461" speed="0.0"/></detail></event>';
6
6
 
7
- t.deepEquals((new XML(packet)).raw, {
7
+ t.deepEquals((new CoT(packet)).raw, {
8
8
  'event': {
9
9
  '_attributes': {
10
10
  'version': '2.0',
@@ -55,7 +55,7 @@ test('Decode COT message', (t) => {
55
55
  test('Decode COT message', (t) => {
56
56
  const packet = '<event version="2.0" uid="TEST-deadbeef" type="a" how="m-g" time="2021-03-12T15:49:07.138Z" start="2021-03-12T15:49:07.138Z" stale="2021-03-12T15:49:07.138Z"><point lat="0.000000" lon="0.000000" hae="0.0" ce="9999999.0" le="9999999.0"/><detail><takv os="Android" version="10" device="Some Device" platform="python unittest"/><status battery="83"/><uid Droid="JENNY"/><contact callsign="JENNY" endpoint="*:-1:stcp" phone="800-867-5309"/><__group role="Team Member" name="Cyan"/><track course="90.1" speed="10.3"/></detail></event>';
57
57
 
58
- t.deepEquals((new XML(packet)).raw, {
58
+ t.deepEquals((new CoT(packet)).raw, {
59
59
  'event': {
60
60
  '_attributes': {
61
61
  'version': '2.0',
@@ -167,7 +167,7 @@ test('Encode COT message', (t) => {
167
167
  };
168
168
 
169
169
  t.deepEquals(
170
- (new XML(packet)).to_xml(),
170
+ (new CoT(packet)).to_xml(),
171
171
  '<event version="2.0" uid="ANDROID-deadbeef" type="a-f-G-U-C" how="m-g" time="2021-02-27T20:32:24.771Z" start="2021-02-27T20:32:24.771Z" stale="2021-02-27T20:38:39.771Z"><point lat="1.234567" lon="-3.141592" hae="-25.7" ce="9.9" le="9999999"/><detail><takv os="29" version="4.0.0.0 (deadbeef).1234567890-CIV" device="Some Android Device" platform="ATAK-CIV"/><contact xmppUsername="xmpp@host.com" endpoint="*:-1:stcp" callsign="JENNY"/><uid Droid="JENNY"/><precisionlocation altsrc="GPS" geopointsrc="GPS"/><__group role="Team Member" name="Cyan"/><status battery="78"/><track course="80.24833892285461" speed="0.0"/></detail></event>'
172
172
  );
173
173
 
@@ -190,7 +190,7 @@ test('Parse GeoChat message', (t) => {
190
190
  ' </detail>\n' +
191
191
  '</event>';
192
192
 
193
- t.deepEquals((new XML(geochat)).raw, {
193
+ t.deepEquals((new CoT(geochat)).raw, {
194
194
  'event': {
195
195
  '_attributes': {
196
196
  'version': '2.0',