Skip to content
design
Components

AIS target row

One AIS target in a list, with its ship type, name, MMSI, CPA, TCPA, SOG, COG and the age of its last report.

On this page

AIS targets, CPA 0.5 NM, TCPA 12 min 5

The AIS targets around an anchorage at night: one fishing vessel passes inside both limits, a yacht at anchor has gone quiet, and the rest pass clear.
Code
import { AisTargetRow, type AisShipType } from "@bosunkit/components/ais-target-row";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The AIS targets around the anchorage at Cala Portals at 02:20 UTC, with the CPA limit at 0.5 NM and the TCPA " +
  "limit at 12 min: a fishing vessel leaving harbor passes inside both and is marked dangerous, a yacht at anchor " +
  "has not reported for 4 minutes, a motor boat has sent no name, and the ferry and a tug pass well clear. Press a " +
  "target to select it.";

interface Target {
  readonly mmsi: string;
  readonly name: string;
  readonly shipType: AisShipType;
  readonly cpa: number | null;
  readonly tcpa: number | null;
  readonly sog: number | null;
  readonly cog: number | null;
  readonly ageSeconds: number;
}

/** The limits the crew set for the anchorage: inside both, a target is dangerous. */
const CPA_LIMIT_NM: number = 0.5;
const TCPA_LIMIT_MIN: number = 12;

const TARGETS: readonly Target[] = [
  {
    mmsi: "224113250",
    name: "AR MEN",
    shipType: "fishing",
    cpa: 0.2,
    tcpa: 6,
    sog: 7.4,
    cog: 212,
    ageSeconds: 4,
  },
  {
    mmsi: "224301450",
    name: "CIUDAD DE PALMA",
    shipType: "passenger",
    cpa: 2.4,
    tcpa: 18,
    sog: 21.5,
    cog: 64,
    ageSeconds: 9,
  },
  {
    mmsi: "224998812",
    name: "VB ANDRATX",
    shipType: "tug",
    cpa: 1.1,
    tcpa: 25,
    sog: 6.2,
    cog: 288,
    ageSeconds: 12,
  },
  {
    mmsi: "235117624",
    name: "WINDGRAZER",
    shipType: "sailing",
    cpa: null,
    tcpa: null,
    sog: 0,
    cog: null,
    ageSeconds: 250,
  },
  {
    mmsi: "224650031",
    name: "",
    shipType: "motor-boat",
    cpa: 0.9,
    tcpa: -3,
    sog: 14.8,
    cog: 140,
    ageSeconds: 31,
  },
];

const LIST: CSSProperties = {
  width: "100%",
  margin: 0,
  padding: 0,
  border: "1px solid var(--border)",
  borderRadius: "var(--radius-md)",
  overflow: "hidden",
};

export default function Showcase(): ReactElement {
  const [selected, setSelected]: [string, Dispatch<SetStateAction<string>>] = useState<string>("224113250");
  return (
    <div style={{ width: "100%", maxWidth: 680, display: "flex", flexDirection: "column", gap: "var(--space-xs)" }}>
      <SectionHeading level={3} count={TARGETS.length}>
        AIS targets, CPA 0.5 NM, TCPA 12 min
      </SectionHeading>
      <ul style={LIST}>
        {TARGETS.map((target: Target): ReactElement => (
          <AisTargetRow
            key={target.mmsi}
            name={target.name}
            mmsi={target.mmsi}
            shipType={target.shipType}
            cpa={target.cpa}
            tcpa={target.tcpa}
            sog={target.sog}
            cog={target.cog}
            cpaLimit={CPA_LIMIT_NM}
            tcpaLimit={TCPA_LIMIT_MIN}
            ageSeconds={target.ageSeconds}
            selected={selected === target.mmsi}
            onPress={(): void => {
              setSelected(target.mmsi);
            }}
          />
        ))}
      </ul>
    </div>
  );
}

An AIS target row shows one vessel the AIS receiver hears: the OpenBridge icon for its ship type with the type in words, its name and its MMSI, and four figures with their units in tabular figures: CPA in nautical miles, TCPA in minutes, SOG in knots and COG in three-digit degrees. A figure that is not known is a dash, never a zero; a TCPA below zero means the closest point has passed. Under the figures it says how long ago the target last reported.

