@tak-ps/node-cot 2.4.0 → 2.6.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/release.yml +29 -0
- package/.github/workflows/test.yml +6 -3
- package/CHANGELOG.md +9 -0
- package/index.js +1 -1
- package/lib/color.js +46 -0
- package/lib/schema.json +83 -0
- package/{src → lib}/util.js +0 -0
- package/{src → lib}/xml.js +36 -13
- package/package.json +4 -2
- package/test/from_geojson.test.js +5 -6
- package/test/styles.test.js +33 -0
- package/scripts/convert.js +0 -29
- package/scripts/parse.js +0 -34
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: NPM Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v3
|
|
13
|
+
|
|
14
|
+
- name: Get tag
|
|
15
|
+
id: tag
|
|
16
|
+
uses: dawidd6/action-get-tag@v1
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v3
|
|
19
|
+
with:
|
|
20
|
+
node-version: 18
|
|
21
|
+
registry-url: https://registry.npmjs.org/
|
|
22
|
+
|
|
23
|
+
- name: npm install
|
|
24
|
+
run: npm install
|
|
25
|
+
|
|
26
|
+
- name: npm publish
|
|
27
|
+
run: npm publish
|
|
28
|
+
env:
|
|
29
|
+
NPM_TOKEN: ${{ secrets.NPM_SECRET }}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
name: Test
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
4
7
|
pull_request:
|
|
5
8
|
types:
|
|
6
9
|
- opened
|
|
@@ -25,8 +28,8 @@ jobs:
|
|
|
25
28
|
- name: Install
|
|
26
29
|
run: npm install
|
|
27
30
|
|
|
28
|
-
- name: Test
|
|
29
|
-
run: npm test
|
|
30
|
-
|
|
31
31
|
- name: Lint
|
|
32
32
|
run: npm run lint
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: npm test
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
## Version History
|
|
12
12
|
|
|
13
|
+
### v2.6.0
|
|
14
|
+
|
|
15
|
+
- :tada: Add support for passing style properties via GeoJSON Properties
|
|
16
|
+
- :rocket: Add support for encoding/decoding 32bit signed ARGB values
|
|
17
|
+
|
|
18
|
+
### v2.5.0
|
|
19
|
+
|
|
20
|
+
- :tada: Automatically perform basic schema validation on CoT Creation
|
|
21
|
+
|
|
13
22
|
### v2.4.0
|
|
14
23
|
|
|
15
24
|
- :tada: `from_geojson(Feature.LineString)` Support
|
package/index.js
CHANGED
package/lib/color.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import _color from 'color';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper functions for working with CoT Colours
|
|
5
|
+
*
|
|
6
|
+
* @param {Number|Number[]} color 32bit packged ARGB or [A, R, G, B]
|
|
7
|
+
* @class
|
|
8
|
+
*/
|
|
9
|
+
export default class Color {
|
|
10
|
+
constructor(color) {
|
|
11
|
+
if (!isNaN(Number(color))) {
|
|
12
|
+
this.r = (color >> 16) & 255;
|
|
13
|
+
this.g = (color >> 8) & 255;
|
|
14
|
+
this.b = (color >> 0) & 255;
|
|
15
|
+
this.a = ((color >> 24) & 255) / 255;
|
|
16
|
+
} else if (Array.isArray(color)) {
|
|
17
|
+
this.a = color[0];
|
|
18
|
+
this.r = color[1];
|
|
19
|
+
this.g = color[2];
|
|
20
|
+
this.b = color[3];
|
|
21
|
+
} else {
|
|
22
|
+
const c = _color(color);
|
|
23
|
+
|
|
24
|
+
this.a = c.valpha;
|
|
25
|
+
this.r = c.color[0];
|
|
26
|
+
this.g = c.color[1];
|
|
27
|
+
this.b = c.color[2];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
as_32bit() {
|
|
32
|
+
return (this.a << 24) | (this.r << 16) | (this.g << 8) | this.b;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
as_opacity() {
|
|
36
|
+
return this.a;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
as_argb() {
|
|
40
|
+
return [this.a, this.r, this.b, this.g];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
as_rgb() {
|
|
44
|
+
return [this.r, this.b, this.g];
|
|
45
|
+
}
|
|
46
|
+
}
|
package/lib/schema.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"required": [
|
|
4
|
+
"event"
|
|
5
|
+
],
|
|
6
|
+
"properties": {
|
|
7
|
+
"event": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"required": [
|
|
10
|
+
"_attributes",
|
|
11
|
+
"point"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"_attributes": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": [
|
|
17
|
+
"version",
|
|
18
|
+
"uid",
|
|
19
|
+
"type",
|
|
20
|
+
"time",
|
|
21
|
+
"start",
|
|
22
|
+
"stale"
|
|
23
|
+
],
|
|
24
|
+
"properties": {
|
|
25
|
+
"version": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
},
|
|
28
|
+
"uid": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"type": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"time": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"start": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"stale": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"point": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": [
|
|
48
|
+
"_attributes"
|
|
49
|
+
],
|
|
50
|
+
"properties": {
|
|
51
|
+
"_attributes": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"required": [
|
|
54
|
+
"lat",
|
|
55
|
+
"lon",
|
|
56
|
+
"hae",
|
|
57
|
+
"ce",
|
|
58
|
+
"le"
|
|
59
|
+
],
|
|
60
|
+
"properties": {
|
|
61
|
+
"lat": {
|
|
62
|
+
"type": "number"
|
|
63
|
+
},
|
|
64
|
+
"lon": {
|
|
65
|
+
"type": "number"
|
|
66
|
+
},
|
|
67
|
+
"hae": {
|
|
68
|
+
"type": "number"
|
|
69
|
+
},
|
|
70
|
+
"ce": {
|
|
71
|
+
"type": "number"
|
|
72
|
+
},
|
|
73
|
+
"le": {
|
|
74
|
+
"type": "number"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/{src → lib}/util.js
RENAMED
|
File without changes
|
package/{src → lib}/xml.js
RENAMED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import xmljs from 'xml-js';
|
|
2
2
|
import Util from './util.js';
|
|
3
|
+
import Color from './color.js';
|
|
3
4
|
import PointOnFeature from '@turf/point-on-feature';
|
|
5
|
+
import AJV from 'ajv';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
const ajv = (new AJV({ allErrors: true })).compile(JSON.parse(fs.readFileSync(new URL('./schema.json', import.meta.url))));
|
|
9
|
+
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Convert to and from an XML CoT message
|
|
@@ -26,6 +32,11 @@ export default class XMLCot {
|
|
|
26
32
|
this.raw.event.point._attributes[key] = parseFloat(this.raw.event.point._attributes[key]);
|
|
27
33
|
}
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
if (!this.raw.event._attributes.uid) this.raw.event._attributes.uuid = Util.cot_uuid().uid;
|
|
37
|
+
|
|
38
|
+
ajv(this.raw);
|
|
39
|
+
if (ajv.errors) throw new Error(ajv.errors[0].message);
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/**
|
|
@@ -61,36 +72,48 @@ export default class XMLCot {
|
|
|
61
72
|
cot.event.point._attributes.lon = feature.geometry.coordinates[0];
|
|
62
73
|
cot.event.point._attributes.lat = feature.geometry.coordinates[1];
|
|
63
74
|
} else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
|
|
64
|
-
|
|
75
|
+
const stroke = new Color(feature.properties.stroke || -1761607936);
|
|
76
|
+
if (feature.properties['stroke-opacity']) stroke.a = feature.properties['stroke-opacity'];
|
|
77
|
+
cot.event.detail.strokeColor = { _attributes: { value: stroke.as_32bit() } };
|
|
65
78
|
|
|
66
|
-
if (feature.
|
|
67
|
-
|
|
79
|
+
if (!feature.properties['stroke-width']) feature.properties['stroke-width'] = 3;
|
|
80
|
+
cot.event.detail.strokeWeight = { _attributes: {
|
|
81
|
+
value: feature.properties['stroke-width']
|
|
82
|
+
} };
|
|
83
|
+
|
|
84
|
+
if (!feature.properties['stroke-style']) feature.properties['stroke-style'] = 'solid';
|
|
85
|
+
cot.event.detail.strokeStyle = { _attributes: {
|
|
86
|
+
value: feature.properties['stroke-style']
|
|
87
|
+
} };
|
|
88
|
+
|
|
89
|
+
if (feature.geometry.type === 'LineString') {
|
|
90
|
+
cot.event._attributes.type = 'u-d-f';
|
|
68
91
|
|
|
69
|
-
// Inner rings are not yet supported
|
|
70
92
|
cot.event.detail.link = [];
|
|
71
|
-
feature.geometry.coordinates
|
|
72
|
-
for (const coord of feature.geometry.coordinates[0]) {
|
|
93
|
+
for (const coord of feature.geometry.coordinates) {
|
|
73
94
|
cot.event.detail.link.push({
|
|
74
95
|
_attributes: { point: `${coord[1]},${coord[0]}` }
|
|
75
96
|
});
|
|
76
97
|
}
|
|
77
|
-
} else if (feature.geometry.type === '
|
|
78
|
-
cot.event._attributes.type = 'u-d-
|
|
98
|
+
} else if (feature.geometry.type === 'Polygon') {
|
|
99
|
+
cot.event._attributes.type = 'u-d-r';
|
|
79
100
|
|
|
101
|
+
// Inner rings are not yet supported
|
|
80
102
|
cot.event.detail.link = [];
|
|
81
|
-
|
|
103
|
+
feature.geometry.coordinates[0].pop(); // Dont' Close Loop in COT
|
|
104
|
+
for (const coord of feature.geometry.coordinates[0]) {
|
|
82
105
|
cot.event.detail.link.push({
|
|
83
106
|
_attributes: { point: `${coord[1]},${coord[0]}` }
|
|
84
107
|
});
|
|
85
108
|
}
|
|
109
|
+
|
|
110
|
+
const fill = new Color(feature.properties.fill || -1761607936);
|
|
111
|
+
if (feature.properties['fill-opacity']) fill.a = feature.properties['fill-opacity'];
|
|
112
|
+
cot.event.detail.fillColor = { _attributes: { value: fill.as_32bit() } };
|
|
86
113
|
}
|
|
87
114
|
|
|
88
115
|
cot.event.detail.labels_on = { _attributes: { value: 'false' } };
|
|
89
116
|
cot.event.detail.tog = { _attributes: { enabled: '0' } };
|
|
90
|
-
cot.event.detail.strokeColor = { _attributes: { value: '-256' } };
|
|
91
|
-
cot.event.detail.strokeWeight = { _attributes: { value: '3.0' } };
|
|
92
|
-
cot.event.detail.strokeStyle = { _attributes: { value: 'solid' } };
|
|
93
|
-
cot.event.detail.fillColor = { _attributes: { value: '-1761607936' } };
|
|
94
117
|
|
|
95
118
|
const centre = PointOnFeature(feature);
|
|
96
119
|
cot.event.point._attributes.lon = centre.geometry.coordinates[0];
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tak-ps/node-cot",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"description": "Lightweight JavaScript library for parsing and manipulating TAK messages",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "tape test/**.test.js",
|
|
9
|
-
"lint": "eslint *.js
|
|
9
|
+
"lint": "eslint *.js lib/*.js test/*.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@turf/point-on-feature": "^6.5.0",
|
|
13
|
+
"ajv": "^8.11.2",
|
|
14
|
+
"color": "^4.2.3",
|
|
13
15
|
"protobufjs": "^7.1.2",
|
|
14
16
|
"uuid": "^9.0.0",
|
|
15
17
|
"xml-js": "^1.6.11"
|
|
@@ -68,10 +68,10 @@ test('XML.from_geojson - Polygon', (t) => {
|
|
|
68
68
|
],
|
|
69
69
|
labels_on: { _attributes: { value: 'false' } },
|
|
70
70
|
tog: { _attributes: { enabled: '0' } },
|
|
71
|
-
strokeColor: { _attributes: { value:
|
|
72
|
-
strokeWeight: { _attributes: { value:
|
|
71
|
+
strokeColor: { _attributes: { value: 16776960 } },
|
|
72
|
+
strokeWeight: { _attributes: { value: 3 } },
|
|
73
73
|
strokeStyle: { _attributes: { value: 'solid' } },
|
|
74
|
-
fillColor: { _attributes: { value:
|
|
74
|
+
fillColor: { _attributes: { value: 16776960 } }
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
t.end();
|
|
@@ -116,10 +116,9 @@ test('XML.from_geojson - LineString', (t) => {
|
|
|
116
116
|
],
|
|
117
117
|
labels_on: { _attributes: { value: 'false' } },
|
|
118
118
|
tog: { _attributes: { enabled: '0' } },
|
|
119
|
-
strokeColor: { _attributes: { value:
|
|
120
|
-
strokeWeight: { _attributes: { value:
|
|
119
|
+
strokeColor: { _attributes: { value: 16776960 } },
|
|
120
|
+
strokeWeight: { _attributes: { value: 3 } },
|
|
121
121
|
strokeStyle: { _attributes: { value: 'solid' } },
|
|
122
|
-
fillColor: { _attributes: { value: '-1761607936' } }
|
|
123
122
|
});
|
|
124
123
|
|
|
125
124
|
t.end();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from 'tape';
|
|
2
|
+
import { XML } from '../index.js';
|
|
3
|
+
|
|
4
|
+
test('XML.from_geojson - Polygon Style', (t) => {
|
|
5
|
+
const geo = XML.from_geojson({
|
|
6
|
+
type: 'Feature',
|
|
7
|
+
properties: {
|
|
8
|
+
|
|
9
|
+
},
|
|
10
|
+
geometry: {
|
|
11
|
+
type: 'Point',
|
|
12
|
+
coordinates: [1.1, 2.2]
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
t.equals(geo.raw.event._attributes.version, '2.0');
|
|
17
|
+
t.equals(geo.raw.event._attributes.type, 'a-f-G');
|
|
18
|
+
t.equals(geo.raw.event._attributes.how, 'm-g');
|
|
19
|
+
t.equals(geo.raw.event._attributes.uid.length, 36);
|
|
20
|
+
t.equals(geo.raw.event._attributes.time.length, 24);
|
|
21
|
+
t.equals(geo.raw.event._attributes.start.length, 24);
|
|
22
|
+
t.equals(geo.raw.event._attributes.stale.length, 24);
|
|
23
|
+
|
|
24
|
+
t.deepEquals(geo.raw.event.point, {
|
|
25
|
+
_attributes: { lat: 2.2, lon: 1.1, hae: 0, ce: 9999999, le: 9999999 }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
t.deepEquals(geo.raw.event.detail, {
|
|
29
|
+
contact: { _attributes: { callsign: 'UNKNOWN' } }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
t.end();
|
|
33
|
+
});
|
package/scripts/convert.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
const {cot, proto} = require(path.join(__dirname, '../index.js'))
|
|
3
|
-
|
|
4
|
-
const run = (message) => {
|
|
5
|
-
if (!message) {
|
|
6
|
-
console.error('Enter a TAK message')
|
|
7
|
-
return
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const bufferMessage = typeof message !== Buffer ? Buffer.from(message, 'hex') : message
|
|
11
|
-
|
|
12
|
-
if (bufferMessage[0] === 191) { // TAK message format 0xbf
|
|
13
|
-
console.log('TAK message received')
|
|
14
|
-
const trimmedBuffer = bufferMessage.slice(3, bufferMessage.length) // remove tak message header from content
|
|
15
|
-
if (bufferMessage[1] === 0) { // is COT XML
|
|
16
|
-
console.error('Enter a TAK proto message')
|
|
17
|
-
} else if (bufferMessage[1] === 1) { // is Protobuf
|
|
18
|
-
console.log('TAK protobuf format')
|
|
19
|
-
const protoMessage = proto.proto2js(trimmedBuffer)
|
|
20
|
-
const cotMessage = proto.protojs2cotjs(protoMessage)
|
|
21
|
-
console.log(cotMessage)
|
|
22
|
-
console.log(cot.js2xml(cotMessage))
|
|
23
|
-
}
|
|
24
|
-
} else { // not TAK message format
|
|
25
|
-
console.error('Enter a TAK proto message')
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
run(process.argv[2])
|
package/scripts/parse.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
const {cot, proto} = require(path.join(__dirname, '../index.js'))
|
|
3
|
-
|
|
4
|
-
// https://github.com/deptofdefense/AndroidTacticalAssaultKit-CIV/blob/master/commoncommo/core/impl/protobuf/protocol.txt
|
|
5
|
-
|
|
6
|
-
const run = (message) => {
|
|
7
|
-
if (!message) {
|
|
8
|
-
console.error('Enter a TAK message')
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const bufferMessage = typeof message !== Buffer ? Buffer.from(message, 'hex') : message
|
|
13
|
-
|
|
14
|
-
if (bufferMessage[0] === 191) { // TAK message format 0xbf
|
|
15
|
-
console.log('TAK message received')
|
|
16
|
-
const trimmedBuffer = bufferMessage.slice(3, bufferMessage.length) // remove tak message header from content
|
|
17
|
-
if (bufferMessage[1] === 0) { // is COT XML
|
|
18
|
-
console.log('COT XML format')
|
|
19
|
-
console.log(cot.xml2js(trimmedBuffer)) // try parsing raw XML
|
|
20
|
-
} else if (bufferMessage[1] === 1) { // is Protobuf
|
|
21
|
-
console.log('TAK protobuf format')
|
|
22
|
-
console.log(proto.proto2js(trimmedBuffer))
|
|
23
|
-
}
|
|
24
|
-
} else { // not TAK message format
|
|
25
|
-
try {
|
|
26
|
-
console.log('COT XML received')
|
|
27
|
-
console.log(cot.xml2js(message)) // try parsing raw XML
|
|
28
|
-
} catch (e) {
|
|
29
|
-
console.error('Failed to parse message', e)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
run(process.argv[2])
|