import {LitElement, svg, html, css, nothing} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {InstrumentState} from '../types';
import {LinearAdvice, LinearAdviceRaw, renderAdvice} from './advice';
import {AdviceState} from '../watch/advice';
import {TickmarkStyle} from '../watch/tickmark';
import {singleSidedTickmark} from './tickmark';
import {PropellerType, bottomPropeller, topPropeller} from './propeller';
/**
* @element obc-thruster
*
* @prop {number} thrust - The thrust of the thruster in percent (-100 - +100)
* @prop {boolean} touching - Highlight the thruster when the lever is being touched
*/
@customElement('obc-thruster')
export class ObcThruster extends LitElement {
@property({type: Number}) thrust: number = 0;
@property({type: Number}) setpoint: number | undefined;
@property({type: Boolean}) touching: boolean = false;
@property({type: Boolean}) atSetpoint: boolean = false;
@property({type: Boolean}) disableAutoAtSetpoint: boolean = false;
@property({type: Number}) autoAtSetpointDeadband: number = 1;
@property({type: Number}) setpointAtZeroDeadband: number = 0.5;
@property({type: String}) state: InstrumentState = InstrumentState.inCommand;
@property({type: Boolean}) tunnel: boolean = false;
@property({type: Boolean}) singleSided: boolean = false;
@property({type: Boolean}) singleDirection: boolean = false;
@property({type: Boolean}) singleDirectionHalfSize: boolean = false;
@property({type: Array}) advices: LinearAdvice[] = [];
@property({type: String}) topPropeller: PropellerType = PropellerType.none;
@property({type: String}) bottomPropeller: PropellerType = PropellerType.none;
override render() {
return html`
${thruster(this.thrust, this.setpoint, this.state, {
atSetpoint: this.atSetpoint,
tunnel: this.tunnel,
setpointAtZeroDeadband: this.setpointAtZeroDeadband,
autoAtSetpoint: !this.disableAutoAtSetpoint,
autoSetpointDeadband: this.autoAtSetpointDeadband,
touching: this.touching,
singleSided: this.singleSided,
advices: this.advices,
singleDirection: this.singleDirection,
singleDirectionHalfSize: this.singleDirectionHalfSize,
topPropeller: this.topPropeller,
bottomPropeller: this.bottomPropeller,
narrow: !this.tunnel,
})}
`;
}
static override styles = css`
.container {
height: 100%;
width: 100%;
}
.container > svg {
height: 100%;
width: 100%;
}
`;
}
export function thrusterTop(
height: number,
value: number,
colors: {box: string; container: string},
options: {hideTicks: boolean; hideContainer: boolean}
) {
const container = svg`
`;
const track = svg``;
const tickmarks = [];
const nTicks = 2;
const delta = height / nTicks;
if (!options.hideTicks) {
for (let i = 1; i < nTicks; i++) {
tickmarks.push(
svg``
);
tickmarks.push(
svg``
);
}
}
const barHeight = (height * value) / 100;
const barY = -2 - barHeight;
const bar = svg``;
if (options.hideContainer) {
return [track, tickmarks, bar];
} else {
return [container, track, tickmarks, bar];
}
}
export function thrusterTopSingleSided(
height: number,
value: number,
colors: {box: string; container: string},
options: {
hideTicks: boolean;
flipAdicePattern: boolean;
hideContainer: boolean;
narrow: boolean;
},
advice: LinearAdviceRaw[]
) {
const container = options.narrow
? svg`
`
: svg`
`;
const track = options.narrow
? svg`
`
: svg`
`;
const tickmarks = options.hideTicks
? []
: [singleSidedTickmark(height, 50, TickmarkStyle.hinted)];
const barHeight = (height * value) / 100;
const barWidth = options.narrow ? 40 : 48;
const barX = options.narrow ? -32 : -40;
const barY = -2 - barHeight;
const maskId = options.flipAdicePattern
? 'thrusterBarMask1'
: 'thrusterBarMask2';
// The mask is used to clip the bar to the container shape
const mask = options.hideContainer
? nothing
: svg`
`;
const maskAttr = options.hideContainer ? '' : `mask="url(#${maskId})"`;
const bar = svg`
${mask}
`;
const advicesSvg = advice.map((a) =>
renderAdvice(height, a, options.flipAdicePattern)
);
const all = [tickmarks, bar, advicesSvg];
if (!options.hideContainer) {
all.splice(0, 0, [container, track]);
}
if (!options.narrow) {
return svg`${all}`;
} else {
return all;
}
}
export function thrusterBottom(
height: number,
value: number,
colors: {box: string; container: string},
options: {hideTicks: boolean; hideContainer: boolean}
) {
const container = svg`
${thrusterTop(height, value, colors, options)}
`;
return container;
}
function thrusterBottomSingleSided(
height: number,
value: number,
colors: {box: string; container: string},
options: {
hideTicks: boolean;
flipAdicePattern: boolean;
hideContainer: boolean;
narrow: boolean;
},
advice: LinearAdviceRaw[]
) {
const container = svg`
${thrusterTopSingleSided(height, value, colors, {hideTicks: options.hideTicks, flipAdicePattern: options.flipAdicePattern, hideContainer: options.hideContainer, narrow: options.narrow}, advice)}
`;
return container;
}
export function setpointSvg(
height: number,
value: number,
setpointAtZero: boolean,
colors: {fill: string; stroke: string},
options: {
inCommand: boolean;
singleSided: boolean;
narrow: boolean;
}
) {
const y = -(setpointAtZero
? 0
: Math.sign(value) * ((height * Math.abs(value)) / 100 + 2));
const extra = (options.singleSided ? -12 : 0) + (options.narrow ? 0 : 4);
let path;
if (options.inCommand) {
path =
'M23.5119 8C24.6981 6.35191 23.5696 4 21.5926 4L2.39959 4C0.422598 4 -0.705911 6.35191 0.480283 8L11.9961 24L23.5119 8Z';
} else {
path =
'M18.5836 8L5.4086 8L11.9961 17.1526L18.5836 8ZM23.5119 8C24.6981 6.35191 23.5696 4 21.5926 4L2.39959 4C0.422598 4 -0.705911 6.35191 0.480283 8L11.9961 24L23.5119 8Z';
}
return svg`
${
options.singleSided
? null
: svg`
`
}
`;
}
export function atSetpoint(
thrust: number,
setpoint: number | undefined,
options: {
autoAtSetpoint: boolean;
autoSetpointDeadband: number;
touching: boolean;
atSetpoint: boolean;
}
): boolean {
if (options.touching) {
return false;
}
if (options.autoAtSetpoint && setpoint !== undefined) {
return Math.abs(thrust - setpoint) < options.autoSetpointDeadband;
}
return options.atSetpoint;
}
export function thruster(
thrust: number,
setpoint: number | undefined,
state: InstrumentState,
options: {
atSetpoint: boolean;
tunnel: boolean;
singleSided: boolean;
singleDirection: boolean;
singleDirectionHalfSize: boolean;
setpointAtZeroDeadband: number;
autoAtSetpoint: boolean;
autoSetpointDeadband: number;
touching: boolean;
advices: LinearAdvice[];
topPropeller: PropellerType;
bottomPropeller: PropellerType;
narrow: boolean;
}
) {
if (options.tunnel) {
thrust = -thrust;
setpoint = setpoint === undefined ? undefined : -setpoint;
}
if (!options.singleSided && options.advices.length > 0) {
throw new Error('Double sided thruster does not support advice');
}
options.atSetpoint = atSetpoint(thrust, setpoint, options);
const tc = thrusterColors(options, state);
let centerLine = svg`
`;
if (options.singleSided) {
const width = options.narrow ? 64 : 72;
const x = options.narrow ? -32 : -36;
centerLine = svg``;
}
const setpointAtZero =
Math.abs(setpoint || 0) < options.setpointAtZeroDeadband;
const {topAdvices, bottomAdvices} = convertThrustAdvices(
options.advices,
thrust
);
const thrusterSvg = [];
const baseheight = options.topPropeller === PropellerType.none ? 134 : 106;
const height = options.singleDirection ? baseheight * 2 : baseheight;
if (options.singleSided) {
thrusterSvg.push(
thrusterTopSingleSided(
height,
Math.max(thrust, 0),
{box: tc.boxColor, container: tc.containerBackgroundColor},
{
hideTicks: tc.hideTicks,
flipAdicePattern: false,
hideContainer: false,
narrow: options.narrow,
},
topAdvices
)
);
if (!(options.singleDirection || options.singleDirectionHalfSize)) {
thrusterSvg.push(
thrusterBottomSingleSided(
height,
Math.max(-thrust, 0),
{box: tc.boxColor, container: tc.containerBackgroundColor},
{
hideTicks: tc.hideTicks,
flipAdicePattern: true,
hideContainer: false,
narrow: options.narrow,
},
bottomAdvices
)
);
}
thrusterSvg.push(centerLine);
} else {
thrusterSvg.push(
thrusterTop(
height,
Math.max(thrust, 0),
{box: tc.boxColor, container: tc.containerBackgroundColor},
{hideTicks: tc.hideTicks, hideContainer: false}
)
);
if (!options.singleDirection) {
thrusterSvg.push(
thrusterBottom(
height,
Math.max(-thrust, 0),
{box: tc.boxColor, container: tc.containerBackgroundColor},
{hideTicks: tc.hideTicks, hideContainer: false}
)
);
}
thrusterSvg.push(centerLine);
}
if (setpoint !== undefined) {
thrusterSvg.push(
setpointSvg(
height,
setpoint,
setpointAtZero,
{
fill: tc.setPointColor,
stroke: 'var(--border-silhouette-color)',
},
{
inCommand: state === InstrumentState.inCommand,
singleSided: options.singleSided,
narrow: options.narrow,
}
)
);
}
if (options.tunnel) {
return svg`
`;
} else {
let viewBox = '-80 -160 160 320';
let y = -160;
if (options.singleDirection) {
viewBox = '-80 -300 160 320';
y = -320;
}
const top = topPropeller(height, tc.arrowColor, options.topPropeller);
const bottom = bottomPropeller(
options.singleDirectionHalfSize ? 0.5 : height,
options.bottomPropeller
);
return svg`
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'obc-thruster': ObcThruster;
}
}
export function convertThrustAdvices(
advices: LinearAdvice[],
thrust: number
): {topAdvices: LinearAdviceRaw[]; bottomAdvices: LinearAdviceRaw[]} {
const rawAdvices: LinearAdviceRaw[] = advices.map((a) => {
const triggered = thrust >= a.min && thrust <= a.max;
let state: AdviceState;
if (triggered) {
state = AdviceState.triggered;
} else if (a.hinted) {
state = AdviceState.hinted;
} else {
state = AdviceState.regular;
}
return {
min: a.min,
max: a.max,
type: a.type,
state,
hinted: a.hinted,
};
});
const topAdvices = rawAdvices.filter((a) => a.min >= 0);
const bottomAdvices = rawAdvices
.filter((a) => a.max <= 0)
.map((a) => ({...a, min: -a.max, max: -a.min}));
return {topAdvices, bottomAdvices};
}
export function thrusterColors(
options: {atSetpoint: boolean; touching: boolean},
state: InstrumentState
) {
let boxColor = 'var(--instrument-enhanced-secondary-color)';
let setPointColor = 'var(--instrument-enhanced-primary-color)';
let arrowColor = 'var(--instrument-regular-secondary-color)';
let containerBackgroundColor = 'var(--instrument-frame-primary-color)';
let zeroLineColor = 'var(--instrument-enhanced-secondary-color)';
let hideTicks = false;
if (options.atSetpoint) {
setPointColor = boxColor;
}
if (state === InstrumentState.active) {
boxColor = 'var(--instrument-regular-secondary-color)';
zeroLineColor = 'var(--instrument-regular-secondary-color)';
setPointColor = 'var(--instrument-regular-primary-color)';
arrowColor = 'var(--instrument-regular-secondary-color)';
if (options.atSetpoint) {
setPointColor = boxColor;
}
} else if (state === InstrumentState.loading) {
boxColor = 'transparent';
setPointColor = 'var(--instrument-frame-tertiary-color)';
zeroLineColor = 'var(--instrument-frame-tertiary-color)';
arrowColor = 'var(--instrument-regular-secondary-color)';
hideTicks = true;
} else if (state === InstrumentState.off) {
boxColor = 'transparent';
setPointColor = 'var(--instrument-frame-tertiary-color)';
arrowColor = 'var(--instrument-frame-tertiary-color)';
zeroLineColor = 'var(--instrument-frame-tertiary-color)';
hideTicks = true;
containerBackgroundColor = 'transparent';
}
return {
zeroLineColor,
boxColor,
containerBackgroundColor,
hideTicks,
setPointColor,
arrowColor,
};
}