UNPKG

@tak-ps/node-cot

Version:

Lightweight JavaScript library for parsing and manipulating TAK messages

496 lines 23.6 kB
import Err from '@openaddresses/batch-error'; import Ellipse from '@turf/ellipse'; import Truncate from '@turf/truncate'; import { destination } from '@turf/destination'; import Color from '../utils/color.js'; import JSONCoT from '../types/types.js'; import CoT from '../cot.js'; // GeoJSON Geospatial ops will truncate to the below const COORDINATE_PRECISION = 6; const ELLIPSE_TYPE_PREFIXES = ['u-d-c-c', 'u-r-b-c-c', 'u-d-c-e']; /** * Return a GeoJSON Feature from an XML CoT message */ export async function to_geojson(cot) { const raw = JSON.parse(JSON.stringify(cot.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 (cot.creator()) { feat.properties.creator = cot.creator(); } if (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 (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.__milicon) { feat.properties.milicon = { id: raw.event.detail.__milicon._attributes.id }; } if (raw.event.detail.sensor) { feat.properties.sensor = raw.event.detail.sensor._attributes; } if (raw.event.detail.radsensordetail) { const rad = raw.event.detail.radsensordetail; feat.properties.radsensordetail = { sensor_data: { ...rad.sensor_data._attributes } }; if (rad.radmeasurement) { const measurements = Array.isArray(rad.radmeasurement) ? rad.radmeasurement : [rad.radmeasurement]; feat.properties.radsensordetail.radmeasurement = measurements.map((m) => ({ ...m._attributes })); } if (rad.physical_module) { const modules = Array.isArray(rad.physical_module) ? rad.physical_module : [rad.physical_module]; feat.properties.radsensordetail.physical_module = modules.map((m) => ({ ...m._attributes })); } if (rad.search_algorithm) { feat.properties.radsensordetail.search_algorithm = { ...rad.search_algorithm._attributes }; } if (rad.spectrum) { const spectra = Array.isArray(rad.spectrum) ? rad.spectrum : [rad.spectrum]; feat.properties.radsensordetail.spectrum = spectra.map((s) => ({ ...s._attributes })); } if (rad.isotope) { const isotopes = Array.isArray(rad.isotope) ? rad.isotope : [rad.isotope]; feat.properties.radsensordetail.isotope = isotopes.map((i) => ({ ...i._attributes })); } if (rad.data_permissions) { feat.properties.radsensordetail.data_permissions = { ...rad.data_permissions._attributes }; } if (rad.command_permissions) { feat.properties.radsensordetail.command_permissions = { ...rad.command_permissions._attributes }; } } if (raw.event.detail.chemsensordetail) { const chem = raw.event.detail.chemsensordetail; feat.properties.chemsensordetail = { sensor_data: { ...chem.sensor_data._attributes } }; if (chem.detection) { const detections = Array.isArray(chem.detection) ? chem.detection : [chem.detection]; feat.properties.chemsensordetail.detection = detections.map((d) => ({ ...d._attributes })); } } if (raw.event.detail.biosensordetail) { const bio = raw.event.detail.biosensordetail; feat.properties.biosensordetail = { sensor_data: { ...bio.sensor_data._attributes } }; if (bio.measurement) { const measurements = Array.isArray(bio.measurement) ? bio.measurement : [bio.measurement]; feat.properties.biosensordetail.measurement = measurements.map((m) => { const measurement = { ...m._attributes }; if (m.level) { const levels = Array.isArray(m.level) ? m.level : [m.level]; measurement.level = levels.map((l) => ({ ...l._attributes })); } return measurement; }); } } if (raw.event.detail.spatial) { const spatial = raw.event.detail.spatial; feat.properties.spatial = { attitude: { ...spatial.attitude._attributes }, spin: { ...spatial.spin._attributes } }; if (spatial._attributes?.version !== undefined) { feat.properties.spatial.version = spatial._attributes.version; } } 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.display?._attributes?.minzoom !== undefined) { feat.properties.minzoom = raw.event.detail.display._attributes.minzoom; } if (raw.event.detail.display?._attributes?.rotate !== undefined) { feat.properties.rotate = raw.event.detail.display._attributes.rotate; } if (raw.event.detail.display?._attributes?.maxzoom !== undefined) { feat.properties.maxzoom = raw.event.detail.display._attributes.maxzoom; } if (raw.event.detail.labels_on?._attributes?.value !== undefined) { feat.properties.labels = raw.event.detail.labels_on._attributes.value; } if (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]; // These CoT Types use "point" links as geometry points if (['u-d-f', 'u-d-r', 'b-m-r', 'u-rb-a'].includes(raw.event._attributes.type)) { feat.properties.links = raw.event.detail.link .filter((link) => { return !link._attributes.point; }).map((link) => { return link._attributes; }); } else { feat.properties.links = raw.event.detail.link .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 }; } else if (raw.event.detail.__chatreceipt) { feat.properties.chat = { ...raw.event.detail.__chatreceipt._attributes, chatgrp: raw.event.detail.__chatreceipt.chatgrp }; } if (raw.event.detail.track?._attributes) { const track = raw.event.detail.track._attributes; const course = Number(track.course); if (track.course !== undefined && track.course !== '' && !isNaN(course)) feat.properties.course = course; const slope = Number(track.slope); if (track.slope !== undefined && track.slope !== '' && !isNaN(slope)) feat.properties.slope = slope; const speed = Number(track.speed); if (track.speed !== undefined && track.speed !== '' && !isNaN(speed)) feat.properties.speed = speed; } if (raw.event.detail.marti) { if (raw.event.detail.marti._attributes?.archive) { feat.properties.marti_archive = true; } if (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?._attributes?.iconsetpath && !['b-m-p-s-m'].includes(raw.event._attributes.type)) { feat.properties.icon = raw.event.detail.usericon._attributes.iconsetpath; } if (raw.event.detail.uid?._attributes?.Droid) { feat.properties.droid = raw.event.detail.uid._attributes.Droid; } if (raw.event.detail.takv?._attributes) { feat.properties.takv = raw.event.detail.takv._attributes; } if (raw.event.detail.__group?._attributes) { feat.properties.group = raw.event.detail.__group._attributes; } if (raw.event.detail['_flow-tags_']?._attributes) { feat.properties.flow = raw.event.detail['_flow-tags_']._attributes; } if (raw.event.detail.status?._attributes) { feat.properties.status = raw.event.detail.status._attributes; } if (raw.event.detail.mission?._attributes) { const mission = { ...raw.event.detail.mission._attributes }; if (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) { const mc = { contentUid: change.MissionChange.contentUid ? change.MissionChange.contentUid._text : undefined, 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, }; if (change.MissionChange.contentResource) { const cr = change.MissionChange.contentResource; mc.contentResource = { uid: cr.uid._text, expiration: cr.expiration._text, hash: cr.hash._text, name: cr.name._text, size: parseInt(cr.size._text), submissionTime: cr.submissionTime._text, submitter: cr.submitter._text, filename: cr.filename ? cr.filename._text : undefined, tool: cr.tool ? cr.tool._text : undefined, }; } if (change.MissionChange.details) { mc.details = { ...change.MissionChange.details._attributes, ...change.MissionChange.details.location ? change.MissionChange.details.location._attributes : {} }; } mission.missionChanges.push(mc); } } if (raw.event.detail.mission.missionLayer) { const missionLayer = {}; if (raw.event.detail.mission.missionLayer.name?._text) { missionLayer.name = raw.event.detail.mission.missionLayer.name._text; } if (raw.event.detail.mission.missionLayer.parentUid?._text) { missionLayer.parentUid = raw.event.detail.mission.missionLayer.parentUid._text; } if (raw.event.detail.mission.missionLayer.type?._text) { missionLayer.type = raw.event.detail.mission.missionLayer.type._text; } if (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?._attributes) { feat.properties.precisionlocation = raw.event.detail.precisionlocation._attributes; } if (raw.event.detail.strokeColor?._attributes?.value !== undefined) { 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?._attributes?.value) { feat.properties['stroke-width'] = Number(raw.event.detail.strokeWeight._attributes.value); } if (raw.event.detail.strokeStyle?._attributes?.value) { feat.properties['stroke-style'] = raw.event.detail.strokeStyle._attributes.value; } if (raw.event.detail.color) { let color = null; if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length > 1) { color = raw.event.detail.color[0]; if (!color._attributes) color._attributes = {}; for (let i = raw.event.detail.color.length - 1; i >= 1; i--) { if (raw.event.detail.color[i]._attributes) { Object.assign(color._attributes, raw.event.detail.color[i]._attributes); } } } else if (Array.isArray(raw.event.detail.color) && raw.event.detail.color.length === 1) { color = raw.event.detail.color[0]; } else if (!Array.isArray(raw.event.detail.color)) { color = raw.event.detail.color; } if (color?._attributes?.argb) { const parsedColor = new Color(Number(color._attributes.argb)); feat.properties['marker-color'] = parsedColor.as_hex(); feat.properties['marker-opacity'] = parsedColor.as_opacity() / 255; } } // Line, 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()); } // Range & Bearing Line if (raw.event._attributes.type === 'u-rb-a') { const detail = cot.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(cot.position(), detail.range._attributes.value / 1000, detail.bearing._attributes.value).geometry.coordinates; feat.geometry = { type: 'LineString', coordinates: [cot.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?._attributes?.value !== undefined) { 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 (ELLIPSE_TYPE_PREFIXES.some((prefix) => raw.event._attributes.type.startsWith(prefix))) { if (!raw.event.detail.shape) throw new Err(400, null, `${raw.event._attributes.type} (Circle) must define shape value`); if (!raw.event.detail.shape.ellipse || !raw.event.detail.shape.ellipse._attributes) throw new Err(400, null, `${raw.event._attributes.type} (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) }; if (raw.event.detail.shape.ellipse._attributes.swapAxis !== undefined) { ellipse.swapAxis = Boolean(raw.event.detail.shape.ellipse._attributes.swapAxis); } if (!Array.isArray(raw.event.detail.shape.link) && raw.event.detail.shape.link?._attributes.type === 'b-x-KmlStyle' && raw.event.detail.shape.link?.Style) { if (raw.event.detail.shape.link.Style.LineStyle?.color) { let rawColor = raw.event.detail.shape.link.Style.LineStyle.color._text; if (rawColor.startsWith('#')) rawColor = rawColor.substring(1); const a = parseInt(rawColor.substring(0, 2), 16); const r = parseInt(rawColor.substring(2, 4), 16); const g = parseInt(rawColor.substring(4, 6), 16); const b = parseInt(rawColor.substring(6, 8), 16); const strokeColor = new Color([a, r, g, b]); feat.properties.stroke = strokeColor.as_hex(); feat.properties['stroke-opacity'] = strokeColor.as_opacity() / 255; } if (raw.event.detail.shape.link.Style.LineStyle?.width) { feat.properties['stroke-width'] = Number(raw.event.detail.shape.link.Style.LineStyle.width._text); } if (raw.event.detail.shape.link.Style.PolyStyle?.color) { let rawColor = raw.event.detail.shape.link.Style.PolyStyle.color._text; if (rawColor.startsWith('#')) rawColor = rawColor.substring(1); const a = parseInt(rawColor.substring(0, 2), 16); const r = parseInt(rawColor.substring(2, 4), 16); const g = parseInt(rawColor.substring(4, 6), 16); const b = parseInt(rawColor.substring(6, 8), 16); const fillColor = new Color([a, r, g, b]); feat.properties['fill-opacity'] = fillColor.as_opacity() / 255; feat.properties['fill'] = fillColor.as_hex(); } } feat.geometry = Truncate(Ellipse(feat.geometry.coordinates, Number(ellipse.major) / 1000, Number(ellipse.minor) / 1000, { angle: 90 - ellipse.angle }), { precision: COORDINATE_PRECISION, coordinates: 3, 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?.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?.closed === true) { coordinates.push(coordinates[0]); feat.geometry = { type: 'Polygon', coordinates: [coordinates] }; } else { feat.geometry = { type: 'LineString', coordinates }; } } if (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; } } } else if (raw.event._attributes.type === 'b-m-p-s-m') { if (raw.event.detail.usericon?._attributes?.iconsetpath && raw.event.detail.usericon._attributes.iconsetpath.startsWith('COT_MAPPING_SPOTMAP/b-m-p-s-m/')) { const spot = new Color(Number(raw.event.detail.usericon._attributes.iconsetpath.split('/')[2])); feat.properties['marker-color'] = spot.as_hex(); feat.properties['marker-opacity'] = spot.as_opacity() / 255; delete feat.properties.icon; } } feat.properties.metadata = cot.metadata; feat.path = cot.path; return feat; } //# sourceMappingURL=to_geojson.js.map