The caller gives the CPA and TCPA limits. A target that closes within both, the limits included, is dangerous: the row draws the IEC dangerous target symbol with the words "Dangerous target", the alarm color on its leading edge, and the crossed figures in bold alarm color. Shape, words and color say it together, so a reader who cannot tell the colors apart sees it too. A target that has not reported for longer than staleAfterSeconds dims its figures and says how long it has been quiet.

01Danger in three waysA dangerous target carries the IEC symbol, the words and the alarm color together, never the color alone.
02Unknown is not zeroA figure the target has not given is a dash. A CPA of zero would read as a collision course.
03Age beside the figuresEvery row says how old its report is, and a quiet target looks quiet, so an old position is never read as a current one.

Figures

FigureMeansWrittenUnknown
CPAClosest point of approachNautical miles, one decimal—
TCPATime to the closest pointWhole minutes, negative once passed—
SOGSpeed over groundKnots, one decimal—
COGCourse over groundThree-digit degrees true—

The numbers follow core/i18n's rules: a decimal comma outside English, no grouping, and an exact tie rounded to the even digit. CPA and TCPA are floored instead, so a passing distance or the time left before it is never written as more than it is: 0.96 NM reads 0.9 NM, and 4.6 minutes 4 min.

Examples

Dangerous target

  • ROTTERDAM EXPRESSCargoMMSI 244123456Dangerous targetCPA0.3 NMTCPA8 minSOG18.2 knCOG095°6 s ago
  • ELBE SPIRITTankerMMSI 211445566CPA0.7 NMTCPA17 minSOG12.0 knCOG022°10 s ago
Two targets against a 0.5 NM and 12 min limit: the cargo ship passing 0.3 NM off in 8 minutes is inside both and marked dangerous by symbol, words and color, with the two figures that crossed; the tanker passing 0.7 NM off is not.
Code
import { AisTargetRow } from "@bosunkit/components/ais-target-row";
import type { ReactElement } from "react";

export const description: string =
  "Two targets against a 0.5 NM and 12 min limit: the cargo ship passing 0.3 NM off in 8 minutes is inside both and " +
  "marked dangerous by symbol, words and color, with the two figures that crossed; the tanker passing 0.7 NM off is not.";

export default function Dangerous(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AisTargetRow
        name="ROTTERDAM EXPRESS"
        mmsi="244123456"
        shipType="cargo"
        cpa={0.3}
        tcpa={8}
        sog={18.2}
        cog={95}
        cpaLimit={0.5}
        tcpaLimit={12}
        ageSeconds={6}
      />
      <AisTargetRow
        name="ELBE SPIRIT"
        mmsi="211445566"
        shipType="tanker"
        cpa={0.7}
        tcpa={17}
        sog={12}
        cog={22}
        cpaLimit={0.5}
        tcpaLimit={12}
        ageSeconds={10}
      />
    </ul>
  );
}

Limits

  • SAINT-MALO FERRYPassengerMMSI 235987654Dangerous targetCPA0.9 NMTCPA14 minSOG21.5 knCOG318°3 s ago
  • SAINT-MALO FERRYPassengerMMSI 235987654CPA0.9 NMTCPA14 minSOG21.5 knCOG318°3 s ago
The same crossing ferry under two settings: dangerous with the open-water limits of 1 NM and 20 min, clear with the harbor limits of 0.3 NM and 10 min.
Code
import { AisTargetRow } from "@bosunkit/components/ais-target-row";
import type { ReactElement } from "react";

export const description: string =
  "The same crossing ferry under two settings: dangerous with the open-water limits of 1 NM and 20 min, clear with " +
  "the harbor limits of 0.3 NM and 10 min.";

