Skip to content
design
Instruments

Position deviation

The drift from a commanded position, as a circle that grows with the distance, with caution and alarm rings.

On this page
ESWN
North up, both limits, the trail and the color of the ring it is past
N
Heading up at the helm, scaled to the caution ring
3
Off station
m
GPS
200
°
Bearing to station
12 m, 20 m
Caution, alarm
Holding station off the fuel dock on a live feed: the drift crosses the caution and alarm rings with a trail of past positions, north up and heading up.
Code
import {
  PositionDeviation,
  PositionDeviationOrientation,
  PositionDeviationPriority,
  Readout,
  ReadoutSize,
  ReadoutValueType,
} from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Holding station off the fuel dock on a live feed: the wind pushes the vessel off its position and the helm brings " +
  "it back, the circle crosses the caution and then the alarm ring and turns their color, and the last eight " +
  "positions trail behind, on a north-up card beside a heading-up one.";

interface TrailPoint {
  distanceM: number;
  bearingDeg: number;
}

const CAUTION_LIMIT: number = 12;
const ALARM_LIMIT: number = 20;
const TRAIL_POINTS: number = 8;
const TRAIL_SPACING_SECONDS: number = 2;

const PANEL: CSSProperties = {
  display: "flex",
  flexWrap: "wrap",
  alignItems: "center",
  justifyContent: "center",
  gap: 24,
  width: "100%",
  maxWidth: 660,
};

const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 6, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 220 };
const COLUMN: CSSProperties = { display: "flex", flexDirection: "column", gap: 14 };

/** Distance in meters from the vessel to the held position: the drift builds over forty seconds and is corrected. */
function deviationAt(time: number): number {
  return 3 + 19 * ((1 - Math.cos((2 * Math.PI * time) / 40)) / 2);
}

/** Bearing in degrees from the vessel to the held position, swinging as the wind shifts. */
function bearingAt(time: number): number {
  return 200 + 25 * Math.sin(time / 9);
}

function priorityFor(deviation: number): PositionDeviationPriority {
  if (deviation >= ALARM_LIMIT) {
    return PositionDeviationPriority.alarm;
  }
  if (deviation >= CAUTION_LIMIT) {
    return PositionDeviationPriority.caution;
  }
  return PositionDeviationPriority.regular;
}

export default function Showcase(): ReactElement {
  const [second, setSecond]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);

  useEffect((): (() => void) => {
    const interval: number = window.setInterval((): void => {
      setSecond((previous: number): number => previous + 1);
    }, 1000);
    return (): void => {
      window.clearInterval(interval);
    };
  }, []);

  const deviation: number = deviationAt(second);
  const bearing: number = bearingAt(second);
  const heading: number = 112 + 6 * Math.sin(second / 5);
  const trail: TrailPoint[] = Array.from({ length: TRAIL_POINTS }, (_: unknown, index: number): TrailPoint => {
    const time: number = second - (TRAIL_POINTS - 1 - index) * TRAIL_SPACING_SECONDS;
    return { distanceM: deviationAt(time), bearingDeg: bearingAt(time) };
  });

  return (
    <div style={PANEL}>
      <figure style={FIGURE}>
        <div style={{ width: 250, height: 250 }}>
          <PositionDeviation
            orientation={PositionDeviationOrientation.northUp}
            priority={priorityFor(deviation)}
            heading={heading}
            deviation={deviation}
            setpointBearing={bearing}
            cautionLimit={CAUTION_LIMIT}
            alarmLimit={ALARM_LIMIT}
            showLabels
            trail={trail}
          />
        </div>
        <figcaption style={CAPTION}>North up, both limits, the trail and the color of the ring it is past</figcaption>
      </figure>

      <figure style={FIGURE}>
        <div style={{ width: 170, height: 170 }}>
          <PositionDeviation
            orientation={PositionDeviationOrientation.headingUp}
            priority={PositionDeviationPriority.enhanced}
            heading={heading}
            deviation={deviation}
            setpointBearing={bearing}
            cautionLimit={CAUTION_LIMIT}
            alarmLimit={ALARM_LIMIT}
            hasAlarmLimit={false}
            showLabels={false}
          />
        </div>
        <figcaption style={CAPTION}>Heading up at the helm, scaled to the caution ring</figcaption>
      </figure>

      <div style={COLUMN}>
        <Readout label="Off station" value={deviation} unit="m" src="GPS" size={ReadoutSize.large} />
        <Readout label="Bearing to station" value={bearing} hasDegree />
        <Readout
          label="Caution, alarm"
          valueType={ReadoutValueType.text}
          value={`${String(CAUTION_LIMIT)} m, ${String(ALARM_LIMIT)} m`}
        />
      </div>
    </div>
  );
}

