@tak-ps/node-cot 2.2.0 → 2.3.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/.github/workflows/test.yml +32 -0
- package/CHANGELOG.md +8 -0
- package/package.json +2 -1
- package/src/util.js +2 -2
- package/src/xml.js +35 -7
- package/test/cot.test.js +21 -21
- package/test/from_geojson.test.js +78 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- synchronize
|
|
8
|
+
- reopened
|
|
9
|
+
- ready_for_review
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
if: github.event.pull_request.draft == false
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v3
|
|
17
|
+
with:
|
|
18
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-node@v3
|
|
21
|
+
with:
|
|
22
|
+
node-version: 18
|
|
23
|
+
registry-url: https://registry.npmjs.org/
|
|
24
|
+
|
|
25
|
+
- name: Install
|
|
26
|
+
run: npm install
|
|
27
|
+
|
|
28
|
+
- name: Test
|
|
29
|
+
run: npm test
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: npm run lint
|
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tak-ps/node-cot",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"description": "Lightweight JavaScript library for parsing and manipulating TAK messages",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"lint": "eslint *.js src/*.js test/*.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@turf/point-on-feature": "^6.5.0",
|
|
12
13
|
"protobufjs": "^7.1.2",
|
|
13
14
|
"uuid": "^9.0.0",
|
|
14
15
|
"xml-js": "^1.6.11"
|
package/src/util.js
CHANGED
|
@@ -27,13 +27,13 @@ export default class Util {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Return an event.
|
|
30
|
+
* Return an event.detail object with as many defaults as possible
|
|
31
31
|
*
|
|
32
32
|
* @param {String} [callsign=UNKNOWN] Display Callsign
|
|
33
33
|
*
|
|
34
34
|
* @returns {Object}
|
|
35
35
|
*/
|
|
36
|
-
static
|
|
36
|
+
static cot_event_detail(callsign = 'UNKNOWN') {
|
|
37
37
|
return {
|
|
38
38
|
contact: {
|
|
39
39
|
_attributes: { callsign }
|
package/src/xml.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import xmljs from 'xml-js';
|
|
2
2
|
import Util from './util.js';
|
|
3
|
+
import PointOnFeature from '@turf/point-on-feature';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Convert to and from an XML CoT message
|
|
@@ -20,9 +21,9 @@ export default class XMLCot {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
// Attempt to cast all point to numerics
|
|
23
|
-
for (const key of Object.keys(this.raw.event.point)) {
|
|
24
|
-
if (!isNaN(parseFloat(this.raw.event.point[key]))) {
|
|
25
|
-
this.raw.event.point[key] = parseFloat(this.raw.event.point[key]);
|
|
24
|
+
for (const key of Object.keys(this.raw.event.point._attributes)) {
|
|
25
|
+
if (!isNaN(parseFloat(this.raw.event.point._attributes[key]))) {
|
|
26
|
+
this.raw.event.point._attributes[key] = parseFloat(this.raw.event.point._attributes[key]);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -36,14 +37,13 @@ export default class XMLCot {
|
|
|
36
37
|
*/
|
|
37
38
|
static from_geojson(feature) {
|
|
38
39
|
if (feature.type !== 'Feature') throw new Error('Must be GeoJSON Feature');
|
|
39
|
-
if (!feature.geometry || feature.geometry.type !== 'Point') throw new Error('Must be GeoJSON Point Feature');
|
|
40
40
|
if (!feature.properties) throw new Error('Feature must have properties');
|
|
41
41
|
|
|
42
42
|
const cot = {
|
|
43
43
|
event: {
|
|
44
44
|
_attributes: Util.cot_event_attr(feature.properties.type || 'a-f-G', feature.properties.how || 'm-g'),
|
|
45
45
|
point: Util.cot_point(),
|
|
46
|
-
|
|
46
|
+
detail: Util.cot_event_detail(feature.properties.callsign)
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
|
|
@@ -54,8 +54,36 @@ export default class XMLCot {
|
|
|
54
54
|
if (feature.properties[key]) cot.event._attributes[key] = feature.properties[key];
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (!feature.geometry) throw new Error('Must have Geometry');
|
|
58
|
+
if (!['Point', 'Polygon'].includes(feature.geometry.type)) throw new Error('Unsupported Geoemtry Type');
|
|
59
|
+
|
|
60
|
+
if (feature.geometry.type === 'Point') {
|
|
61
|
+
cot.event.point._attributes.lon = feature.geometry.coordinates[0];
|
|
62
|
+
cot.event.point._attributes.lat = feature.geometry.coordinates[1];
|
|
63
|
+
} else if (feature.geometry.type === 'Polygon') {
|
|
64
|
+
// Inner rings are not yet supported
|
|
65
|
+
|
|
66
|
+
cot.event.detail.link = [];
|
|
67
|
+
feature.geometry.coordinates[0].pop(); // Dont' Close Loop in COT
|
|
68
|
+
for (const coord of feature.geometry.coordinates[0]) {
|
|
69
|
+
cot.event.detail.link.push({
|
|
70
|
+
_attributes: {
|
|
71
|
+
point: `${coord[1]},${coord[0]}`
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cot.event.detail.labels_on = { _attributes: { value: 'false' } };
|
|
77
|
+
cot.event.detail.tog = { _attributes: { enabled: '0' } };
|
|
78
|
+
cot.event.detail.strokeColor = { _attributes: { value: '-256' } };
|
|
79
|
+
cot.event.detail.strokeWeight = { _attributes: { value: '3.0' } };
|
|
80
|
+
cot.event.detail.strokeStyle = { _attributes: { value: 'solid' } };
|
|
81
|
+
cot.event.detail.fillColor = { _attributes: { value: '-1761607936' } };
|
|
82
|
+
|
|
83
|
+
const centre = PointOnFeature(feature);
|
|
84
|
+
cot.event.point._attributes.lon = centre.geometry.coordinates[0];
|
|
85
|
+
cot.event.point._attributes.lat = centre.geometry.coordinates[1];
|
|
86
|
+
}
|
|
59
87
|
|
|
60
88
|
return new XMLCot(cot);
|
|
61
89
|
}
|
package/test/cot.test.js
CHANGED
|
@@ -17,11 +17,11 @@ test('Decode COT message', (t) => {
|
|
|
17
17
|
},
|
|
18
18
|
'point': {
|
|
19
19
|
'_attributes': {
|
|
20
|
-
'lat':
|
|
21
|
-
'lon':
|
|
22
|
-
'hae':
|
|
23
|
-
'ce':
|
|
24
|
-
'le':
|
|
20
|
+
'lat': 1.234567,
|
|
21
|
+
'lon': -3.141592,
|
|
22
|
+
'hae': -25.7,
|
|
23
|
+
'ce': 9.9,
|
|
24
|
+
'le': 9999999.0
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
'detail': {
|
|
@@ -68,11 +68,11 @@ test('Decode COT message', (t) => {
|
|
|
68
68
|
},
|
|
69
69
|
'point': {
|
|
70
70
|
'_attributes': {
|
|
71
|
-
'lat':
|
|
72
|
-
'lon':
|
|
73
|
-
'hae':
|
|
74
|
-
'ce':
|
|
75
|
-
'le':
|
|
71
|
+
'lat': 0.000000,
|
|
72
|
+
'lon': 0.000000,
|
|
73
|
+
'hae': 0.0,
|
|
74
|
+
'ce': 9999999.0,
|
|
75
|
+
'le': 9999999.0
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
'detail': {
|
|
@@ -134,11 +134,11 @@ test('Encode COT message', (t) => {
|
|
|
134
134
|
},
|
|
135
135
|
'point': {
|
|
136
136
|
'_attributes': {
|
|
137
|
-
'lat':
|
|
138
|
-
'lon':
|
|
139
|
-
'hae':
|
|
140
|
-
'ce':
|
|
141
|
-
'le':
|
|
137
|
+
'lat': 1.234567,
|
|
138
|
+
'lon': -3.141592,
|
|
139
|
+
'hae': -25.7,
|
|
140
|
+
'ce': 9.9,
|
|
141
|
+
'le': 9999999
|
|
142
142
|
}
|
|
143
143
|
},
|
|
144
144
|
'detail': {
|
|
@@ -168,7 +168,7 @@ test('Encode COT message', (t) => {
|
|
|
168
168
|
|
|
169
169
|
t.deepEquals(
|
|
170
170
|
(new XML(packet)).to_xml(),
|
|
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
|
|
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
|
|
|
174
174
|
t.end();
|
|
@@ -203,11 +203,11 @@ test('Parse GeoChat message', (t) => {
|
|
|
203
203
|
},
|
|
204
204
|
'point': {
|
|
205
205
|
'_attributes': {
|
|
206
|
-
'lat':
|
|
207
|
-
'lon':
|
|
208
|
-
'hae':
|
|
209
|
-
'ce':
|
|
210
|
-
'le':
|
|
206
|
+
'lat': 1.234567,
|
|
207
|
+
'lon': -3.141592,
|
|
208
|
+
'hae': -25.8,
|
|
209
|
+
'ce': 9.9,
|
|
210
|
+
'le': 9999999
|
|
211
211
|
}
|
|
212
212
|
},
|
|
213
213
|
'detail': {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'tape';
|
|
2
|
+
import { XML } from '../index.js';
|
|
3
|
+
|
|
4
|
+
test('XML.from_geojson - point', (t) => {
|
|
5
|
+
const geo = XML.from_geojson({
|
|
6
|
+
type: 'Feature',
|
|
7
|
+
properties: {},
|
|
8
|
+
geometry: {
|
|
9
|
+
type: 'Point',
|
|
10
|
+
coordinates: [1.1, 2.2]
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
t.equals(geo.raw.event._attributes.version, '2.0');
|
|
15
|
+
t.equals(geo.raw.event._attributes.type, 'a-f-G');
|
|
16
|
+
t.equals(geo.raw.event._attributes.how, 'm-g');
|
|
17
|
+
t.equals(geo.raw.event._attributes.uid.length, 36);
|
|
18
|
+
t.equals(geo.raw.event._attributes.time.length, 24);
|
|
19
|
+
t.equals(geo.raw.event._attributes.start.length, 24);
|
|
20
|
+
t.equals(geo.raw.event._attributes.stale.length, 24);
|
|
21
|
+
|
|
22
|
+
t.deepEquals(geo.raw.event.point, {
|
|
23
|
+
_attributes: { lat: 2.2, lon: 1.1, hae: 0, ce: 9999999, le: 9999999 }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
t.deepEquals(geo.raw.event.detail, {
|
|
27
|
+
contact: { _attributes: { callsign: 'UNKNOWN' } }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
t.end();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('XML.from_geojson - polygon', (t) => {
|
|
34
|
+
const geo = XML.from_geojson({
|
|
35
|
+
type: 'Feature',
|
|
36
|
+
properties: {},
|
|
37
|
+
geometry: {
|
|
38
|
+
type: 'Polygon',
|
|
39
|
+
coordinates: [[
|
|
40
|
+
[-108.587, 39.098],
|
|
41
|
+
[-108.587, 39.032],
|
|
42
|
+
[-108.505, 39.032],
|
|
43
|
+
[-108.505, 39.098],
|
|
44
|
+
[-108.587, 39.098]
|
|
45
|
+
]]
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t.equals(geo.raw.event._attributes.version, '2.0');
|
|
50
|
+
t.equals(geo.raw.event._attributes.type, 'a-f-G');
|
|
51
|
+
t.equals(geo.raw.event._attributes.how, 'm-g');
|
|
52
|
+
t.equals(geo.raw.event._attributes.uid.length, 36);
|
|
53
|
+
t.equals(geo.raw.event._attributes.time.length, 24);
|
|
54
|
+
t.equals(geo.raw.event._attributes.start.length, 24);
|
|
55
|
+
t.equals(geo.raw.event._attributes.stale.length, 24);
|
|
56
|
+
|
|
57
|
+
t.deepEquals(geo.raw.event.point, {
|
|
58
|
+
_attributes: { lat: 39.065, lon: -108.54599999999999, hae: 0, ce: 9999999, le: 9999999 }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
t.deepEquals(geo.raw.event.detail, {
|
|
62
|
+
contact: { _attributes: { callsign: 'UNKNOWN' } },
|
|
63
|
+
link: [
|
|
64
|
+
{ _attributes: { point: '39.098,-108.587' } },
|
|
65
|
+
{ _attributes: { point: '39.032,-108.587' } },
|
|
66
|
+
{ _attributes: { point: '39.032,-108.505' } },
|
|
67
|
+
{ _attributes: { point: '39.098,-108.505' } }
|
|
68
|
+
],
|
|
69
|
+
labels_on: { _attributes: { value: 'false' } },
|
|
70
|
+
tog: { _attributes: { enabled: '0' } },
|
|
71
|
+
strokeColor: { _attributes: { value: '-256' } },
|
|
72
|
+
strokeWeight: { _attributes: { value: '3.0' } },
|
|
73
|
+
strokeStyle: { _attributes: { value: 'solid' } },
|
|
74
|
+
fillColor: { _attributes: { value: '-1761607936' } }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
t.end();
|
|
78
|
+
});
|