export default function Limits(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AisTargetRow
        name="SAINT-MALO FERRY"
        mmsi="235987654"
        shipType="passenger"
        cpa={0.9}
        tcpa={14}
        sog={21.5}
        cog={318}
        cpaLimit={1}
        tcpaLimit={20}
        ageSeconds={3}
      />
      <AisTargetRow
        name="SAINT-MALO FERRY"
        mmsi="235987654"
        shipType="passenger"
        cpa={0.9}
        tcpa={14}
        sog={21.5}
        cog={318}
        cpaLimit={0.3}
        tcpaLimit={10}
        ageSeconds={3}
      />
    </ul>
  );
}

Unknown figures

  • MMSI 227006120Unknown typeCPA—TCPA—SOG0.0 knCOG—1 min ago
A target that has sent no name or ship type, at anchor, so it has no closest approach and no course: the MMSI names it and each unknown figure is a dash, never a zero.
Code
import { AisTargetRow } from "@bosunkit/components/ais-target-row";
import type { ReactElement } from "react";

export const description: string =
  "A target that has sent no name or ship type, at anchor, so it has no closest approach and no course: the MMSI " +
  "names it and each unknown figure is a dash, never a zero.";

export default function Unknown(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AisTargetRow
        name=""
        mmsi="227006120"
        shipType="unknown"
        cpa={null}
        tcpa={null}
        sog={0}
        cog={null}
        cpaLimit={0.5}
        tcpaLimit={12}
        ageSeconds={95}
      />
    </ul>
  );
}

Stale report

  • PEN DUICKSailingMMSI 227334455CPA1.6 NMTCPA21 minSOG6.4 knCOG210°8 s ago
  • BELLE-ÎLEPassengerMMSI 228334455CPA3.0 NMTCPA44 minSOG16.0 knCOG182°No report for 6 min
A fresh target beside one that has not reported for 6 minutes, past the 180 s default: its figures dim and it says how long there has been no report.
Code
import { AisTargetRow } from "@bosunkit/components/ais-target-row";
import type { ReactElement } from "react";

export const description: string =
  "A fresh target beside one that has not reported for 6 minutes, past the 180 s default: its figures dim and it " +
  "says how long there has been no report.";

export default function Stale(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AisTargetRow
        name="PEN DUICK"
        mmsi="227334455"
        shipType="sailing"
        cpa={1.6}
        tcpa={21}
        sog={6.4}
        cog={210}
        cpaLimit={0.5}
        tcpaLimit={12}
        ageSeconds={8}
      />
      <AisTargetRow
        name="BELLE-ÎLE"
        mmsi="228334455"
        shipType="passenger"
        cpa={3}
        tcpa={44}
        sog={16}
        cog={182}
        cpaLimit={0.5}
        tcpaLimit={12}
        ageSeconds={372}
      />
    </ul>
  );
}

Selecting a target

With `onPress` and `selected`, each target is a toggle button named by the vessel; pressing one selects it.
Code
import { AisTargetRow, type AisShipType } from "@bosunkit/components/ais-target-row";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "With `onPress` and `selected`, each target is a toggle button named by the vessel; pressing one selects it.";

interface Target {
  readonly mmsi: string;
  readonly name: string;
  readonly shipType: AisShipType;
  readonly cpa: number;
  readonly tcpa: number;
  readonly sog: number;
  readonly cog: number;
}

const TARGETS: readonly Target[] = [
  { mmsi: "244990011", name: "SMIT TUG 7", shipType: "tug", cpa: 1.1, tcpa: 28, sog: 9.2, cog: 75 },
  { mmsi: "227112233", name: "AR MEN", shipType: "fishing", cpa: 1.4, tcpa: 35, sog: 3.1, cog: 240 },
  { mmsi: "538009911", name: "PACIFIC TRADER", shipType: "cargo", cpa: 4.2, tcpa: 63, sog: 14.8, cog: 12 },
];

export default function Select(): ReactElement {
  const [selected, setSelected]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      {TARGETS.map((target: Target): ReactElement => (
        <AisTargetRow
          key={target.mmsi}
          {...target}
          cpaLimit={0.5}
          tcpaLimit={12}
          ageSeconds={5}
          selected={selected === target.mmsi}
          onPress={(): void => {
            setSelected(selected === target.mmsi ? "" : target.mmsi);
          }}
        />
      ))}
    </ul>
  );
}