The position deviation instrument shows how far the vessel has drifted from a position it is holding, as in station keeping. A filled circle grows with the distance, a line and a dot point at the setpoint, and a dotted caution ring and a solid alarm ring mark the limits, so the circle crosses a ring as the drift grows. trail adds the recent positions. It ports obc-position-deviation, and the trail is a house extension; the overview lists the other instruments.

Examples

North up

ESWN
A drift of 8 meters, inside the caution limit, with north at the top.
Code
import { PositionDeviation, PositionDeviationOrientation, PositionDeviationPriority } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A drift of 8 meters, inside the caution limit, with north at the top.";

export default function NorthUp(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <PositionDeviation
        orientation={PositionDeviationOrientation.northUp}
        priority={PositionDeviationPriority.regular}
        heading={47}
        deviation={8}
        setpointBearing={225}
        cautionLimit={12}
        alarmLimit={20}
      />
    </div>
  );
}

Heading up

NESW
A drift of 14 meters, past the caution limit, with the card turned so the heading arrow points up.
Code
import { PositionDeviation, PositionDeviationOrientation, PositionDeviationPriority } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "A drift of 14 meters, past the caution limit, with the card turned so the heading arrow points up.";

export default function HeadingUp(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <PositionDeviation
        orientation={PositionDeviationOrientation.headingUp}
        priority={PositionDeviationPriority.caution}
        heading={47}
        deviation={14}
        setpointBearing={225}
        cautionLimit={12}
        alarmLimit={20}
      />
    </div>
  );
}

With trail

ESWN
A drift of 18 meters, near the alarm limit, with the last five positions as a fading trail.
Code
import { PositionDeviation, PositionDeviationOrientation, PositionDeviationPriority } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "A drift of 18 meters, near the alarm limit, with the last five positions as a fading trail.";

export default function WithTrail(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <PositionDeviation
        orientation={PositionDeviationOrientation.northUp}
        priority={PositionDeviationPriority.alarm}
        heading={47}
        deviation={18}
        setpointBearing={205}
        cautionLimit={12}
        alarmLimit={20}
        trail={[
          { distanceM: 4, bearingDeg: 335 },
          { distanceM: 7, bearingDeg: 8 },
          { distanceM: 11, bearingDeg: 42 },
          { distanceM: 15, bearingDeg: 73 },
          { distanceM: 18, bearingDeg: 101 },
        ]}
      />
    </div>
  );
}

No reading

ESWN
A deviation, bearing and heading that are not numbers draw no deviation circle and no setpoint line.
Code
import { PositionDeviation, PositionDeviationOrientation } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "A deviation, bearing and heading that are not numbers draw no deviation circle and no setpoint line.";

export default function NoReading(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <PositionDeviation
        orientation={PositionDeviationOrientation.northUp}
        deviation={Number.NaN}
        setpointBearing={Number.NaN}
        heading={Number.NaN}
      />
    </div>
  );
}

Origin and limits

Declare the held position, distance unit and orientation of the view. Keep the current offset, setpoint and trail in the same coordinate frame; a change of origin must update them together. Explain the trail's time window so a long path is not confused with a large instantaneous error.

Caution and alarm rings depict supplied limits. The drawing does not establish an active station-keeping or anchor-watch service and does not emit alerts. Pair it with the numeric deviation, position quality and the monitor's actual state. Missing positions must not reset the vessel silently to the center.

Props

PropTypeDefaultDescription
orientationPositionDeviationOrientation | undefinedPositionDeviationOrientation.northUpCompass card orientation: northUp (default) keeps north at the top and turns the heading arrow; headingUp keeps the heading arrow at the top and turns the card.
priorityPositionDeviationPriority | undefinedPositionDeviationPriority.regularColor of the deviation circle: regular (neutral, default), enhanced (blue accent), or caution and alarm, which tint it with the alert colors.
headingnumber | undefined0Heading in degrees (0 = north, clockwise).
deviationnumber | undefined0Distance from the current position to the setpoint, in the same unit as the limits.
setpointBearingnumber | undefined0Bearing from the current position towards the setpoint in degrees (0 = north, clockwise).
cautionLimitnumber | undefined15Deviation at which the dotted caution ring is drawn; hidden when not a positive finite number.
alarmLimitnumber | undefined20Deviation at which the solid alarm ring is drawn; hidden when not a positive finite number.
hasAlarmLimitboolean | undefinedtrueWhen false, hides the alarm ring and anchors the scale to the caution ring.
showLabelsboolean | undefinedtrueWhen true, shows the N/E/S/W labels on the compass card.
trail{ distanceM: number; bearingDeg: number }[] | undefined[]PORT EXTENSION: position history, oldest first, rendered as a fading trail. Upstream obc-position-deviation has no trail property.