Skip to content

Commit

Permalink
Load Breadcrumb
Browse files Browse the repository at this point in the history
  • Loading branch information
ingalls committed Dec 5, 2024
1 parent bccd654 commit 57d6c42
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 5 deletions.
4 changes: 2 additions & 2 deletions api/lib/api/mission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ export default class {

const res: any = xmljs.xml2js(await this.latestCots(name, opts), { compact: true });

if (!Object.keys(res.events).length) return [];
if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return [];
if (!Object.keys(res.events).length) return feats;
if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return feats;

for (const event of Array.isArray(res.events.event) ? res.events.event : [res.events.event] ) {
feats.push((new CoT({ event })).to_geojson());
Expand Down
56 changes: 56 additions & 0 deletions api/lib/api/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import TAKAPI from '../tak-api.js';
import xmljs from 'xml-js';
import { CoT } from '@tak-ps/node-tak';
import FormData from 'form-data';
import { Readable } from 'node:stream';
import mime from 'mime';
import { Type, Static } from '@sinclair/typebox';
import type { Feature } from '@tak-ps/node-cot';

export const HistoryOptions = Type.Object({
start: Type.Optional(Type.String()),
end: Type.Optional(Type.String()),
secago: Type.Optional(Type.String()),
})

export default class COTQuery {
api: TAKAPI;

constructor(api: TAKAPI) {
this.api = api;
}

async historyFeats(uid: string, opts?: Static<typeof HistoryOptions>): Promise<Static<typeof Feature.Feature>> {
const feats: Static<typeof Feature.Feature>[] = [];

const res: any = xmljs.xml2js(await this.history(uid, opts), { compact: true });

if (!Object.keys(res.events).length) return feats;
if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return feats;

for (const event of Array.isArray(res.events.event) ? res.events.event : [res.events.event] ) {
feats.push((new CoT({ event })).to_geojson());
}

return feats;
}

async history(uid: string, opts?: Static<typeof HistoryOptions>): Promise<string> {
const url = new URL(`/Marti/api/cot/xml/${encodeURIComponent(uid)}/all`, this.api.url);

let q: keyof Static<typeof HistoryOptions>;
for (q in opts) {
if (opts[q] !== undefined) {
url.searchParams.append(q, String(opts[q]));
}
}

const res = await this.api.fetch(url, {
method: 'GET'
}, true);

const body = await res.text();

return body;
}
}
3 changes: 3 additions & 0 deletions api/lib/tak-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FormData from 'form-data';
import OAuth from './api/oauth.js';
import Package from './api/package.js';
import Query from './api/query.js';
import Mission from './api/mission.js';
import MissionLog from './api/mission-log.js';
import MissionLayer from './api/mission-layer.js';
Expand Down Expand Up @@ -33,12 +34,14 @@ export default class TAKAPI {
Group: Group;
Video: Video;
Export: Export;
Query: Query;
Files: Files;

constructor(url: URL, auth: auth.APIAuth) {
this.url = url;
this.auth = auth;

this.Query = new Query(this);
this.Package = new Package(this);
this.OAuth = new OAuth(this);
this.Export = new Export(this);
Expand Down
67 changes: 67 additions & 0 deletions api/routes/marti-export.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Type } from '@sinclair/typebox'
import Schema from '@openaddresses/batch-schema';
import Err from '@openaddresses/batch-error';
import { Feature } from '@tak-ps/node-cot'
import { HistoryOptions } from '../lib/api/query.js';
import Auth from '../lib/auth.js';
import Config from '../lib/config.js';
import { ExportInput } from '../lib/api/export.js';
Expand Down Expand Up @@ -35,4 +37,69 @@ export default async function router(schema: Schema, config: Config) {
Err.respond(err, res);
}
});

await schema.get('/marti/cot/:uid/all', {
name: 'COT History',
group: 'MartiCOTQuery',
description: 'Helper API to list COT Queries',
params: Type.Object({
uid: Type.String()
}),
query: Type.Composite([
Type.Object({
track: Type.Boolean({
description: 'By default each historic point will be its own feature, if true this will attempt to join all points into a single Feature Collection at the cost of temporal attributes',
default: true
})
}),
Type.HistoryOptions
]),
res: Type.Object({
type: Type.String(),
features: Type.Array(Feature.Feature)
})
}, async (req, res) => {
try {
const user = await Auth.as_user(config, req);
const profile = await config.models.Profile.from(user.email);
const api = await TAKAPI.init(new URL(String(config.server.api)), new APIAuthCertificate(profile.auth.cert, profile.auth.key));

const features = await api.Query.historyFeats(req.params.uid, {
start: req.query.start,
end: req.query.end,
secago: req.query.secago,
});

const fc = { type: 'FeatureCollection', features: [] };

if (req.query.track) {
let composite: Static<typeof Feature.Feature>;

for (const feat of features) {
if (feat.geometry.type !== 'Point') {
fc.features.push(feat);
} if (!composite) {
composite = feat;
composite.id = `${composite.id}-track`;
composite.geometry = {
type: 'LineString',
coordinates: [ composite.geometry.coordinates ]
}
} else {
if (feat.geometry.coordinates[0] !== 0 && feat.geometry.coordinates[1]) {
composite.geometry.coordinates.push(feat.geometry.coordinates);
}
}
}

fc.features.push(composite);
} else {
fc.features = features;
}

res.json(fc);
} catch (err) {
Err.respond(err, res);
}
});
}
3 changes: 2 additions & 1 deletion api/routes/marti-mission.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Static, Type } from '@sinclair/typebox'
import Schema from '@openaddresses/batch-schema';
import { Feature } from '@tak-ps/node-cot'
import Err from '@openaddresses/batch-error';
import Auth from '../lib/auth.js';
import Config from '../lib/config.js';
Expand Down Expand Up @@ -72,7 +73,7 @@ export default async function router(schema: Schema, config: Config) {
description: 'Helper API to get latest CoTs',
res: Type.Object({
type: Type.String(),
features: Type.Array(Type.Any())
features: Type.Array(Feature.Feature)
})
}, async (req, res) => {
try {
Expand Down
43 changes: 42 additions & 1 deletion api/web/src/components/CloudTAK/CoTView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
</TablerIconButton>
</div>
<div class='ms-auto btn-list mx-2'>
<TablerIconButton
v-if='!isArchivable && !loadingBreadcrumb'
title='Load Breadcrumb'
@click='loadBreadcrumb'
>
<IconRoute
size='32'
stroke='1'
/>
</TablerIconButton>
<TablerDelete
displaytype='icon'
@delete='deleteCOT'
Expand Down Expand Up @@ -264,6 +274,12 @@
style='height: calc(100vh - 225px)'
>
<div class='row g-0'>
<div
v-if='loadingBreadcrumb'
class='col-12'
>
<TablerLoading :compact='true' desc='Loading Breadcrumb...'/>
</div>
<div
v-if='mission'
class='col-12'
Expand Down Expand Up @@ -618,6 +634,7 @@ import {
TablerEnum,
TablerRange,
TablerDropdown,
TablerLoading,
TablerIconButton,
} from '@tak-ps/vue-tabler';
Expand All @@ -634,6 +651,7 @@ import Elevation from './util/Elevation.vue';
import Attachments from './util/Attachments.vue';
import {
IconMovie,
IconRoute,
IconCone,
IconStar,
IconStarFilled,
Expand All @@ -650,7 +668,7 @@ import {
} from '@tabler/icons-vue';
import Subscriptions from './util/Subscriptions.vue';
import timediff from '../../../src/timediff.ts';
import { std } from '../../../src/std.ts';
import { std, stdurl } from '../../../src/std.ts';
import { useCOTStore } from '../../../src/stores/cots.ts';
const cotStore = useCOTStore();
import { useProfileStore } from '../../../src/stores/profile.ts';
Expand All @@ -666,6 +684,7 @@ const cot = ref<COT | undefined>(cotStore.get(String(route.params.uid), {
}))
const mission = ref<Mission | undefined>();
const loadingBreadcrumb = ref(false);
if (cot.value && cot.value.origin.mode === OriginMode.MISSION && cot.value.origin.mode_id) {
mission.value = cotStore.subscriptions.get(cot.value.origin.mode_id);
Expand Down Expand Up @@ -743,6 +762,28 @@ function timediffFormat(date: string) {
}
}
async function loadBreadcrumb() {
if (!cot.value) return;
loadingBreadcrumb.value = true;
try {
const url = stdurl(`/api/marti/cot/${cot.value.id}/all`)
url.searchParams.append('secago', String(60 * 60))
url.searchParams.append('track', String(true))
const crumb = await std(url);
for (const feat of crumb.features) {
cotStore.add(feat)
}
loadingBreadcrumb.value = false;
} catch (err) {
loadingBreadcrumb.value = false;
throw err;
}
}
async function fetchType() {
if (!cot.value) return;
type.value = await std(`/api/type/cot/${cot.value.properties.type}`) as COTType
Expand Down
2 changes: 1 addition & 1 deletion api/web/src/components/CloudTAK/Menu/Channels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

<TablerLoading v-if='loading' />
<TablerAlert
v-if='error'
v-else-if='error'
:err='error'
/>
<TablerNone
Expand Down

0 comments on commit 57d6c42

Please sign in to comment.