Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@patternfly/react-core": "6.1.0",
"@patternfly/react-icons": "6.1.0",
"@patternfly/react-styles": "6.3.1",
"qrcode-generator": "1.4.4",
"react": "18.3.1",
"react-dom": "18.3.1"
}
Expand Down
3 changes: 3 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
parseEnvDefault,
serializeEnvDefault,
} from './envDefaultFile';
import { IconsetCard } from './iconsetCard';
import { ServiceManagementCard, type ToastMessage } from './serviceCard';
import { TlsUploadCard } from './tlsCard';

Expand Down Expand Up @@ -311,6 +312,8 @@ export const Application: React.FC = () => {

<TlsUploadCard onToast={setToast} onInstalledPaths={onTlsInstalled} />

<IconsetCard />

<Card
className="aiscot-expandable-card"
isExpanded={isConfigExpanded}
Expand Down
32 changes: 32 additions & 0 deletions src/conf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,38 @@ export const CONF_PARAMS: Record<string, EnvVarDefinition> = {
required: false,
},

VESSEL_NAME_PREFIX: {
type: 'boolean',
description: 'Prepend conventional ship-type callsign prefixes: T/B (tug), P/V (pilot), M/V (cargo), M/T (tanker), F/V, S/V, SAR, A/P, L/E',
defaultValue: 'true',
validation: /^(true|false|yes|no|1|0)$/i,
required: false,
},

SHIPCLASS_COLORS: {
type: 'boolean',
description: 'Color vessel markers by AIS-catcher ship class: tankers red, cargo green, passenger blue, special brown, HSC yellow, fishing pink',
defaultValue: 'true',
validation: /^(true|false|yes|no|1|0)$/i,
required: false,
},

SHIPCLASS_ICONS: {
type: 'boolean',
description: 'Use ship-class marker icons from the bundled ais-ships-iconset.zip (dart underway / circle stopped). Import the iconset into TAK clients first — see the ATAK Vessel Iconset card',
defaultValue: 'false',
validation: /^(true|false|yes|no|1|0)$/i,
required: false,
},

UNDERWAY_ONLY: {
type: 'boolean',
description: 'Only forward vessels that are underway; drop parked (anchored/moored/aground) hulls so anchorage clutter does not drown the traffic picture',
defaultValue: 'false',
validation: /^(true|false|yes|no|1|0)$/i,
required: false,
},

MID_DB_FILE: {
type: 'path',
description: 'Path to the MID database file, used for decoding AIS messages',
Expand Down
42 changes: 42 additions & 0 deletions src/iconset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Sensors & Signals LLC https://www.snstac.com/
*/

import { describe, expect, it } from 'vitest';

import {
ICONSET_IMPORT_URL,
ICONSET_QR_SVG,
ICONSET_URL,
ICONSET_VERSION,
qrSvg,
} from './iconset';

describe('iconset URLs', () => {
it('downloads from the aiscot repo at a pinned release tag', () => {
expect(ICONSET_URL).toMatch(/^https:\/\/github\.com\/snstac\/aiscot\//);
expect(ICONSET_URL).toContain(`/raw/${ICONSET_VERSION}/`);
expect(ICONSET_VERSION).toMatch(/^v\d+\.\d+\.\d+$/);
expect(ICONSET_URL).toMatch(/ais-ships-iconset\.zip$/);
});

it('wraps the download URL in an ATAK import intent', () => {
expect(ICONSET_IMPORT_URL).toBe(
`tak://com.atakmap.app/import?url=${encodeURIComponent(ICONSET_URL)}`
);
});
});

describe('qrSvg', () => {
it('renders the import URL as an SVG QR code', () => {
const svg = qrSvg(ICONSET_IMPORT_URL);
expect(svg).toBeTruthy();
expect(svg).toContain('<svg');
expect(svg).toContain('</svg>');
});

it('precomputes the import QR at module load', () => {
expect(ICONSET_QR_SVG).toBeTruthy();
expect(ICONSET_QR_SVG).toContain('<svg');
});
});
41 changes: 41 additions & 0 deletions src/iconset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Sensors & Signals LLC https://www.snstac.com/
*
* ATAK vessel iconset download locations and QR generation. Kept free of
* cockpit/React imports so it stays unit-testable under vitest (node env).
*/

import qrcode from 'qrcode-generator';

/**
* AIS-catcher style ship-class ATAK iconset bundled with AISCOT >= 7.3.0.
* Pinned to the release tag that first shipped it — the installed aiscot
* emits iconsetpath values matching this exact zip, so the download must
* not track a moving branch. The same zip also ships on-box in the aiscot
* package under aiscot/data/ for air-gapped units.
*/
export const ICONSET_VERSION = 'v7.3.0';
export const ICONSET_URL =
`https://github.com/snstac/aiscot/raw/${ICONSET_VERSION}/src/aiscot/data/ais-ships-iconset.zip`;

/**
* qrtak-style ATAK import URL: scanning this with ATAK's QR scanner (or the
* device camera) downloads and imports the iconset directly.
*/
export const ICONSET_IMPORT_URL =
`tak://com.atakmap.app/import?url=${encodeURIComponent(ICONSET_URL)}`;

/** Render a payload as a QR code SVG string, or null if generation fails. */
export function qrSvg(payload: string): string | null {
try {
const qr = qrcode(0, 'M');
qr.addData(payload);
qr.make();
return qr.createSvgTag(3, 8);
} catch {
return null;
}
}

/** The import-intent QR, rendered once at module load (constant input). */
export const ICONSET_QR_SVG = qrSvg(ICONSET_IMPORT_URL);
75 changes: 75 additions & 0 deletions src/iconsetCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Sensors & Signals LLC https://www.snstac.com/
*/

import React, { useState } from 'react';
import {
Card,
CardBody,
CardExpandableContent,
CardHeader,
CardTitle,
} from '@patternfly/react-core/dist/esm/components/Card/index.js';
import cockpit from 'cockpit';

import { ICONSET_IMPORT_URL, ICONSET_QR_SVG, ICONSET_URL } from './iconset';

const _ = cockpit.gettext;

export function IconsetCard() {
const [expanded, setExpanded] = useState(false);

return (
<Card className="aiscot-expandable-card" isExpanded={expanded} data-testid="aiscot-iconset-card">
<CardHeader
className="ct-card-expandable-header"
onExpand={() => setExpanded(!expanded)}
toggleButtonProps={{
id: 'aiscot-iconset-expand',
'aria-label': expanded
? _('Collapse iconset downloads')
: _('Expand iconset downloads'),
}}
>
<CardTitle>{_('ATAK Vessel Iconset')}</CardTitle>
</CardHeader>
<CardExpandableContent>
<CardBody>
<p>
{_('AIS-catcher style ship-class markers for TAK clients: dart when underway, circle when stopped, diamond for AtoN / base station / SART, colored by vessel class (tankers red, cargo green, passenger blue, ...).')}
</p>
<p>
{_('Scan the QR code with ATAK (or the device camera) to import directly, or download the zip and import via Settings → Tool Preferences → Point Dropper → Iconset Manager. Once clients have the iconset, set SHIPCLASS_ICONS=true in the configuration above.')}
</p>
<p>
{_('Both options fetch from github.com. On offline networks, the same zip ships on this device with the aiscot package (aiscot/data/ais-ships-iconset.zip) — copy it to clients by any available means.')}
</p>
<div style={{ display: 'flex', gap: '1.5em', alignItems: 'center', flexWrap: 'wrap' }}>
{ICONSET_QR_SVG && (
<span
style={{
display: 'inline-block',
lineHeight: 0,
background: '#fff',
border: '1px solid #d2d2d2',
borderRadius: '4px',
}}
title={ICONSET_IMPORT_URL}
dangerouslySetInnerHTML={{ __html: ICONSET_QR_SVG }}
/>
)}
<div>
<div style={{ fontWeight: 600 }}>ais-ships-iconset.zip</div>
<div style={{ color: '#888', margin: '2px 0 6px' }}>
{_('Ships with AISCOT ≥ 7.3.0')}
</div>
<a href={ICONSET_URL} target="_blank" rel="noopener noreferrer" download>
{_('Download iconset zip')}
</a>
</div>
</div>
</CardBody>
</CardExpandableContent>
</Card>
);
}
Loading