@tak-ps/node-tak
Version:
Lightweight JavaScript library for communicating with TAK Server
320 lines (273 loc) • 9.99 kB
text/typescript
import { TAKList, TAKItem } from './types.js';
import { Type, Static } from '@sinclair/typebox';
import type { MissionOptions } from './mission.js';
import { GUIDMatch } from './mission.js';
import Err from '@openaddresses/batch-error';
import Commands, { CommandOutputFormat } from '../commands.js';
import type { Feature } from '@tak-ps/node-cot';
export enum MissionLayerType {
GROUP = 'GROUP',
UID = 'UID',
CONTENTS = 'CONTENTS',
MAPLAYER = 'MAPLAYER',
ITEM = 'ITEM'
}
export const MissionLayer = Type.Object({
name: Type.String({ minLength: 1 }),
type: Type.Enum(MissionLayerType),
parentUid: Type.Optional(Type.String()),
uid: Type.String(),
mission_layers: Type.Optional(Type.Array(Type.Any())),
uids: Type.Optional(Type.Array(Type.Object({
data: Type.String({
description: 'The UID of the COT'
}),
timestamp: Type.String(),
creatorUid: Type.String(),
keywords: Type.Optional(Type.Array(Type.String())),
details: Type.Optional(Type.Object({
type: Type.String(),
callsign: Type.String(),
color: Type.Optional(Type.String()),
location: Type.Object({
lat: Type.Number(),
lon: Type.Number()
})
}))
}))),
contents: Type.Optional(Type.Array(Type.Any())),
maplayers: Type.Optional(Type.Array(Type.Any()))
});
export const DeleteInput = Type.Object({
uid: Type.Array(Type.String()),
creatorUid: Type.String(),
});
export const RenameInput = Type.Object({
name: Type.String(),
creatorUid: Type.String(),
});
export const CreateInput = Type.Object({
name: Type.String(),
type: Type.Enum(MissionLayerType),
uid: Type.Optional(Type.String()),
parentUid: Type.Optional(Type.String()),
afterUid: Type.Optional(Type.String()),
creatorUid: Type.String()
})
export const TAKList_MissionLayer = TAKList(MissionLayer);
export const TAKItem_MissionLayer = TAKItem(MissionLayer);
export default class MissionLayerCommands extends Commands {
schema = {
list: {
description: 'List Injectors',
params: Type.Object({}),
query: Type.Object({}),
formats: [ CommandOutputFormat.JSON ]
}
}
async cli(): Promise<object | string> {
throw new Error('Unsupported Subcommand');
}
#encodeName(name: string): string {
return encodeURIComponent(name.trim())
}
#isGUID(id: string): boolean {
return GUIDMatch.test(id)
}
#headers(opts?: Static<typeof MissionOptions>): object {
if (opts && opts.token) {
return {
MissionAuthorization: `Bearer ${opts.token}`
}
} else {
return {};
}
}
isEmpty(layer: Static<typeof MissionLayer>): boolean {
if (layer.mission_layers && layer.mission_layers.length) return false;
if (layer.uids && layer.uids.length) return false;
if (layer.contents && layer.contents.length) return false;
if (layer.maplayers && layer.maplayers.length) return false;
return true;
}
async listAsPathMap(
name: string,
opts?: Static<typeof MissionOptions>
): Promise<Map<string, Static<typeof MissionLayer>>> {
const layers = (await this.list(name, opts)).data;
const pathMap: Map<string, Static<typeof MissionLayer>> = new Map();
for (const layer of layers) {
this.#listAsPathMapRecurse(pathMap, '', layer);
}
return pathMap;
}
#listAsPathMapRecurse(
pathMap: Map<string, Static<typeof MissionLayer>>,
pathCurrent: string,
layer: Static<typeof MissionLayer>
) {
pathCurrent = `${pathCurrent}/${encodeURIComponent(layer.name)}`;
pathMap.set(`${pathCurrent}/`, layer);
for (const l of (layer.mission_layers || []) as Array<Static<typeof MissionLayer>>) {
this.#listAsPathMapRecurse(pathMap, pathCurrent, l);
}
}
async list(
name: string,
opts?: Static<typeof MissionOptions>
): Promise<Static<typeof TAKList_MissionLayer>> {
let res;
if (this.#isGUID(name)) {
const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
res = await this.api.fetch(url, {
method: 'GET',
headers: this.#headers(opts),
});
} else {
const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
res = await this.api.fetch(url, {
method: 'GET',
headers: this.#headers(opts),
});
}
res.data.map((l: Static<typeof MissionLayer>) => {
if (l.type === MissionLayerType.UID && !l.uids) {
l.uids = [];
}
return l;
})
return res;
}
/**
* Stopgap function until the main latestFeats function can accept a path
* parameter
*/
async latestFeats(
name: string,
layerUid: string, // Layer UID
opts?: Static<typeof MissionOptions>
): Promise<Static<typeof Feature.Feature>[]> {
const layer = (await this.get(name, layerUid, opts)).data;
const feats = await this.api.Mission.latestFeats(name, opts);
const layerUids = new Set((layer.uids || []).map((u) => {
return u.data
}));
const filtered = feats.filter((f) => {
return layerUids.has(f.id)
});
return filtered;
}
async get(
name: string,
layerUid: string, // Layer UID
opts?: Static<typeof MissionOptions>
): Promise<Static<typeof TAKItem_MissionLayer>> {
const layers = await this.list(name, opts);
// TODO this will only return top level layers - need to recurse to lower level layers
for (const layer of layers.data) {
if (layer.uid === layerUid) {
return {
version: layers.version,
type: layers.type,
data: layer,
nodeId: layers.nodeId
}
}
}
throw new Err(404, null, `Layer ${layerUid} not found - only top level layers will be returned`);
}
async create(
name: string,
query: Static<typeof CreateInput>,
opts?: Static<typeof MissionOptions>
): Promise<Static<typeof TAKItem_MissionLayer>> {
if (this.#isGUID(name)) {
const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
let q: keyof Static<typeof CreateInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'PUT',
headers: this.#headers(opts),
});
} else {
const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
let q: keyof Static<typeof CreateInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'PUT',
headers: this.#headers(opts),
});
}
}
async rename(
name: string,
layer: string,
query: Static<typeof RenameInput>,
opts?: Static<typeof MissionOptions>
) {
if (this.#isGUID(name)) {
const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers/${layer}/name`, this.api.url);
let q: keyof Static<typeof RenameInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'PUT',
headers: this.#headers(opts),
});
} else {
const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers/${layer}/name`, this.api.url);
let q: keyof Static<typeof RenameInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'PUT',
headers: this.#headers(opts),
});
}
}
async delete(
name: string,
query: Static<typeof DeleteInput>,
opts?: Static<typeof MissionOptions>
) {
if (this.#isGUID(name)) {
const url = new URL(`/Marti/api/missions/guid/${this.#encodeName(name)}/layers`, this.api.url);
let q: keyof Static<typeof DeleteInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'DELETE',
headers: this.#headers(opts),
});
} else {
const url = new URL(`/Marti/api/missions/${this.#encodeName(name)}/layers`, this.api.url);
let q: keyof Static<typeof DeleteInput>;
for (q in query) {
if (query[q] !== undefined) {
url.searchParams.append(q, String(query[q]));
}
}
return await this.api.fetch(url, {
method: 'DELETE',
headers: this.#headers(opts),
});
}
}
}