Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,12 @@ label div {
line-height: 1.45;
}

/* Touch targets sized for gloved field use (>=48px on all controls). */
.pf-v6-c-button,
.pf-c-button,
button {
min-height: 2.25rem;
min-height: 3rem;
padding-inline: 1.2rem;
border-radius: 5px;
font-weight: 650;
}
Expand Down Expand Up @@ -172,6 +174,7 @@ select,
textarea {
width: min(100%, 34rem) !important;
max-width: 100%;
min-height: 3rem;
padding: 0.55rem 0.65rem;
border: 1px solid var(--aos-border-strong);
border-radius: 5px;
Expand Down
5 changes: 4 additions & 1 deletion 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 Expand Up @@ -414,7 +417,7 @@ export const Application: React.FC = () => {
}
onChange={ev =>
handleEnvVarChange(key, ev.target.value)}
style={{ width: '300px', fontFamily: 'monospace' }}
style={{ fontFamily: 'monospace' }}
/>
)}
{formErrors[key] && (
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
Loading