@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.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/{xml.js → cot.js} +167 -13
- package/dist/lib/cot.js.map +1 -0
- package/dist/lib/util.js +1 -1
- package/dist/lib/util.js.map +1 -1
- package/dist/test/cot-itak.test.js +62 -0
- package/dist/test/cot-itak.test.js.map +1 -0
- package/dist/test/cot.test.js +5 -5
- package/dist/test/cot.test.js.map +1 -1
- package/dist/test/cot_is.test.js +353 -0
- package/dist/test/cot_is.test.js.map +1 -0
- package/dist/test/from_geojson.test.js +26 -27
- package/dist/test/from_geojson.test.js.map +1 -1
- package/dist/test/reversal.test.js +15 -0
- package/dist/test/reversal.test.js.map +1 -0
- package/index.ts +2 -2
- package/lib/{xml.ts → cot.ts} +189 -16
- package/lib/util.ts +2 -2
- package/package.json +1 -1
- package/test/cot-itak.test.ts +65 -0
- package/test/cot.test.ts +5 -5
- package/test/cot_is.test.ts +431 -0
- package/test/fixtures/basic.geojson +16 -0
- package/test/fixtures/hae.json +16 -0
- package/test/fixtures/icon.geojson +17 -0
- package/test/fixtures/props.geojson +19 -0
- package/test/from_geojson.test.ts +26 -27
- package/test/reversal.test.ts +17 -0
- package/dist/lib/xml.js.map +0 -1
package/lib/{xml.ts → cot.ts}
RENAMED
|
@@ -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
|
|
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
|
|
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
|
|
103
|
+
* Return an CoT Message
|
|
104
104
|
*
|
|
105
105
|
* @param {Object} feature GeoJSON Point Feature
|
|
106
106
|
*
|
|
107
|
-
* @return {
|
|
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
|
|
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
|
|
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 './
|
|
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 (
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
193
|
+
t.deepEquals((new CoT(geochat)).raw, {
|
|
194
194
|
'event': {
|
|
195
195
|
'_attributes': {
|
|
196
196
|
'version': '2.0',
|