Ship types

  • MMSI 244000100CargoCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000101TankerCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000102PassengerCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000103Cruise shipCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000104FishingCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000105SailingCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000106Motor boatCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000107TugCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000108Pilot vesselCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000109Search and rescueCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000110MilitaryCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000111High-speed craftCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000112Small craftCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000113BargeCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000114Offshore supplyCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000115LifeboatCPA—TCPA—SOG—COG—10 s ago
  • MMSI 244000116Unknown typeCPA—TCPA—SOG—COG—10 s ago
Every ship type with its OpenBridge vessel icon and its word, from cargo to a target that reported none.
Code
import { AisTargetRow, type AisShipType } from "@bosunkit/components/ais-target-row";
import type { ReactElement } from "react";

export const description: string =
  "Every ship type with its OpenBridge vessel icon and its word, from cargo to a target that reported none.";

const TYPES: readonly AisShipType[] = [
  "cargo",
  "tanker",
  "passenger",
  "cruise",
  "fishing",
  "sailing",
  "motor-boat",
  "tug",
  "pilot",
  "sar",
  "military",
  "speed-craft",
  "small-craft",
  "barge",
  "psv",
  "lifeboat",
  "unknown",
];

export default function ShipTypes(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      {TYPES.map((shipType: AisShipType, index: number): ReactElement => (
        <AisTargetRow
          key={shipType}
          name=""
          mmsi={String(244000100 + index)}
          shipType={shipType}
          cpa={null}
          tcpa={null}
          sog={null}
          cog={null}
          cpaLimit={0.5}
          tcpaLimit={12}
          ageSeconds={10}
        />
      ))}
    </ul>
  );
}

Props

PropTypeDefaultDescription
namerequiredstring–The vessel's name as its AIS reports it; empty when it has reported none, and the MMSI then names the row.
mmsirequiredstring–The target's MMSI, nine digits.
shipTyperequiredAisShipType–The ship type the target reports, which picks its icon and word.
cparequirednumber | null–Closest point of approach, in nautical miles (NM); null when it is not known.
tcparequirednumber | null–Time to the closest point of approach, in minutes; negative once it has passed; null when not known.
sogrequirednumber | null–Speed over ground, in knots (kn); null when not known.
cogrequirednumber | null–Course over ground, in degrees true from 0 to 360; null when not known.
cpaLimitrequirednumber–The CPA limit in nautical miles: within it, and within tcpaLimit, the target is dangerous.
tcpaLimitrequirednumber–The TCPA limit in minutes: a target closing within cpaLimit this soon or sooner is dangerous.
ageSecondsrequirednumber–How long ago the target's last report arrived, in seconds.
staleAfterSecondsnumber | undefinedDEFAULT_STALE_AFTER_SECONDSThe age in seconds from which the row reads stale: its figures dim and it says how long there has been no report. 180 s by default, the longest interval at which a Class A transponder reports (at anchor).
onPress(() => void) | undefined–Called when the crew press the row, to select the target or open its detail; the row is then a button.
selectedboolean | undefined–Whether the target is the selected one, for a row that has onPress; it is then a toggle button.
classNamestring | undefined–Classes added to the row's own.
testIdstring | undefined–data-testid on the row; its parts are ${testId}--cpa, --tcpa, --sog, --cog, --age and --danger.
refRef<HTMLLIElement> | undefined–The row's <li> element.
Do
  • WINDGRAZERSailingMMSI 235117624CPA—TCPA—SOG0.0 knCOG—20 s ago
Write a figure the target has not given as a dash.
Don't
  • WINDGRAZERSailingMMSI 235117624Dangerous targetCPA0.0 NMTCPA0 minSOG0.0 knCOG000°20 s ago
Don't pass zero for a CPA nobody computed. The row reads a vessel on a collision course and marks it dangerous.
  • Data table: the AIS target list as a table to sort and filter, with a row's detail under it.
  • Skeleton: the placeholders an AIS list shows while it loads.
  • Alert list row: the close-quarters alarm a dangerous target raises.