@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/CHANGELOG.md +8 -0
- package/dist/lib/cot.js +368 -292
- package/dist/lib/cot.js.map +1 -1
- package/dist/lib/types/feature.js +13 -3
- package/dist/lib/types/feature.js.map +1 -1
- package/dist/lib/types/types.js +4 -2
- package/dist/lib/types/types.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/test/cot-video.test.js +65 -1
- package/dist/test/cot-video.test.js.map +1 -1
- package/dist/test/reversal-geojson.test.js.map +1 -1
- package/lib/cot.ts +766 -674
- package/lib/types/feature.ts +13 -2
- package/lib/types/types.ts +4 -2
- package/package.json +1 -1
- package/test/cot-video.test.ts +71 -1
- package/test/fixtures/video-connection.geojson +26 -0
- package/test/reversal-geojson.test.ts +1 -0
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>):
|
|
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>):
|
|
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
|
|
180
|
-
*
|
|
181
|
-
* @param {Object} feature GeoJSON Point Feature
|
|
182
|
-
*
|
|
183
|
-
* @return {CoT}
|
|
252
|
+
* Return an ATAK Compliant Protobuf
|
|
184
253
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
281
|
+
msg.cotEvent.detail.xmlDetail = xmljs.js2xml({
|
|
282
|
+
...detail,
|
|
283
|
+
metadata: this.metadata
|
|
284
|
+
}, { compact: true });
|
|
218
285
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}))
|
|
222
|
-
}
|
|
286
|
+
return ProtoMessage.encode(msg).finish();
|
|
287
|
+
}
|
|
223
288
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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 (
|
|
239
|
-
|
|
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 (
|
|
243
|
-
|
|
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 (
|
|
247
|
-
|
|
333
|
+
if (raw.event.detail.sensor) {
|
|
334
|
+
feat.properties.sensor = raw.event.detail.sensor._attributes;
|
|
248
335
|
}
|
|
249
336
|
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
}
|
|
337
|
+
if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
|
|
338
|
+
feat.properties.video = raw.event.detail.__video._attributes;
|
|
253
339
|
|
|
254
|
-
|
|
255
|
-
|
|
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 (
|
|
259
|
-
|
|
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 (
|
|
268
|
-
|
|
349
|
+
if (raw.event.detail.ackrequest) {
|
|
350
|
+
feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
|
|
269
351
|
}
|
|
270
352
|
|
|
271
|
-
if (
|
|
272
|
-
|
|
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 (
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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 (
|
|
286
|
-
|
|
369
|
+
if (raw.event.detail.archive) {
|
|
370
|
+
feat.properties.archived = true;
|
|
287
371
|
}
|
|
288
372
|
|
|
289
|
-
if (
|
|
290
|
-
|
|
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 (
|
|
294
|
-
|
|
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 (
|
|
298
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
393
|
+
feat.properties.dest = dest.length === 1 ? dest[0] : dest
|
|
394
|
+
}
|
|
314
395
|
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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 (
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
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 (
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
382
|
-
|
|
452
|
+
if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
|
|
453
|
+
const missionLayer: Static<typeof FeaturePropertyMissionLayer> = {};
|
|
383
454
|
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
506
|
+
feat.geometry = {
|
|
507
|
+
type: 'Polygon',
|
|
508
|
+
coordinates: [coordinates]
|
|
509
|
+
}
|
|
473
510
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
848
|
+
* Return a CoT Message
|
|
539
849
|
*/
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 (
|
|
582
|
-
|
|
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 (
|
|
586
|
-
|
|
889
|
+
if (feature.properties.droid) {
|
|
890
|
+
cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
|
|
587
891
|
}
|
|
588
892
|
|
|
589
|
-
if (
|
|
590
|
-
|
|
893
|
+
if (feature.properties.archived) {
|
|
894
|
+
cot.event.detail.archive = { _attributes: { } };
|
|
591
895
|
}
|
|
592
896
|
|
|
593
|
-
if (
|
|
594
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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 (
|
|
602
|
-
|
|
906
|
+
if (feature.properties.dest) {
|
|
907
|
+
const dest = !Array.isArray(feature.properties.dest) ? [ feature.properties.dest ] : feature.properties.dest;
|
|
603
908
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
909
|
+
cot.event.detail.marti = {
|
|
910
|
+
dest: dest.map((dest) => {
|
|
911
|
+
return { _attributes: { ...dest } };
|
|
912
|
+
})
|
|
913
|
+
}
|
|
914
|
+
}
|
|
609
915
|
|
|
610
|
-
|
|
916
|
+
if (feature.properties.takv) {
|
|
917
|
+
cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
|
|
611
918
|
}
|
|
612
919
|
|
|
613
|
-
if (
|
|
614
|
-
|
|
920
|
+
if (feature.properties.geofence) {
|
|
921
|
+
cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
|
|
615
922
|
}
|
|
616
923
|
|
|
617
|
-
if (
|
|
618
|
-
|
|
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 (
|
|
625
|
-
|
|
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 (
|
|
631
|
-
if (
|
|
932
|
+
if (feature.properties.video) {
|
|
933
|
+
if (feature.properties.video.connection) {
|
|
934
|
+
const video = JSON.parse(JSON.stringify(feature.properties.video));
|
|
632
935
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
});
|
|
936
|
+
const connection = video.connection;
|
|
937
|
+
delete video.connection;
|
|
636
938
|
|
|
637
|
-
|
|
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 (
|
|
641
|
-
|
|
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 (
|
|
646
|
-
|
|
963
|
+
if (feature.properties.fileshare) {
|
|
964
|
+
cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
|
|
647
965
|
}
|
|
648
966
|
|
|
649
|
-
if (
|
|
650
|
-
|
|
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 (
|
|
654
|
-
|
|
973
|
+
if (feature.properties.group) {
|
|
974
|
+
cot.event.detail.__group = { _attributes: { ...feature.properties.group } }
|
|
655
975
|
}
|
|
656
976
|
|
|
657
|
-
if (
|
|
658
|
-
|
|
977
|
+
if (feature.properties.flow) {
|
|
978
|
+
cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } }
|
|
659
979
|
}
|
|
660
980
|
|
|
661
|
-
if (
|
|
662
|
-
|
|
981
|
+
if (feature.properties.status) {
|
|
982
|
+
cot.event.detail.status = { _attributes: { ...feature.properties.status } }
|
|
663
983
|
}
|
|
664
984
|
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
};
|
|
985
|
+
if (feature.properties.precisionlocation) {
|
|
986
|
+
cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } }
|
|
987
|
+
}
|
|
669
988
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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
|
-
|
|
706
|
-
|
|
1010
|
+
|
|
1011
|
+
if (feature.properties.mission.missionLayer.parentUid) {
|
|
1012
|
+
cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
|
|
707
1013
|
}
|
|
708
|
-
|
|
709
|
-
|
|
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
|
|
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
|
-
|
|
719
|
-
|
|
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 (
|
|
723
|
-
|
|
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 (
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
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 (
|
|
737
|
-
|
|
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 (
|
|
741
|
-
|
|
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 (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
756
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
799
|
-
for (const
|
|
800
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
814
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|