import { TemplateResult, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import '@shoelace-style/shoelace/dist/themes/light.css';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js';
import SlBadge from '@shoelace-style/shoelace/dist/components/badge/badge.component.js';
import SlTooltip from '@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js';
import SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js';
import SlButtonGroup from '@shoelace-style/shoelace/dist/components/button-group/button-group.component.js';
import {
biSkipStart,
biPlay,
biSkipEnd,
biAlphabet,
biSkipForward,
biArrowCounterclockwise,
biPause,
biStop,
biArrowRight,
biHandIndexThumb,
} from '../styles/icons';
import { Automaton, SimulationFeedback, SimulationResult, SimulationStatus } from '../automata';
import { classMap } from 'lit/directives/class-map.js';
import { SlChangeEvent } from '@shoelace-style/shoelace';
import { styleMap } from 'lit/directives/style-map.js';
import { simulationMenuStyles } from '../styles/simulationMenu';
import { LitElementWw } from '@webwriter/lit';
import { Graph } from '../graph';
import { ManualAutoSimulator } from 'automata/manual-auto';
import { localized, msg } from '@lit/localize';
import { Logger } from '@u/logger';
@customElement('webwriter-automaton-simulatormenu')
@localized()
export class SimulatorMenu extends LitElementWw {
@state()
private accessor _automaton!: Automaton;
public set automaton(automaton: Automaton) {
this._automaton = automaton;
}
@property({ type: Object, attribute: false })
public accessor graph!: Graph;
@state()
private accessor _result!: {
status: SimulationStatus;
wordPosition: number;
step: number;
} | null;
private set result(result: { status: SimulationStatus; wordPosition: number; step?: number }) {
this._result = {
status: result.status,
wordPosition: result.wordPosition,
step: result.step || result.wordPosition,
};
}
@state()
private accessor _simulationResult: SimulationResult | null = null;
@property({ type: String, attribute: false })
private accessor _mode: 'idle' | 'step' | 'run' | 'animate' = 'idle';
@state()
private accessor _animationRunning: boolean = false;
public static get styles() {
return simulationMenuStyles;
}
@query('#simulator_back') private accessor _backButton!: SlButton;
@query('#simulator_next') private accessor _nextButton!: SlButton;
@query('#simulator_toggle') private accessor _toggleButton!: SlButton;
@query('#simulator_stop') private accessor _stopButton!: SlButton;
@query('#wordInput') private accessor _wordInput!: SlInput;
public static get scopedElements() {
return {
'sl-button': SlButton,
'sl-tooltip': SlTooltip,
'sl-badge': SlBadge,
'sl-input': SlInput,
'sl-button-group': SlButtonGroup,
};
}
render() {
return html`
${this.renderLabel()} ${this.renderInput()} ${this.renderButtonGroup()}
`;
}
private renderLabel() {
if (!this._simulationResult) {
return html`
`;
}
else if (this._simulationResult.errors && this._simulationResult.errors.length > 0) {
return html`
${msg(html`Please fix the following errors to run the simulation:`)}
${ this._simulationResult.errors.map(e => {
if (e.node) {
return html`${e.node?.label}: ${e.message}
`;
}
return html`${e.message}
`;
}) }
`;
}
else if (this._result?.status === SimulationStatus.NO_PATH || !this._simulationResult.path || this._simulationResult.path.nodes.length === 0) {
return html`
${msg(html`No valid path found. The automaton rejects the word ${this._automaton.simulator.word}.`)}
`;
}
else {
let pathHtml: TemplateResult<1>[] = [];
const pathLengthShown = (this._result?.step || 0) + 1;
for (let i = 0; i < pathLengthShown; i++) {
const nodeLabel = this._simulationResult?.path.nodes[i].label || '';
pathHtml.push(html` {
this.goToStep(i);
}}
class="simulator__label__path__node"
?circle=${nodeLabel.length <= 3}
?pill=${nodeLabel.length > 3}
size="medium"
>${nodeLabel}`);
if (i < pathLengthShown - 1) {
pathHtml.push(html`
${this._simulationResult?.path.transitions[i].symbol || 'ε'}
${biArrowRight}
`);
}
}
return html`
${!!this._result?.status && this._result.status === SimulationStatus.ACCEPTED
? msg(html`
The automaton accepts the word ${this._automaton.simulator.word || "ε"}.`)
: !!this._result?.status && this._result.status === SimulationStatus.REJECTED
? msg(html`
The automaton rejects the word ${this._automaton.simulator.word || "ε"}.`)
: !!this._result?.status && this._result.status === SimulationStatus.NO_MOVES
? html`
${msg("No further moves possible. Try a different path.")}`
: !!this._result?.status && this._result.status === SimulationStatus.PAUSED
? html`
${msg("Simulation paused.")}`
: ""}
${pathHtml}
`;
}
}
private renderInput() {
return html` {
this._automaton.simulator.word = (e.target as SlInput).value;
this.reset();
this.requestUpdate();
}}
value=${this._automaton.simulator.word}
id="wordInput"
placeholder=${msg("Input Word e.g. aaabbb, step;step;stop")}
clearable
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>
${biAlphabet}
`;
}
private renderButtonGroup() {
return html`
${this._automaton.type === 'pda' || this._automaton.type === 'nfa' ? html`
{
this.startStepByStep(true);
}}
>${biHandIndexThumb}
` : ''}
{
this.startAnimation();
}}
>${biPlay}
{
this.startStepByStep();
}}
>${biSkipEnd}
{
this.run();
}}
>${biSkipForward}
{
this.stepBackward();
}}
disabled
>${biSkipStart}
{
this.stepForward();
}}
>${biSkipEnd}
{
this.reset();
}}
>${biArrowCounterclockwise}
{
this.reset();
}}
>${biArrowCounterclockwise}
{
this.toggleAnimation();
}}
id="simulator_toggle"
>${this._animationRunning ? biPause : biPlay}
{
this.stopAnimation();
}}
id="simulator_stop"
>${biStop}
{
this.reset();
}}
>${biArrowCounterclockwise}
`;
}
private run() {
this.reset();
this._mode = 'run';
Logger.time('simulation');
const result = this._automaton.simulator.simulate();
Logger.timeEnd('simulation');
this.result = {
status: result.status,
wordPosition: this._automaton.simulator.word.length,
step: (result.simulationResult?.path?.nodes.length || 1) - 1,
};
this._simulationResult = result.simulationResult || null;
Logger.log('Simulation Result:', this._simulationResult);
this.requestUpdate();
}
private startAnimation() {
this.reset();
this._mode = 'animate';
if (!!this._automaton.getInitialNode())
this._automaton.highlightNode(this._automaton.getInitialNode());
this._automaton.simulator.startAnimation((result: SimulationFeedback) => {
Logger.log('Animation Result:', result);
this.result = result;
this._simulationResult = result.simulationResult || null;
if (result.status !== SimulationStatus.RUNNING) {
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
}
this.requestUpdate();
});
this._animationRunning = true;
this.requestUpdate();
}
private stopAnimation() {
this._automaton.simulator.stopAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
this.requestUpdate();
});
this._animationRunning = false;
this.requestUpdate();
}
private toggleAnimation() {
if (this._animationRunning) {
this._automaton.simulator.pauseAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
this.requestUpdate();
});
this._animationRunning = false;
} else {
this._automaton.simulator.startAnimation((result) => {
this.result = result;
this._simulationResult = result.simulationResult || null;
if (result.status !== SimulationStatus.RUNNING) {
this._toggleButton.disabled = true;
this._stopButton.disabled = true;
}
this.requestUpdate();
});
this._animationRunning = true;
}
this.requestUpdate();
}
public reset() {
this._automaton.simulator.reset();
this._result = {
status: SimulationStatus.IDLE,
wordPosition: 0,
step: 0,
};
this._simulationResult = null;
this._mode = 'idle';
this._nextButton.disabled = false;
this._backButton.disabled = true;
this._toggleButton.disabled = false;
this._stopButton.disabled = false;
this.requestUpdate();
}
public init() {
this._automaton.simulator.init();
}
private startStepByStep(manual = false) {
this.reset();
this._mode = 'step';
if (manual && (this._automaton.type === 'nfa' || this._automaton.type === 'pda')) {
(this._automaton.simulator as ManualAutoSimulator).setManualMode(true);
}
this._automaton.simulator.initStepByStep(this.graph, (res: any) => {
Logger.log(res);
this.result = res;
this._simulationResult = res.simulationResult || null;
this._backButton.disabled = res.step <= 0;
if (
res.status === SimulationStatus.NO_PATH
|| res.status === SimulationStatus.ACCEPTED
|| res.status === SimulationStatus.REJECTED
|| res.status === SimulationStatus.ERROR
) {
this._nextButton.disabled = true;
}
this.requestUpdate();
});
if (!!this._automaton.getInitialNode())
this._automaton.highlightNode(this._automaton.getInitialNode());
this.requestUpdate();
}
private goToStep(step: number) {
if (this._mode !== 'step') {
this.startStepByStep();
}
const result = this._automaton.simulator.goToStep(step);
this.result = result;
if (result.simulationResult) {
this._simulationResult = result.simulationResult;
}
this._nextButton.disabled = result.finalStep || false;
this._backButton.disabled = result.firstStep || false;
this.requestUpdate();
}
private stepForward() {
this.goToStep(this._result?.step !== undefined ? this._result.step + 1 : 0);
}
private stepBackward() {
this.goToStep(this._result?.step !== undefined ? this._result.step - 1 : 0);
}
}