@tak-ps/node-cot
Version:
Lightweight JavaScript library for parsing and manipulating TAK messages
1,117 lines • 47.4 kB
JavaScript
import crypto from 'node:crypto';
import protobuf from 'protobufjs';
import Err from '@openaddresses/batch-error';
import { diff } from 'json-diff-ts';
import xmljs from 'xml-js';
import { InputFeature, } from './types/feature.js';
import Sensor from './sensor.js';
import PointOnFeature from '@turf/point-on-feature';
import Truncate from '@turf/truncate';
import { destination } from '@turf/destination';
import Ellipse from '@turf/ellipse';
import Util from './utils/util.js';
import Color from './utils/color.js';
import JSONCoT, { Detail } from './types/types.js';
import AJV from 'ajv';
import fs from 'fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// GeoJSON Geospatial ops will truncate to the below
const COORDINATE_PRECISION = 6;
const protoPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'proto', 'takmessage.proto');
const RootMessage = await protobuf.load(protoPath);
const pkg = JSON.parse(String(fs.readFileSync(new URL('../package.json', import.meta.url))));
const checkXML = (new AJV({
allErrors: true,
coerceTypes: true,
allowUnionTypes: true
}))
.compile(JSONCoT);
const checkFeat = (new AJV({
allErrors: true,
coerceTypes: true,
allowUnionTypes: true
}))
.compile(InputFeature);
/**
* Convert to and from an XML CoT message
* @class
*
* @param cot A string/buffer containing the XML representation or the xml-js object tree
*
* @prop raw Raw XML-JS representation of CoT
*/
export default class CoT {
raw;
// Key/Value JSON Records - not currently support by TPC Clients
// but used for styling/dynamic overrides and hopefully eventually
// merged into the CoT spec
metadata;
// Does the CoT belong to a folder - defaults to "/"
path;
constructor(cot, opts = {}) {
if (typeof cot === 'string' || cot instanceof Buffer) {
const raw = xmljs.xml2js(String(cot), { compact: true });
this.raw = raw;
}
else {
this.raw = cot;
}
this.metadata = {};
this.path = '/';
if (!this.raw.event._attributes.uid)
this.raw.event._attributes.uid = Util.cot_uuid();
if (process.env.DEBUG_COTS)
console.log(JSON.stringify(this.raw));
checkXML(this.raw);
if (checkXML.errors)
throw new Err(400, null, `${checkXML.errors[0].message} (${checkXML.errors[0].instancePath})`);
if (!this.raw.event.detail)
this.raw.event.detail = {};
if (!this.raw.event.detail['_flow-tags_'])
this.raw.event.detail['_flow-tags_'] = {};
this.raw.event.detail['_flow-tags_'][`NodeCoT-${pkg.version}`] = new Date().toISOString();
if (this.raw.event.detail.archive && Object.keys(this.raw.event.detail.archive).length === 0) {
this.raw.event.detail.archive = { _attributes: {} };
}
if (opts.creator) {
this.creator({
uid: opts.creator instanceof CoT ? opts.creator.uid() : opts.creator.uid,
type: opts.creator instanceof CoT ? opts.creator.type() : opts.creator.type,
callsign: opts.creator instanceof CoT ? opts.creator.callsign() : opts.creator.callsign,
time: opts.creator instanceof CoT ? new Date() : opts.creator.time
});
}
}
/**
* Detect difference between CoT messages
* Note: This diffs based on GeoJSON Representation of message
* So if unknown properties are present they will be excluded from the diff
*/
isDiff(cot, opts = {
diffMetadata: false,
diffStaleStartTime: false,
diffDest: false,
diffFlow: false
}) {
const a = this.to_geojson();
const b = cot.to_geojson();
if (!opts.diffDest) {
delete a.properties.dest;
delete b.properties.dest;
}
if (!opts.diffMetadata) {
delete a.properties.metadata;
delete b.properties.metadata;
}
if (!opts.diffFlow) {
delete a.properties.flow;
delete b.properties.flow;
}
if (!opts.diffStaleStartTime) {
delete a.properties.time;
delete a.properties.stale;
delete a.properties.start;
delete b.properties.time;
delete b.properties.stale;
delete b.properties.start;
}
const diffs = diff(a, b);
return diffs.length > 0;
}
/**
* Returns or sets the UID of the CoT
*/
uid(uid) {
if (uid)
this.raw.event._attributes.uid = uid;
return this.raw.event._attributes.uid;
}
/**
* Returns or sets the Callsign of the CoT
*/
type(type) {
if (type) {
this.raw.event._attributes.type = type;
}
return this.raw.event._attributes.type;
}
/**
* Returns or sets the Archived State of the CoT
*
* @param callsign - Optional Archive state to set
*/
archived(archived) {
const detail = this.detail();
if (archived === true) {
detail.archive = { _attributes: {} };
}
else if (archived === false) {
delete detail.archive;
}
return detail.archive !== undefined;
}
/**
* Returns or sets the Callsign of the CoT
*
* @param callsign - Optional Callsign to set
*/
callsign(callsign) {
const detail = this.detail();
if (callsign && !detail.contact) {
detail.contact = { _attributes: { callsign } };
}
else if (callsign && detail.contact) {
detail.contact._attributes.callsign = callsign;
}
if (detail.contact && detail.contact._attributes && typeof detail.contact._attributes.callsign === 'string') {
return detail.contact._attributes.callsign;
}
else {
return 'UNKNOWN';
}
}
/**
* Return Detail Object of CoT or create one if it doesn't yet exist and pass a reference
*/
detail() {
if (!this.raw.event.detail)
this.raw.event.detail = {};
return this.raw.event.detail;
}
/**
* Add a given Dest tag to a CoT
*/
addDest(dest) {
const detail = this.detail();
if (!detail.marti)
detail.marti = {};
let destArr = [];
if (detail.marti.dest && !Array.isArray(detail.marti.dest)) {
destArr = [detail.marti.dest];
}
else if (detail.marti.dest && Array.isArray(detail.marti.dest)) {
destArr = detail.marti.dest;
}
destArr.push({ _attributes: dest });
detail.marti.dest = destArr;
return this;
}
addVideo(video, connection) {
const detail = this.detail();
if (detail.__video)
throw new Err(400, null, 'A video stream already exists on this CoT');
if (!video.url)
throw new Err(400, null, 'A Video URL must be provided');
if (!video.uid && connection && connection.uid) {
video.uid = connection.uid;
}
else if (video.uid && connection && !connection.uid) {
connection.uid = video.uid;
}
else if (!video.uid) {
video.uid = crypto.randomUUID();
}
detail.__video = {
_attributes: video
};
if (connection) {
detail.__video.ConnectionEntry = {
_attributes: connection
};
}
else {
detail.__video.ConnectionEntry = {
_attributes: {
uid: video.uid,
networkTimeout: 12000,
path: '',
protocol: 'raw',
bufferTime: -1,
address: video.url,
port: -1,
roverPort: -1,
rtspReliable: 0,
ignoreEmbeddedKLV: false,
alias: this.callsign()
}
};
}
return this;
}
position(position) {
if (position) {
this.raw.event.point._attributes.lon = String(position[0]);
this.raw.event.point._attributes.lat = String(position[1]);
}
return [
Number(this.raw.event.point._attributes.lon),
Number(this.raw.event.point._attributes.lat)
];
}
sensor(sensor) {
const detail = this.detail();
if (sensor) {
detail.sensor = {
_attributes: sensor
};
}
if (!detail.sensor || !detail.sensor._attributes) {
return null;
}
return new Sensor(this.position(), detail.sensor._attributes).to_geojson();
}
;
creator(creator) {
const detail = this.detail();
if (creator) {
this.addLink({
uid: creator.uid,
production_time: creator.time ? new Date(creator.time).toISOString() : new Date().toISOString(),
type: creator.type,
parent_callsign: creator.callsign,
relation: 'p-p'
});
detail.creator = {
_attributes: {
...creator,
time: creator.time ? new Date(creator.time).toISOString() : new Date().toISOString(),
}
};
}
if (detail.creator) {
return detail.creator._attributes;
}
else {
return;
}
}
addLink(link) {
const detail = this.detail();
let linkArr = [];
if (detail.link && !Array.isArray(detail.link)) {
linkArr = [detail.link];
}
else if (detail.link && Array.isArray(detail.link)) {
linkArr = detail.link;
}
linkArr.push({ _attributes: link });
detail.link = linkArr;
return this;
}
/**
* Return an ATAK Compliant Protobuf
*/
to_proto(version = 1) {
if (version < 1 || version > 1)
throw new Err(400, null, `Unsupported Proto Version: ${version}`);
const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`);
// The spread operator is important to make sure the delete doesn't modify the underlying detail object
const detail = { ...this.raw.event.detail };
const msg = {
cotEvent: {
...this.raw.event._attributes,
sendTime: new Date(this.raw.event._attributes.time).getTime(),
startTime: new Date(this.raw.event._attributes.start).getTime(),
staleTime: new Date(this.raw.event._attributes.stale).getTime(),
...this.raw.event.point._attributes,
detail: {
xmlDetail: ''
}
}
};
let key;
for (key in detail) {
if (['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
msg.cotEvent.detail[key] = detail[key]._attributes;
delete detail[key];
}
}
msg.cotEvent.detail.xmlDetail = xmljs.js2xml({
...detail,
metadata: this.metadata
}, { compact: true });
return ProtoMessage.encode(msg).finish();
}
/**
* Return a GeoJSON Feature from an XML CoT message
*/
to_geojson() {
const raw = JSON.parse(JSON.stringify(this.raw));
if (!raw.event.detail)
raw.event.detail = {};
if (!raw.event.detail.contact)
raw.event.detail.contact = { _attributes: { callsign: 'UNKNOWN' } };
if (!raw.event.detail.contact._attributes)
raw.event.detail.contact._attributes = { callsign: 'UNKNOWN' };
const feat = {
id: raw.event._attributes.uid,
type: 'Feature',
properties: {
callsign: raw.event.detail.contact._attributes.callsign || 'UNKNOWN',
center: [Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae)],
type: raw.event._attributes.type,
how: raw.event._attributes.how || '',
time: raw.event._attributes.time,
start: raw.event._attributes.start,
stale: raw.event._attributes.stale,
},
geometry: {
type: 'Point',
coordinates: [Number(raw.event.point._attributes.lon), Number(raw.event.point._attributes.lat), Number(raw.event.point._attributes.hae)]
}
};
const contact = JSON.parse(JSON.stringify(raw.event.detail.contact._attributes));
delete contact.callsign;
if (Object.keys(contact).length) {
feat.properties.contact = contact;
}
if (this.creator()) {
feat.properties.creator = this.creator();
}
if (raw.event.detail.remarks && raw.event.detail.remarks._text) {
feat.properties.remarks = raw.event.detail.remarks._text;
}
if (raw.event.detail.fileshare) {
feat.properties.fileshare = raw.event.detail.fileshare._attributes;
if (feat.properties.fileshare && typeof feat.properties.fileshare.sizeInBytes === 'string') {
feat.properties.fileshare.sizeInBytes = parseInt(feat.properties.fileshare.sizeInBytes);
}
}
if (raw.event.detail.__milsym) {
feat.properties.milsym = {
id: raw.event.detail.__milsym._attributes.id
};
}
if (raw.event.detail.sensor) {
feat.properties.sensor = raw.event.detail.sensor._attributes;
}
if (raw.event.detail.range) {
feat.properties.range = raw.event.detail.range._attributes.value;
}
if (raw.event.detail.bearing) {
feat.properties.bearing = raw.event.detail.bearing._attributes.value;
}
if (raw.event.detail.__video && raw.event.detail.__video._attributes) {
feat.properties.video = raw.event.detail.__video._attributes;
if (raw.event.detail.__video.ConnectionEntry) {
feat.properties.video.connection = raw.event.detail.__video.ConnectionEntry._attributes;
}
}
if (raw.event.detail.__geofence) {
feat.properties.geofence = raw.event.detail.__geofence._attributes;
}
if (raw.event.detail.ackrequest) {
feat.properties.ackrequest = raw.event.detail.ackrequest._attributes;
}
if (raw.event.detail.attachment_list) {
feat.properties.attachments = JSON.parse(raw.event.detail.attachment_list._attributes.hashes);
}
if (raw.event.detail.link) {
if (!Array.isArray(raw.event.detail.link))
raw.event.detail.link = [raw.event.detail.link];
feat.properties.links = raw.event.detail.link.filter((link) => {
return !!link._attributes.url;
}).map((link) => {
return link._attributes;
});
if (!feat.properties.links || !feat.properties.links.length)
delete feat.properties.links;
}
if (raw.event.detail.archive) {
feat.properties.archived = true;
}
if (raw.event.detail.__chat) {
feat.properties.chat = {
...raw.event.detail.__chat._attributes,
chatgrp: raw.event.detail.__chat.chatgrp
};
}
if (raw.event.detail.track && raw.event.detail.track._attributes) {
if (raw.event.detail.track._attributes.course)
feat.properties.course = Number(raw.event.detail.track._attributes.course);
if (raw.event.detail.track._attributes.slope)
feat.properties.slope = Number(raw.event.detail.track._attributes.slope);
if (raw.event.detail.track._attributes.course)
feat.properties.speed = Number(raw.event.detail.track._attributes.speed);
}
if (raw.event.detail.marti && raw.event.detail.marti.dest) {
if (!Array.isArray(raw.event.detail.marti.dest))
raw.event.detail.marti.dest = [raw.event.detail.marti.dest];
const dest = raw.event.detail.marti.dest.map((d) => {
return { ...d._attributes };
});
feat.properties.dest = dest.length === 1 ? dest[0] : dest;
}
if (raw.event.detail.usericon && raw.event.detail.usericon._attributes && raw.event.detail.usericon._attributes.iconsetpath) {
feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath;
}
if (raw.event.detail.uid && raw.event.detail.uid._attributes && raw.event.detail.uid._attributes.Droid) {
feat.properties.droid = raw.event.detail.uid._attributes.Droid;
}
if (raw.event.detail.takv && raw.event.detail.takv._attributes) {
feat.properties.takv = raw.event.detail.takv._attributes;
}
if (raw.event.detail.__group && raw.event.detail.__group._attributes) {
feat.properties.group = raw.event.detail.__group._attributes;
}
if (raw.event.detail['_flow-tags_'] && raw.event.detail['_flow-tags_']._attributes) {
feat.properties.flow = raw.event.detail['_flow-tags_']._attributes;
}
if (raw.event.detail.status && raw.event.detail.status._attributes) {
feat.properties.status = raw.event.detail.status._attributes;
}
if (raw.event.detail.mission && raw.event.detail.mission._attributes) {
const mission = {
...raw.event.detail.mission._attributes
};
if (raw.event.detail.mission && raw.event.detail.mission.MissionChanges) {
const changes = Array.isArray(raw.event.detail.mission.MissionChanges)
? raw.event.detail.mission.MissionChanges
: [raw.event.detail.mission.MissionChanges];
mission.missionChanges = [];
for (const change of changes) {
mission.missionChanges.push({
contentUid: change.MissionChange.contentUid._text,
creatorUid: change.MissionChange.creatorUid._text,
isFederatedChange: change.MissionChange.isFederatedChange._text,
missionName: change.MissionChange.missionName._text,
timestamp: change.MissionChange.timestamp._text,
type: change.MissionChange.type._text,
details: {
...change.MissionChange.details._attributes,
...change.MissionChange.details.location
? change.MissionChange.details.location._attributes
: {}
}
});
}
}
if (raw.event.detail.mission && raw.event.detail.mission.missionLayer) {
const missionLayer = {};
if (raw.event.detail.mission.missionLayer.name && raw.event.detail.mission.missionLayer.name._text) {
missionLayer.name = raw.event.detail.mission.missionLayer.name._text;
}
if (raw.event.detail.mission.missionLayer.parentUid && raw.event.detail.mission.missionLayer.parentUid._text) {
missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text;
}
if (raw.event.detail.mission.missionLayer.type && raw.event.detail.mission.missionLayer.type._text) {
missionLayer.type = raw.event.detail.mission.missionLayer.type._text;
}
if (raw.event.detail.mission.missionLayer.uid && raw.event.detail.mission.missionLayer.uid._text) {
missionLayer.uid = raw.event.detail.mission.missionLayer.uid._text;
}
mission.missionLayer = missionLayer;
}
feat.properties.mission = mission;
}
if (raw.event.detail.precisionlocation && raw.event.detail.precisionlocation._attributes) {
feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes;
}
// Line or Polygon style types
if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type) && Array.isArray(raw.event.detail.link)) {
const coordinates = [];
for (const l of raw.event.detail.link) {
if (!l._attributes.point)
continue;
coordinates.push(l._attributes.point.split(',').map((p) => { return Number(p.trim()); }).splice(0, 2).reverse());
}
if (raw.event.detail.strokeColor && raw.event.detail.strokeColor._attributes && raw.event.detail.strokeColor._attributes.value) {
const stroke = new Color(Number(raw.event.detail.strokeColor._attributes.value));
feat.properties.stroke = stroke.as_hex();
feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
}
if (raw.event.detail.strokeWeight && raw.event.detail.strokeWeight._attributes && raw.event.detail.strokeWeight._attributes.value) {
feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value);
}
if (raw.event.detail.strokeStyle && raw.event.detail.strokeStyle._attributes && raw.event.detail.strokeStyle._attributes.value) {
feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value;
}
// Range & Bearing Line
if (raw.event._attributes.type === 'u-rb-a') {
const detail = this.detail();
if (!detail.range)
throw new Error('Range value not provided');
if (!detail.bearing)
throw new Error('Bearing value not provided');
// TODO Support inclination
const dest = destination(this.position(), detail.range._attributes.value / 1000, detail.bearing._attributes.value).geometry.coordinates;
feat.geometry = {
type: 'LineString',
coordinates: [this.position(), dest]
};
}
else if (raw.event._attributes.type === 'u-d-r' || (coordinates[0][0] === coordinates[coordinates.length - 1][0] && coordinates[0][1] === coordinates[coordinates.length - 1][1])) {
if (raw.event._attributes.type === 'u-d-r') {
// CoT rectangles are only 4 points - GeoJSON needs to be closed
coordinates.push(coordinates[0]);
}
feat.geometry = {
type: 'Polygon',
coordinates: [coordinates]
};
if (raw.event.detail.fillColor && raw.event.detail.fillColor._attributes && raw.event.detail.fillColor._attributes.value) {
const fill = new Color(Number(raw.event.detail.fillColor._attributes.value));
feat.properties['fill-opacity'] = fill.as_opacity() / 255;
feat.properties['fill'] = fill.as_hex();
}
}
else {
feat.geometry = {
type: 'LineString',
coordinates
};
}
}
else if (raw.event._attributes.type.startsWith('u-d-c-c')) {
if (!raw.event.detail.shape)
throw new Err(400, null, 'u-d-c-c (Circle) must define shape value');
if (!raw.event.detail.shape.ellipse
|| !raw.event.detail.shape.ellipse._attributes)
throw new Err(400, null, 'u-d-c-c (Circle) must define ellipse shape value');
const ellipse = {
major: Number(raw.event.detail.shape.ellipse._attributes.major),
minor: Number(raw.event.detail.shape.ellipse._attributes.minor),
angle: Number(raw.event.detail.shape.ellipse._attributes.angle)
};
feat.geometry = Truncate(Ellipse(feat.geometry.coordinates, Number(ellipse.major) / 1000, Number(ellipse.minor) / 1000, {
angle: ellipse.angle
}), {
precision: COORDINATE_PRECISION,
mutate: true
}).geometry;
feat.properties.shape = {};
feat.properties.shape.ellipse = ellipse;
}
else if (raw.event._attributes.type.startsWith('b-m-p-s-p-i')) {
// TODO: Currently the "shape" tag is only parsed here - asking ARA for clarification if it is a general use tag
if (raw.event.detail.shape && raw.event.detail.shape.polyline && raw.event.detail.shape.polyline.vertex) {
const coordinates = [];
const vertices = Array.isArray(raw.event.detail.shape.polyline.vertex) ? raw.event.detail.shape.polyline.vertex : [raw.event.detail.shape.polyline.vertex];
for (const v of vertices) {
coordinates.push([Number(v._attributes.lon), Number(v._attributes.lat)]);
}
if (coordinates.length === 1) {
feat.geometry = { type: 'Point', coordinates: coordinates[0] };
}
else if (raw.event.detail.shape.polyline._attributes && raw.event.detail.shape.polyline._attributes.closed === true) {
coordinates.push(coordinates[0]);
feat.geometry = { type: 'Polygon', coordinates: [coordinates] };
}
else {
feat.geometry = { type: 'LineString', coordinates };
}
}
if (raw.event.detail.shape
&& raw.event.detail.shape.polyline
&& raw.event.detail.shape.polyline._attributes
&& raw.event.detail.shape.polyline._attributes) {
if (raw.event.detail.shape.polyline._attributes.fillColor) {
const fill = new Color(Number(raw.event.detail.shape.polyline._attributes.fillColor));
feat.properties['fill-opacity'] = fill.as_opacity() / 255;
feat.properties['fill'] = fill.as_hex();
}
if (raw.event.detail.shape.polyline._attributes.color) {
const stroke = new Color(Number(raw.event.detail.shape.polyline._attributes.color));
feat.properties.stroke = stroke.as_hex();
feat.properties['stroke-opacity'] = stroke.as_opacity() / 255;
}
}
}
if (raw.event.detail.color && raw.event.detail.color._attributes && raw.event.detail.color._attributes.argb) {
const color = new Color(Number(raw.event.detail.color._attributes.argb));
feat.properties['marker-color'] = color.as_hex();
feat.properties['marker-opacity'] = color.as_opacity() / 255;
}
feat.properties.metadata = this.metadata;
feat.path = this.path;
return feat;
}
to_xml() {
return xmljs.js2xml(this.raw, { compact: true });
}
is_stale() {
return new Date(this.raw.event._attributes.stale) < new Date();
}
/**
* Determines if the CoT message represents a Tasking Message
*
* @return {boolean}
*/
is_tasking() {
return !!this.raw.event._attributes.type.match(/^t-/);
}
/**
* Determines if the CoT message represents a Chat Message
*
* @return {boolean}
*/
is_chat() {
return !!(this.raw.event.detail && this.raw.event.detail.__chat);
}
/**
* Determines if the CoT message represents a Friendly Element
*
* @return {boolean}
*/
is_friend() {
return !!this.raw.event._attributes.type.match(/^a-f-/);
}
/**
* Determines if the CoT message represents a Hostile Element
*
* @return {boolean}
*/
is_hostile() {
return !!this.raw.event._attributes.type.match(/^a-h-/);
}
/**
* Determines if the CoT message represents a Unknown Element
*
* @return {boolean}
*/
is_unknown() {
return !!this.raw.event._attributes.type.match(/^a-u-/);
}
/**
* Determines if the CoT message represents a Pending Element
*
* @return {boolean}
*/
is_pending() {
return !!this.raw.event._attributes.type.match(/^a-p-/);
}
/**
* Determines if the CoT message represents an Assumed Element
*
* @return {boolean}
*/
is_assumed() {
return !!this.raw.event._attributes.type.match(/^a-a-/);
}
/**
* Determines if the CoT message represents a Neutral Element
*
* @return {boolean}
*/
is_neutral() {
return !!this.raw.event._attributes.type.match(/^a-n-/);
}
/**
* Determines if the CoT message represents a Suspect Element
*
* @return {boolean}
*/
is_suspect() {
return !!this.raw.event._attributes.type.match(/^a-s-/);
}
/**
* Determines if the CoT message represents a Joker Element
*
* @return {boolean}
*/
is_joker() {
return !!this.raw.event._attributes.type.match(/^a-j-/);
}
/**
* Determines if the CoT message represents a Faker Element
*
* @return {boolean}
*/
is_faker() {
return !!this.raw.event._attributes.type.match(/^a-k-/);
}
/**
* Determines if the CoT message represents an Element
*
* @return {boolean}
*/
is_atom() {
return !!this.raw.event._attributes.type.match(/^a-/);
}
/**
* Determines if the CoT message represents an Airborne Element
*
* @return {boolean}
*/
is_airborne() {
return !!this.raw.event._attributes.type.match(/^a-.-A/);
}
/**
* Determines if the CoT message represents a Ground Element
*
* @return {boolean}
*/
is_ground() {
return !!this.raw.event._attributes.type.match(/^a-.-G/);
}
/**
* Determines if the CoT message represents an Installation
*
* @return {boolean}
*/
is_installation() {
return !!this.raw.event._attributes.type.match(/^a-.-G-I/);
}
/**
* Determines if the CoT message represents a Vehicle
*
* @return {boolean}
*/
is_vehicle() {
return !!this.raw.event._attributes.type.match(/^a-.-G-E-V/);
}
/**
* Determines if the CoT message represents Equipment
*
* @return {boolean}
*/
is_equipment() {
return !!this.raw.event._attributes.type.match(/^a-.-G-E/);
}
/**
* Determines if the CoT message represents a Surface Element
*
* @return {boolean}
*/
is_surface() {
return !!this.raw.event._attributes.type.match(/^a-.-S/);
}
/**
* Determines if the CoT message represents a Subsurface Element
*
* @return {boolean}
*/
is_subsurface() {
return !!this.raw.event._attributes.type.match(/^a-.-U/);
}
/**
* Determines if the CoT message represents a UAV Element
*
* @return {boolean}
*/
is_uav() {
return !!this.raw.event._attributes.type.match(/^a-f-A-M-F-Q-r/);
}
/**
* Parse an ATAK compliant Protobuf to a JS Object
*/
static from_proto(raw, version = 1) {
const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`);
// TODO Type this
const msg = ProtoMessage.decode(raw);
if (!msg.cotEvent)
throw new Err(400, null, 'No cotEvent Data');
const detail = {};
const metadata = {};
for (const key in msg.cotEvent.detail) {
if (key === 'xmlDetail') {
const parsed = xmljs.xml2js(`<detail>${msg.cotEvent.detail.xmlDetail}</detail>`, { compact: true });
Object.assign(detail, parsed.detail);
if (detail.metadata) {
for (const key in detail.metadata) {
metadata[key] = detail.metadata[key]._text;
}
delete detail.metadata;
}
}
else if (key === 'group') {
if (msg.cotEvent.detail[key]) {
detail.__group = { _attributes: msg.cotEvent.detail[key] };
}
}
else if (['contact', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
if (msg.cotEvent.detail[key]) {
detail[key] = { _attributes: msg.cotEvent.detail[key] };
}
}
}
const cot = new CoT({
event: {
_attributes: {
version: '2.0',
uid: msg.cotEvent.uid, type: msg.cotEvent.type, how: msg.cotEvent.how,
qos: msg.cotEvent.qos, opex: msg.cotEvent.opex, access: msg.cotEvent.access,
time: new Date(msg.cotEvent.sendTime.toNumber()).toISOString(),
start: new Date(msg.cotEvent.startTime.toNumber()).toISOString(),
stale: new Date(msg.cotEvent.staleTime.toNumber()).toISOString(),
},
detail,
point: {
_attributes: {
lat: msg.cotEvent.lat,
lon: msg.cotEvent.lon,
hae: msg.cotEvent.hae,
le: msg.cotEvent.le,
ce: msg.cotEvent.ce,
},
}
}
});
cot.metadata = metadata;
return cot;
}
/**
* Return a CoT Message
*/
static ping() {
return new CoT({
event: {
_attributes: Util.cot_event_attr('t-x-c-t', 'h-g-i-g-o'),
detail: {},
point: Util.cot_point()
}
});
}
/**
* Return an CoT Message given a GeoJSON Feature
*
* @param {Object} feature GeoJSON Point Feature
*
* @return {CoT}
*/
static from_geojson(feature) {
checkFeat(feature);
if (checkFeat.errors)
throw new Err(400, null, `${checkFeat.errors[0].message} (${checkFeat.errors[0].instancePath})`);
const cot = {
event: {
_attributes: Util.cot_event_attr(feature.properties.type || 'a-f-G', feature.properties.how || 'm-g', feature.properties.time, feature.properties.start, feature.properties.stale),
point: Util.cot_point(),
detail: Util.cot_event_detail(feature.properties.callsign)
}
};
if (feature.id)
cot.event._attributes.uid = String(feature.id);
if (feature.properties.callsign && !feature.id)
cot.event._attributes.uid = feature.properties.callsign;
if (!cot.event.detail)
cot.event.detail = {};
if (feature.properties.droid) {
cot.event.detail.uid = { _attributes: { Droid: feature.properties.droid } };
}
if (feature.properties.archived) {
cot.event.detail.archive = { _attributes: {} };
}
if (feature.properties.links) {
if (!cot.event.detail.link)
cot.event.detail.link = [];
else if (!Array.isArray(cot.event.detail.link))
cot.event.detail.link = [cot.event.detail.link];
cot.event.detail.link.push(...feature.properties.links.map((link) => {
return { _attributes: link };
}));
}
if (feature.properties.dest) {
const dest = !Array.isArray(feature.properties.dest) ? [feature.properties.dest] : feature.properties.dest;
cot.event.detail.marti = {
dest: dest.map((dest) => {
return { _attributes: { ...dest } };
})
};
}
if (feature.properties.takv) {
cot.event.detail.takv = { _attributes: { ...feature.properties.takv } };
}
if (feature.properties.creator) {
cot.event.detail.creator = { _attributes: { ...feature.properties.creator } };
}
if (feature.properties.range !== undefined) {
cot.event.detail.range = { _attributes: { value: feature.properties.range } };
}
if (feature.properties.bearing !== undefined) {
cot.event.detail.bearing = { _attributes: { value: feature.properties.bearing } };
}
if (feature.properties.geofence) {
cot.event.detail.__geofence = { _attributes: { ...feature.properties.geofence } };
}
if (feature.properties.milsym) {
cot.event.detail.__milsym = { _attributes: { id: feature.properties.milsym.id } };
}
if (feature.properties.sensor) {
cot.event.detail.sensor = { _attributes: { ...feature.properties.sensor } };
}
if (feature.properties.ackrequest) {
cot.event.detail.ackrequest = { _attributes: { ...feature.properties.ackrequest } };
}
if (feature.properties.video) {
if (feature.properties.video.connection) {
const video = JSON.parse(JSON.stringify(feature.properties.video));
const connection = video.connection;
delete video.connection;
cot.event.detail.__video = {
_attributes: { ...video },
ConnectionEntry: {
_attributes: connection
}
};
}
else {
cot.event.detail.__video = { _attributes: { ...feature.properties.video } };
}
}
if (feature.properties.attachments) {
cot.event.detail.attachment_list = { _attributes: { hashes: JSON.stringify(feature.properties.attachments) } };
}
if (feature.properties.contact) {
cot.event.detail.contact = {
_attributes: {
...feature.properties.contact,
callsign: feature.properties.callsign || 'UNKNOWN',
}
};
}
if (feature.properties.fileshare) {
cot.event.detail.fileshare = { _attributes: { ...feature.properties.fileshare } };
}
if (feature.properties.course !== undefined || feature.properties.speed !== undefined || feature.properties.slope !== undefined) {
cot.event.detail.track = {
_attributes: Util.cot_track_attr(feature.properties.course, feature.properties.speed, feature.properties.slope)
};
}
if (feature.properties.group) {
cot.event.detail.__group = { _attributes: { ...feature.properties.group } };
}
if (feature.properties.flow) {
cot.event.detail['_flow-tags_'] = { _attributes: { ...feature.properties.flow } };
}
if (feature.properties.status) {
cot.event.detail.status = { _attributes: { ...feature.properties.status } };
}
if (feature.properties.precisionlocation) {
cot.event.detail.precisionlocation = { _attributes: { ...feature.properties.precisionlocation } };
}
if (feature.properties.icon) {
cot.event.detail.usericon = { _attributes: { iconsetpath: feature.properties.icon } };
}
if (feature.properties.mission) {
cot.event.detail.mission = {
_attributes: {
type: feature.properties.mission.type,
guid: feature.properties.mission.guid,
tool: feature.properties.mission.tool,
name: feature.properties.mission.name,
authorUid: feature.properties.mission.authorUid,
}
};
if (feature.properties.mission.missionLayer) {
cot.event.detail.mission.missionLayer = {};
if (feature.properties.mission.missionLayer.name) {
cot.event.detail.mission.missionLayer.name = { _text: feature.properties.mission.missionLayer.name };
}
if (feature.properties.mission.missionLayer.parentUid) {
cot.event.detail.mission.missionLayer.parentUid = { _text: feature.properties.mission.missionLayer.parentUid };
}
if (feature.properties.mission.missionLayer.type) {
cot.event.detail.mission.missionLayer.type = { _text: feature.properties.mission.missionLayer.type };
}
if (feature.properties.mission.missionLayer.uid) {
cot.event.detail.mission.missionLayer.uid = { _text: feature.properties.mission.missionLayer.uid };
}
}
}
cot.event.detail.remarks = { _attributes: {}, _text: feature.properties.remarks || '' };
if (!feature.geometry) {
throw new Err(400, null, 'Must have Geometry');
}
else if (!['Point', 'Polygon', 'LineString'].includes(feature.geometry.type)) {
throw new Err(400, null, 'Unsupported Geometry Type');
}
if (feature.geometry.type === 'Point') {
cot.event.point._attributes.lon = String(feature.geometry.coordinates[0]);
cot.event.point._attributes.lat = String(feature.geometry.coordinates[1]);
cot.event.point._attributes.hae = String(feature.geometry.coordinates[2] || '0.0');
if (feature.properties['marker-color']) {
const color = new Color(feature.properties['marker-color'] || -1761607936);
color.a = feature.properties['marker-opacity'] !== undefined ? feature.properties['marker-opacity'] * 255 : 128;
cot.event.detail.color = { _attributes: { argb: String(color.as_32bit()) } };
}
}
else if (feature.geometry.type === 'Polygon' && feature.properties.type === 'u-d-c-c') {
if (!feature.properties.shape || !feature.properties.shape.ellipse) {
throw new Err(400, null, 'u-d-c-c (Circle) must define a feature.properties.shape.ellipse property');
}
cot.event.detail.shape = { ellipse: { _attributes: feature.properties.shape.ellipse } };
if (feature.properties.center) {
cot.event.point._attributes.lon = String(feature.properties.center[0]);
cot.event.point._attributes.lat = String(feature.properties.center[1]);
}
else {
const centre = PointOnFeature(feature);
cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
cot.event.point._attributes.hae = '0.0';
}
}
else if (['Polygon', 'LineString'].includes(feature.geometry.type)) {
const stroke = new Color(feature.properties.stroke || -1761607936);
stroke.a = feature.properties['stroke-opacity'] !== undefined ? feature.properties['stroke-opacity'] * 255 : 128;
cot.event.detail.strokeColor = { _attributes: { value: String(stroke.as_32bit()) } };
if (!feature.properties['stroke-width'])
feature.properties['stroke-width'] = 3;
cot.event.detail.strokeWeight = { _attributes: {
value: String(feature.properties['stroke-width'])
} };
if (!feature.properties['stroke-style'])
feature.properties['stroke-style'] = 'solid';
cot.event.detail.strokeStyle = { _attributes: {
value: feature.properties['stroke-style']
} };
if (feature.geometry.type === 'LineString') {
cot.event._attributes.type = 'u-d-f';
if (!cot.event.detail.link)
cot.event.detail.link = [];
else if (!Array.isArray(cot.event.detail.link))
cot.event.detail.link = [cot.event.detail.link];
for (const coord of feature.geometry.coordinates) {
cot.event.detail.link.push({
_attributes: { point: `${coord[1]},${coord[0]}` }
});
}
}
else if (feature.geometry.type === 'Polygon') {
cot.event._attributes.type = 'u-d-f';
if (!cot.event.detail.link)
cot.event.detail.link = [];
else if (!Array.isArray(cot.event.detail.link))
cot.event.detail.link = [cot.event.detail.link];
// Inner rings are not yet supported
for (const coord of feature.geometry.coordinates[0]) {
cot.event.detail.link.push({
_attributes: { point: `${coord[1]},${coord[0]}` }
});
}
const fill = new Color(feature.properties.fill || -1761607936);
fill.a = feature.properties['fill-opacity'] !== undefined ? feature.properties['fill-opacity'] * 255 : 128;
cot.event.detail.fillColor = { _attributes: { value: String(fill.as_32bit()) } };
}
cot.event.detail.labels_on = { _attributes: { value: 'false' } };
cot.event.detail.tog = { _attributes: { enabled: '0' } };
if (feature.properties.center && Array.isArray(feature.properties.center) && feature.properties.center.length >= 2) {
cot.event.point._attributes.lon = String(feature.properties.center[0]);
cot.event.point._attributes.lat = String(feature.properties.center[1]);
if (feature.properties.center.length >= 3) {
cot.event.point._attributes.hae = String(feature.properties.center[2] || '0.0');
}
else {
cot.event.point._attributes.hae = '0.0';
}
}
else {
const centre = PointOnFeature(feature);
cot.event.point._attributes.lon = String(centre.geometry.coordinates[0]);
cot.event.point._attributes.lat = String(centre.geometry.coordinates[1]);
cot.event.point._attributes.hae = '0.0';
}
}
const newcot = new CoT(cot);
if (feature.properties.metadata) {
newcot.metadata = feature.properties.metadata;
}
if (feature.path) {
newcot.path = feature.path;
}
return newcot;
}
}
//# sourceMappingURL=cot.js.map