Skip to content

Commit

Permalink
Merge pull request #37 from dfpc-coe/vertex-type
Browse files Browse the repository at this point in the history
AJV Type Coercion
  • Loading branch information
ingalls authored Sep 5, 2024
2 parents 6fdb6bd + b893524 commit bae4a3e
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 57 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@

## Version History

### Pending Fixed

- :rocket: Add `object` type support to COT Constructor definition
- :bug: Removal of `Type.Index` on `event` in JSONCOT type definition to fix AJV Coerce Types
- :bug: Removal surfaced `archive` inconsistencies
- :bug: Removal surfaced `__forcedelete` inconsistencies

### v11.2.0 - 2024-09-03

- :rocket: Add basic typed support for Range & Bearing Values
Expand Down
38 changes: 27 additions & 11 deletions lib/cot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Truncate from '@turf/truncate';
import Ellipse from '@turf/ellipse';
import Util from './util.js';
import Color from './color.js';
import JSONCoT from './types.js'
import JSONCoT, { Detail } from './types.js'
import type { MartiDest, MartiDestAttributes, Link, LinkAttributes } from './types.js'
import AJV from 'ajv';
import fs from 'fs';
Expand Down Expand Up @@ -59,14 +59,14 @@ export default class CoT {
// merged into the CoT spec
metadata: Record<string, unknown>;

constructor(cot: Buffer | Static<typeof JSONCoT> | string) {
constructor(cot: Buffer | Static<typeof JSONCoT> | object | string) {
if (typeof cot === 'string' || cot instanceof Buffer) {
if (cot instanceof Buffer) cot = String(cot);

const raw = xmljs.xml2js(cot, { compact: true });
this.raw = raw as Static<typeof JSONCoT>;
} else {
this.raw = cot;
this.raw = cot as Static<typeof JSONCoT>;
}

this.metadata = {};
Expand All @@ -82,7 +82,9 @@ export default class CoT {
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.archived && Object.keys(this.raw.event.detail.archived).length === 0) this.raw.event.detail.archived = { _attributes: {} };
if (this.raw.event.detail.archive && Object.keys(this.raw.event.detail.archive).length === 0) {
this.raw.event.detail.archive = { _attributes: {} };
}
}

/**
Expand Down Expand Up @@ -207,7 +209,7 @@ export default class CoT {
}

if (feature.properties.archived) {
cot.event.detail.archived = { _attributes: { } };
cot.event.detail.archive = { _attributes: { } };
}

if (feature.properties.links) {
Expand Down Expand Up @@ -438,7 +440,7 @@ export default class CoT {
if (version < 1 || version > 1) throw new Err(400, null, `Unsupported Proto Version: ${version}`);
const ProtoMessage = RootMessage.lookupType(`atakmap.commoncommo.protobuf.v${version}.TakMessage`)

const detail = this.raw.event.detail;
const detail = this.raw.event.detail || {};

const msg: any = {

Check warning on line 445 in lib/cot.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

Check warning on line 445 in lib/cot.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
cotEvent: {
Expand All @@ -453,7 +455,8 @@ export default class CoT {
}
};

for (const key in detail) {
let key: keyof Static<typeof Detail>;
for (key in detail) {
if(['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
msg.cotEvent.detail[key] = detail[key]._attributes;
delete detail[key]
Expand Down Expand Up @@ -493,7 +496,9 @@ export default class CoT {
delete detail.metadata;
}
} else if (['contact', 'group', 'precisionlocation', 'status', 'takv', 'track'].includes(key)) {
detail[key] = { _attributes: msg.cotEvent.detail[key] };
if (msg.cotEvent.detail[key]) {
detail[key] = { _attributes: msg.cotEvent.detail[key] };
}
}
}

Expand Down Expand Up @@ -601,7 +606,7 @@ export default class CoT {
if (!feat.properties.links || !feat.properties.links.length) delete feat.properties.links;
}

if (raw.event.detail.archived) {
if (raw.event.detail.archive) {
feat.properties.archived = true;
}

Expand Down Expand Up @@ -755,6 +760,12 @@ export default class CoT {
}
}
} 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),
Expand Down Expand Up @@ -787,15 +798,20 @@ export default class CoT {

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') {
} 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._attributes && raw.event.detail.shape.polyline._attributes) {
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;
Expand Down
23 changes: 11 additions & 12 deletions lib/data-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,32 +235,31 @@ export class DataPackage {
});

for (const cot of cots) {
if (!cot.raw.event.detail) {
cot.raw.event.detail = {};
}

const attaches = attachments.get(cot.uid());
if (!attaches) continue;

for (const attach of attaches) {
if (!cot.raw.event.detail.attachment_list) {
cot.raw.event.detail.attachment_list = {
_attributes: {
hashes: []
}
_attributes: { hashes: '[]' }
};
} else {
cot.raw.event.detail.attachment_list._attributes.hashes = JSON.parse(
cot.raw.event.detail.attachment_list._attributes.hashes
);
}

const hashes: string[] = JSON.parse(cot.raw.event.detail.attachment_list._attributes.hashes)

// Until told otherwise the FileHash appears to always be the directory name
const hash = await this.hash(attach._attributes.zipEntry);

if (!cot.raw.event.detail.attachment_list._attributes.hashes.includes(hash)) {
cot.raw.event.detail.attachment_list._attributes.hashes.push(hash)
if (!hashes.includes(hash)) {
hashes.push(hash)
}

cot.raw.event.detail.attachment_list._attributes.hashes = JSON.stringify(
cot.raw.event.detail.attachment_list._attributes.hashes
);
cot.raw.event.detail.attachment_list._attributes.hashes = JSON.stringify(hashes);

}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/force-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class ForceDelete extends CoT {
relation: 'none'
}
},
__forceddelete: {}
__forcedelete: {}
}
}
};
Expand Down
13 changes: 7 additions & 6 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const ShapeEllipseAttributes = Type.Object({
})

export const ShapeEllipse = Type.Object({
_attributes: Type.Optional(ShapeEllipseAttributes),
_attributes: ShapeEllipseAttributes,
})

export const Shape = Type.Object({
Expand All @@ -155,8 +155,8 @@ export const MissionLayer = Type.Object({
export const MissionChangeDetails = Type.Object({
_attributes: Type.Object({
type: Type.String(),
callsign: Type.String(),
color: Type.String()
callsign: Type.Optional(Type.String()),
color: Type.Optional(Type.String())
}),
location: Type.Optional(Type.Object({
_attributes: Type.Object({
Expand All @@ -177,7 +177,7 @@ export const MissionChange = Type.Object({
})

export const MissionChanges = Type.Object({
MissionChange: Type.Union([Type.Array(MissionChange), MissionChange])
MissionChange: Type.Union([MissionChange, Type.Array(MissionChange)])
})

export const Mission = Type.Object({
Expand Down Expand Up @@ -304,6 +304,7 @@ export const Uid = Type.Object({
export const Contact = Type.Object({
_attributes: Type.Object({
phone: Type.Optional(Type.String()),
name: Type.Optional(Type.String()),
callsign: Type.String(),
endpoint: Type.Optional(Type.String())
})
Expand Down Expand Up @@ -428,7 +429,7 @@ export const Detail = Type.Object({
precisionlocation: Type.Optional(PrecisionLocation),
color: Type.Optional(ColorAttributes),
strokeColor: Type.Optional(GenericAttributes),
archive: Type.Optional(GenericAttributes),
archive: Type.Optional(Type.Union([GenericAttributes, Type.Array(GenericAttributes)])),
strokeWeight: Type.Optional(GenericAttributes),
strokeStyle: Type.Optional(GenericAttributes),
labels_on: Type.Optional(GenericAttributes),
Expand Down Expand Up @@ -470,7 +471,7 @@ export const Point = Type.Object({
export default Type.Object({
event: Type.Object({
_attributes: EventAttributes,
detail: Type.Optional(Type.Index(Detail, Type.KeyOf(Detail))),
detail: Type.Optional(Detail),
point: Point,
}),
})
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions test/cot-fileshare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ test('Decode MultiMissionAircraft CoTs', (t) => {
ackrequest: {
"_attributes": {
"uid": "814d0d4a-3339-4fd2-8e09-0556444112f3",
"ackrequested": "true",
"ackrequested": true,
"tag": "20240615_144641.jpg"
}
},
fileshare: {
"_attributes": {
"filename": "20240615_144641.jpg.zip",
"senderUrl": "https://18.254.242.65:8443/Marti/sync/content?hash=c18e00d123057a8e33107e91ab02f999ecc6f849aed2a41b84e237ff36106a4e",
"sizeInBytes": "4233884",
"sizeInBytes": 4233884,
"sha256": "c18e00d123057a8e33107e91ab02f999ecc6f849aed2a41b84e237ff36106a4e",
"senderUid": "ANDROID-0ca41830e11d2ef3",
"senderCallsign": "DFPC Ingalls",
Expand All @@ -66,7 +66,7 @@ test('Decode MultiMissionAircraft CoTs', (t) => {
stale: '2024-07-02T17:13:29Z',
ackrequest: {
"uid": "814d0d4a-3339-4fd2-8e09-0556444112f3",
"ackrequested": "true",
"ackrequested": true,
"tag": "20240615_144641.jpg"
},
fileshare: {
Expand Down
10 changes: 5 additions & 5 deletions test/cot-mma.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ test('Decode MultiMissionAircraft CoTs', (t) => {
"shape":{
"polyline":{
"_attributes":{
"closed":"true",
"closed":true,
"fillColor":"0",
"color":"-1"
},
"vertex":[
{"_attributes":{"lat":"40.059445","lon":"-108.328368"}},
{"_attributes":{"lat":"40.061846","lon":"-108.314776"}},
{"_attributes":{"lat":"40.075499","lon":"-108.315566"}},
{"_attributes":{"lat":"40.071252","lon":"-108.330782"}}
{"_attributes":{"lat":40.059445,"lon":-108.328368}},
{"_attributes":{"lat":40.061846,"lon":-108.314776}},
{"_attributes":{"lat":40.075499,"lon":-108.315566}},
{"_attributes":{"lat":40.071252,"lon":-108.330782}}
]
}
},
Expand Down
1 change: 1 addition & 0 deletions test/cot-range-and-bearing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ test('Decode Range & Bearing', (t) => {
id: 'ebbf42a7-ea71-43a1-baf6-e259c3d115bf',
type: 'Feature',
properties: {
archived: true,
callsign: 'R&B 1',
center: [ -108.7395013, 39.0981196, 0 ],
type: 'u-rb-a',
Expand Down
10 changes: 6 additions & 4 deletions test/data-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ test(`DataPackage CoT Parsing: QuickPic.zip`, async (t) => {
Parameter: { _attributes: { name: 'uid', value: '3b758d3c-5b7a-4fba-a0dc-bbde18e895b5' } }
}, {
_attributes: { ignore: false, zipEntry: 'e63e920689815c961ec9d873c83f08a6/20240702_144514.jpg' },
Parameter: { _attributes: { name: 'uid', value: '3b758d3c-5b7a-4fba-a0dc-bbde18e895b5' } }
Parameter: { _attributes: { name: 'uid', value: '3b758d3c-5b7a-4fba-a0dc-bbde18e895b5' } }
}
]);

Expand Down Expand Up @@ -187,7 +187,7 @@ test(`DataPackage CoT Parsing: addFile,getFile`, async (t) => {

const buff = await stream2buffer(await pkg.getFile('123/package.json'));

t.equals(JSON.parse(buff.toString()).name, '@tak-ps/node-cot');
t.equals(JSON.parse(buff.toString()).name, '@tak-ps/node-cot');

const cots = await pkg.cots();

Expand Down Expand Up @@ -223,16 +223,18 @@ test(`DataPackage CoT Parsing: AttachmentInManifest.zip`, async (t) => {
Parameter: { _attributes: { name: 'uid', value: 'c7f90966-f048-41fd-8951-70cd9a380cd2' } }
}, {
_attributes: { ignore: false, zipEntry: '6988443373b26e519cfd1096665b8eaa/1000001544.jpg' },
Parameter: { _attributes: { name: 'uid', value: 'c7f90966-f048-41fd-8951-70cd9a380cd2' } }
Parameter: { _attributes: { name: 'uid', value: 'c7f90966-f048-41fd-8951-70cd9a380cd2' } }
}
]);

const cots = await pkg.cots();

t.equal(cots.length, 1);

if (!cots[0].raw.event.detail) throw new Error('Detail field not set');

t.deepEquals(
cots[0].raw.event.detail.attachment_list,
cots[0].raw.event.detail.attachment_list,
{ _attributes: { hashes: '["3adefc8c1935166d3e501844549776c6fc21cc6d72f371fdda23788e6ec7181d"]' } }
)

Expand Down
2 changes: 1 addition & 1 deletion test/force-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test('ForceDelete - Basic', (t) => {
_attributes: { lat: '0.000000', lon: '0.000000', hae: '0.0', ce: '9999999.0', le: '9999999.0' }
},
detail: {
__forceddelete: {},
__forcedelete: {},
link: {
_attributes: {
uid: 'delete-uid',
Expand Down
9 changes: 1 addition & 8 deletions test/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,7 @@ import CoT from '../index.js';

test('CoT.from_geojson - Point', (t) => {
try {
new CoT(
{
event: {
// @ts-expect-error we're trying to force an error here
_attributes: {}
}
}
);
new CoT({ event: { _attributes: {} } });
t.fail();
} catch (err) {
t.ok(String(err).includes('must have required property'));
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
},
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"module": "es2022",
"esModuleInterop": true,
"target": "es2022",
Expand Down

0 comments on commit bae4a3e

Please sign in to comment.