@webwriter/chemlab
Version:
WIP - Prepare virtual laboratory environments for various topics in chemistry. Includes the building of molecules as well as applications for electrochemistry and acid/base theory.
1,826 lines (1,784 loc) • 193 kB
text/typescript
import { html, LitElement, PropertyValueMap } from "lit";
import { LitElementWw } from "@webwriter/lit";
import {
customElement,
property,
query,
} from "lit/decorators.js";
import { style } from "chemlab-style";
import * as PIXI from "pixi.js";
import * as chemlib from "chemlib";
import SmilesDrawer from "smiles-drawer"
import SlRange from "@shoelace-style/shoelace/dist/components/range/range.component.js";
import SlDetails from "@shoelace-style/shoelace/dist/components/details/details.component.js";
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js";
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js";
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
import SlTag from "@shoelace-style/shoelace/dist/components/tag/tag.component.js";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import SlOption from "@shoelace-style/shoelace/dist/components/option/option.component.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlAlert from "@shoelace-style/shoelace/dist/components/alert/alert.component.js";
import SlBadge from "@shoelace-style/shoelace/dist/components/badge/badge.component.js";
import SlTab from "@shoelace-style/shoelace/dist/components/tab/tab.component.js";
import SlCard from "@shoelace-style/shoelace/dist/components/card/card.component.js";
import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js";
import SlTabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component.js";
import SlTabPanel from "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.component.js";
import SlSplitPanel from "@shoelace-style/shoelace/dist/components/split-panel/split-panel.component.js";
import SlDivider from "@shoelace-style/shoelace/dist/components/divider/divider.component.js";
import SlRating from "@shoelace-style/shoelace/dist/components/rating/rating.component.js";
import SlMenu from "@shoelace-style/shoelace/dist/components/menu/menu.component.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js";
import SlMenuLabel from "@shoelace-style/shoelace/dist/components/menu-label/menu-label.component.js";
import SlCarousel from "@shoelace-style/shoelace/dist/components/carousel/carousel.component.js";
import SlCarouselItem from "@shoelace-style/shoelace/dist/components/carousel-item/carousel-item.component.js";
import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js";
import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js";
import SlDrawer from "@shoelace-style/shoelace/dist/components/drawer/drawer.component.js";
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js";
import SlRadio from "@shoelace-style/shoelace/dist/components/radio/radio.component.js";
import SlSwitch from "@shoelace-style/shoelace/dist/components/switch/switch.component.js";
import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.component.js";
import "@shoelace-style/shoelace/dist/themes/light.css";
import { setAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry.js";
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
setBasePath("@shoelace-style/shoelace/dist");
import {
HelpOverlay,
HelpPopup,
} from "@webwriter/wui/dist/helpSystem/helpSystem.js";
import {
uniqueNamesGenerator,
Config,
adjectives,
colors,
animals,
} from "unique-names-generator";
import Chart from "chart.js/auto";
// @ts-ignore
import beaker_svg from "./media/beaker.svg";
// @ts-ignore
import beaker_fl_svg from "./media/beaker_full_l.svg";
// @ts-ignore
import beaker_fs_svg from "./media/beaker_full_s.svg";
// @ts-ignore
import flask_svg from "./media/flask.svg";
// @ts-ignore
import flask_fl_svg from "./media/flask_full_l.svg";
// @ts-ignore
import flask_fs_svg from "./media/flask_full_s.svg";
// @ts-ignore
import burette_svg from "./media/burette.svg";
// @ts-ignore
import burette_fl_svg from "./media/burette_full_l.svg";
// @ts-ignore
import burette_fs_svg from "./media/burette_full_s.svg";
// @ts-ignore
import testtube_svg from "./media/testtube.svg";
// @ts-ignore
import testtube_fl_svg from "./media/testtube_full_l.svg";
// @ts-ignore
import testtube_fs_svg from "./media/testtube_full_S.svg";
// @ts-ignore
import cable_svg from "./media/cable.svg";
// @ts-ignore
import source_off_svg from "./media/source_off.svg";
// @ts-ignore
import source_on_svg from "./media/source_on.svg";
// @ts-ignore
import voltmeter_off_svg from "./media/voltmeter_off.svg";
// @ts-ignore
import voltmeter_on_svg from "./media/voltmeter_on.svg";
// @ts-ignore
import default_svg from "./media/default.svg";
// @ts-ignore
import bg_svg from "./media/bg.png";
import { HALF_PI, isNumber } from "chart.js/helpers";
/**
* chemlab webwriter widget for presenting interactive chemistry experiments
*
* @export
* @class ChemLab
* @typedef {ChemLab}
* @extends {LitElementWw}
*/
@customElement("webwriter-chemlab")
export class ChemLab extends LitElementWw {
/**
* css styles
*
* @static
* @type {*}
*/
static styles = style;
/**
* main container of the canvas
*
* @private
* @type {HTMLDivElement}
*/
@query("#upper")
private accessor containerDiv: HTMLDivElement;
/**
* molecule part of context menu in teacher view
*
* @private
* @type {SlMenu}
*/
@query("#molConfig")
private accessor molConfig: SlMenu;
/**
* mixture part of context menu in teacher view
*
* @private
* @type {SlMenu}
*/
@query("#mixConfig")
private accessor mixConfig: SlMenu;
/**
* component part of context menu in teacher view
*
* @private
* @type {SlMenu}
*/
@query("#compConfig")
private accessor compConfig: SlMenu;
/**
* molecule list element inside the tab panel
*
* @private
* @type {SlCarousel}
*/
@query("#mixCarousel")
private accessor mixCarousel: SlCarousel;
/**
* main context menu element for both views
*
* @private
* @type {SlMenu}
*/
@query("#contextMenu")
private accessor contextMenu: SlMenu;
/**
* confirmation dialog box element
*
* @private
* @type {SlDialog}
*/
@query("#confirmDialog")
private accessor confirmDialog: SlDialog;
/**
* input for molar amounts of molecules
*
* @private
* @type {SlInput}
*/
@query("#molAmount")
private accessor molAmount: SlInput;
/**
* search field for filtering element data
*
* @private
* @type {SlInput}
*/
@query("#elemSearch")
private accessor elemSearch: SlInput;
/**
* shoelace tab container for the panel
*
* @private
* @type {SlTabGroup}
*/
@query("#tab_select")
private accessor tab_select: SlTabGroup;
/**
* main panel tab for current component
*
* @private
* @type {SlTab}
*/
@query("#settings_drawer")
private accessor settings_drawer: SlTab;
/**
* shoelace badge for labeling possible conent in a component
*
* @private
* @type {SlTag}
*/
@query("#property_badge")
private accessor property_tag: SlTag;
/**
* molecule select for new bonds
*
* @private
* @type {SlSelect}
*/
@query("#bond_1")
private accessor bond_sel_1: SlSelect;
/**
* molecule select for new bonds
*
* @private
* @type {SlSelect}
*/
@query("#bond_2")
private accessor bond_sel_2: SlSelect;
/**
* bond type select
*
* @private
* @type {SlSelect}
*/
@query("#bond_3")
private accessor bond_sel_3: SlSelect;
/**
* select for e-component connection partners
*
* @private
* @type {SlSelect}
*/
@query("#newECon")
private accessor conSelect: SlSelect;
/**
* slider for bond magnitude of new bonds
*
* @private
* @type {HTMLInputElement}
*/
@query("#bondMag")
private accessor bondMag: HTMLInputElement;
/**
* container for the connection view of the panel
*
* @private
* @type {HTMLDivElement}
*/
@query("#conInfo")
private accessor eConInfo: HTMLDivElement;
/**
* container for the information view of the panel
*
* @private
* @type {HTMLDivElement}
*/
@query("#compInfo")
private accessor eCompInfo: HTMLDivElement;
/**
* shoelace alert element for all messages
*
* @private
* @type {SlAlert}
*/
@query("#dialogOverview")
private accessor dialogAlert: SlAlert;
/**
* shoelace popup containing the object context menu element
*
* @private
* @type {SlPopup}
*/
@query("#contextPopup")
private accessor contextPopup: SlPopup;
/**
* container inside object context menu showing molar info
*
* @private
* @type {HTMLDivElement}
*/
@query("#contextMolInfo")
private accessor contextMolInfo: HTMLDivElement;
/**
* container inside object context menu showing general info on current mixture
*
* @private
* @type {HTMLDivElement}
*/
@query("#contextMixInfo")
private accessor contextMixInfo: HTMLDivElement;
/**
* button inside object context menu to open burette components
*
* @private
* @type {HTMLButtonElement}
*/
@query("#openComp")
private accessor openBtn: HTMLButtonElement;
/**
* button inside object context menu to activate electric components
*
* @private
* @type {HTMLButtonElement}
*/
@query("#activateEComp")
private accessor activateEComp: HTMLButtonElement;
/**
* container inside object context menu showing general info on current e-component
*
* @private
* @type {HTMLButtonElement}
*/
@query("#contextEchemInfo")
private accessor contextEchemInfo: HTMLButtonElement;
/**
* shoelace popup containing the tab panel
*
* @private
* @type {SlPopup}
*/
@query("#tabPopup")
private accessor tabPopup: SlPopup;
/**
* anchor element of the panel popup
*
* @private
* @type {HTMLSpanElement}
*/
@query("#tabPopupAnchor")
private accessor tabPopupAnchor: HTMLSpanElement;
/**
* anchor element of the context popup
*
* @private
* @type {HTMLSpanElement}
*/
@query("#contextPopupAnchor")
private accessor contextPopupAnchor: HTMLSpanElement;
/**
* part of options, toggles xAPI statements
*
* @private
* @type {SlCheckbox}
*/
@query("#xAPICheck")
private accessor xAPICheck: SlCheckbox;
/**
* xAPI API key input
*
* @private
* @type {SlInput}
*/
@query("#xAPIKeyInput")
private accessor xAPIKeyInput: SlInput;
/**
* select for components to get deactivated for students
*
* @private
* @type {SlSelect}
*/
@query("#invCompSelect")
private accessor invCompSelect: SlSelect;
/**
* select for premade molecules inside the panel
*
* @private
* @type {SlSelect}
*/
@query("#molSelect")
private accessor molSelect: SlSelect;
/**
* select for premade mixtures inside the panel
*
* @private
* @type {SlSelect}
*/
@query("#mixSelect")
private accessor mixSelect: SlSelect;
/**
* input for the volume of premade mixtures
*
* @private
* @type {SlInput}
*/
@query("#mixVolAmount")
private accessor mixVolAmount: SlInput;
/**
* shoelace drawer containing diagramm containers
*
* @private
* @type {SlDrawer}
*/
@query("#diagramDrawer")
private accessor diagramDrawer: SlDrawer;
/**
* main container inside diagram drawer for all diagrams and headings
*
* @private
* @type {HTMLDivElement}
*/
@query("#canvasContainer")
private accessor canvasContainer: HTMLDivElement;
/**
* part of options, toggles different panel variants
*
* @private
* @type {SlRadioGroup}
*/
@query("#panelVariantOptions")
private accessor panelVariantOptions: SlRadioGroup;
/**
* part of the tab panels split panel element, contains element overview and molecule split panel
*
* @private
* @type {SlSplitPanel}
*/
@query("#atom_panel")
private accessor atomPanel: SlSplitPanel;
/**
* part of the tab panels split panel element, contains molecule carousel and molecule editor
*
* @private
* @type {SlSplitPanel}
*/
@query("#molecule_panel")
private accessor moleculePanel: SlSplitPanel;
@query("#smilesSwitch")
private accessor smilesSwitch: SlSwitch
/**
* color palet for pH indication
*
* @private
* @type {object}
*/
@property({ attribute: false })
private accessor pHColors: object = {
// *pride*
0: "#ee1c25",
1: "#f26724",
2: "#f9c511",
3: "#f5ed1c",
4: "#b5d333",
5: "#83c241",
6: "#33a94b",
7: "#33a94b",
8: "#22b46b",
9: "#0bb8b6",
10: "#4690cd",
11: "#3853a4",
12: "#5a51a2",
13: "#63459d",
14: "#462c83",
};
/**
* dictionary for german translation of all internal keywords
*
* @private
* @type {object}
*/
@property({ attribute: false })
private accessor dictionary: object = {
testtube: "Reagenzglas",
beaker: "Becherglas",
burette: "Bürette",
flask: "Kolben",
condenser: "Kühler",
separatingfunnel: "Scheidetrichter",
cable: "Kabel",
electrode: "Elektrode",
source: "Spannungsquelle",
voltmeter: "Voltmeter",
hydrogen: "Wasserstoff",
helium: "Helium",
lithium: "Lithium",
beryllium: "Beryllium",
boron: "Bor",
carbon: "Kohlenstoff",
nitrogen: "Stickstoff",
oxygen: "Sauerstoff",
fluorine: "Fluor",
neon: "Neon",
sodium: "Natrium",
magnesium: "Magnesium",
aluminum: "Aluminium",
silicon: "Silizium",
phosphorus: "Phosphor",
sulfur: "Schwefel",
chlorine: "Chlor",
argon: "Argon",
potassium: "Kalium",
calcium: "Calcium",
scandium: "Scandium",
titanium: "Titan",
vanadium: "Vanadium",
chromium: "Chrom",
manganese: "Mangan",
iron: "Eisen",
cobalt: "Cobalt",
nickel: "Nickel",
copper: "Kupfer",
zinc: "Zink",
gallium: "Gallium",
germanium: "Germanium",
arsenic: "Arsen",
selenium: "Selen",
bromine: "Brom",
krypton: "Krypton",
rubidium: "Rubidium",
strontium: "Strontium",
yttrium: "Yttrium",
zirconium: "Zirkonium",
niobium: "Niob",
molybdenum: "Molybdän",
technetium: "Technetium",
ruthenium: "Ruthenium",
rhodium: "Rhodium",
palladium: "Palladium",
silver: "Silber",
cadmium: "Cadmium",
indium: "Indium",
tin: "Zinn",
antimony: "Antimon",
tellurium: "Tellur",
iodine: "Iod",
xenon: "Xenon",
cesium: "Caesium",
barium: "Barium",
lanthanum: "Lanthan",
cerium: "Cer",
praseodymium: "Praseodym",
neodymium: "Neodym",
promethium: "Promethium",
samarium: "Samarium",
europium: "Europium",
gadolinium: "Gadolinium",
terbium: "Terbium",
dysprosium: "Dysprosium",
holmium: "Holmium",
erbium: "Erbium",
thulium: "Thulium",
ytterbium: "Ytterbium",
lutetium: "Lutetium",
hafnium: "Hafnium",
tantalum: "Tantal",
tungsten: "Wolfram",
rhenium: "Rhenium",
osmium: "Osmium",
iridium: "Iridium",
platinum: "Platin",
gold: "Gold",
mercury: "Quecksilber",
thallium: "Thallium",
lead: "Blei",
bismuth: "Wismut",
polonium: "Polonium",
astatine: "Astat",
radon: "Radon",
francium: "Francium",
radium: "Radium",
actinium: "Actinium",
thorium: "Thorium",
protactinium: "Protactinium",
uranium: "Uran",
neptunium: "Neptunium",
plutonium: "Plutonium",
americium: "Americium",
curium: "Curium",
berkelium: "Berkelium",
californium: "Californium",
einsteinium: "Einsteinium",
fermium: "Fermium",
mendelevium: "Mendelevium",
nobelium: "Nobelium",
lawrencium: "Lawrencium",
rutherfordium: "Rutherfordium",
dubnium: "Dubnium",
seaborgium: "Seaborgium",
bohrium: "Bohrium",
hassium: "Hassium",
meitnerium: "Meitnerium",
darmstadtium: "Darmstadtium",
roentgenium: "Roentgenium",
copernicium: "Copernicium",
nihonium: "Nihonium",
flerovium: "Flerovium",
moscovium: "Moscovium",
livermorium: "Livermorium",
tennessine: "Tenness",
oganesson: "Oganesson",
ununennium: "Ununennium",
"alkali metal": "Alkalimetall",
"alkaline earth metal": "Erdalkalimetall",
"transition metal": "Übergangsmetall",
"post transition metal": "Metall der Erdmetalle",
metalloid: "Halbmetall",
"polyatomic nonmetal": "Polyatomares Nichtmetall",
"diatomic nonmetal": "Diatomares Nichtmetall",
halogen: "Halogen",
"noble gas": "Edelgas",
lanthanide: "Lanthanoid",
actinide: "Actinoid",
ionic: "ionische Bindung",
acidbase: "Säure/Base Bindung",
covalent: "kovalente Bindung"
};
/**
* reflected data on available molecules
*
* @private
* @type {Array<object>}
*/
@property({
type: Array<object>,
reflect: true,
converter: {
fromAttribute: (value: string) => {
return JSON.parse(value);
},
toAttribute: (value: Array<object>) => {
return JSON.stringify(value);
},
},
hasChanged(newVal: Array<object>, oldVal: Array<object>) {
return newVal.length != oldVal.length;
},
})
private accessor availableMolecules: Array<object> = [
{
formula: "OH2",
atoms: ["oxygen", "hydrogen", "hydrogen"],
bonds: ["0-1:covalent:1", "0-2:covalent:1"],
},
];
/**
* reflected data on available mixtures
*
* @private
* @type {Array<object>}
*/
@property({
type: Array<object>,
reflect: true,
converter: {
fromAttribute: (value: string) => {
return JSON.parse(value);
},
toAttribute: (value: Array<object>) => {
return JSON.stringify(value);
},
},
hasChanged(newVal: Array<object>, oldVal: Array<object>) {
return newVal.length != oldVal.length;
},
})
private accessor availableMixtures: Array<object> = [
];
/**
* reflected data on available components
*
* @private
* @type {Array<object>}
*/
@property({
type: Array<object>,
reflect: true,
converter: {
fromAttribute: (value: string) => {
return JSON.parse(value);
},
toAttribute: (value: Array<object>) => {
return JSON.stringify(value);
},
},
hasChanged(newVal: Array<object>, oldVal: Array<object>) {
return newVal.length != oldVal.length;
},
})
private accessor availableComponents: Array<object> = [];
/**
* list of all sprites of available components
*
* @private
* @type {Array<[PIXI.Sprite, number]>}
*/
@property({ attribute: false })
private accessor avComponentsSprites: Array<[PIXI.Sprite, number]> = [];
/**
* bounding rectangle of the main canvas for relative positioning
*
* @private
* @type {DOMRect}
*/
@property({ attribute: false })
private accessor canvasBoundingRect: DOMRect;
/**
* list of functions to be iteratively called by the PixiJS ticker system
*
* @private
* @type {Array<any>}
*/
@property({ attribute: false })
private accessor tickerFunctionList: Array<any> = [];
/**
* list of current diagrams, contains charts and corresponding div containers
*
* @private
* @type {Array<any>}
*/
@property({ attribute: false })
private accessor diagramList: Array<any> = [];
/**
* database to store update intervals for ticker functions
*
* @private
* @type {object}
*/
@property({ attribute: false })
private accessor tickerTimeObj: object = {};
/**
* show diagram lines
*
* @private
* @type {boolean}
*/
@property({ attribute: false })
private accessor diagShowLine: boolean = false;
/**
* device has touch abilities
*
* @private
* @type {boolean}
*/
@property({ attribute: false })
private accessor hasTouch: boolean = false;
/**
* touch input mode (drag/click)
*
* @private
* @type {boolean}
*/
@property({ attribute: false })
private accessor dragToggle: boolean = true;
/**
* simulate mouseover event when clicking once on a component sprite (for touch use)
*
* @private
* @type {boolean}
*/
@property({ attribute: false })
private accessor touchHighlight: boolean = false;
/**
* show colored element symbols
*
* @private
* @type {number}
*/
@property({ attribute: false })
private accessor useColoredElements: number = 1;
/**
* currently dragged sprite object
*
* @private
* @type {*}
*/
@property({ attribute: false })
private accessor dragObject = undefined;
/**
* current mouse position
*
* @private
* @type {number[]}
*/
@property({ attribute: false })
private accessor mousePos: number[] = [0, 0];
/**
* seperate container element for the PixiJS Application
*
* @private
* @type {*}
*/
@property({ attribute: false })
private accessor pix_canvas = document.createElement("p_c");
/**
* the PixiJS Application
*
* @private
* @type {*}
*/
@property({ attribute: false })
private accessor app = null;
/**
* signalizes which datasets (avail. components/e-components/atoms) have already been loaded into their selects
*
* @private
* @type {boolean[]}
*/
@property({ attribute: false })
private accessor components_signal: boolean[] = [false, false, false];
/**
* which e-component info to show in the panel
*
* @private
* @type {boolean[]}
*/
@property({ attribute: false })
private accessor eCompPanelVisible: boolean[] = [false, false, false];
/**
* list of available electrode materials
*
* @private
* @type {{}}
*/
@property({ attribute: false })
private accessor electrodeMaterials = [];
/**
* list of all current component objects
*
* @private
* @type {chemlib.ChemComponent[]}
*/
@property({ attribute: false })
private accessor allChemComponents: chemlib.ChemComponent[] = [];
/**
* list of all current e-component objects
*
* @private
* @type {chemlib.ElectricComponent[]}
*/
@property({ attribute: false })
private accessor allEComponents: chemlib.ElectricComponent[] = [];
/**
* list of currently filtered element categories
*
* @private
* @type {string[]}
*/
@property({ attribute: false })
private accessor filterCategories: string[] = [];
/**
* the currently selected component/e-component object
*
* @private
* @type {*}
*/
@property({ attribute: false })
private accessor curr_obj: any = null;
/**
* the currently modified molecule object
*
* @private
* @type {chemlib.Molecule}
*/
@property({ attribute: false })
private accessor curr_mol: chemlib.Molecule = null;
/**
* the name of the current object
*
* @private
* @type {string}
*/
@property({ attribute: false })
private accessor curr_name: string = "";
/**
* the index of the sprite of the selected object in the PixiJS stage
*
* @private
* @type {number}
*/
@property({ attribute: false })
private accessor curr_stage_index: number = -1;
/**
* the current panel variant
*
* @private
* @type {string}
*/
@property({ type: String, reflect: true })
private accessor panelVariant: string = "1";
/**
* if an action is permitted or aborted
*
* @private
* @type {number}
*/
@property({ attribute: false })
private accessor confirmAction: number = 0; //0-pending,1-true,2-false
/**
* the random pseudonym of the current session
*
* @private
* @type {string}
*/
@property({ attribute: false })
private accessor RndName: string = uniqueNamesGenerator({
dictionaries: [adjectives, colors, animals],
});
/**
* reflected list of disabled compoents/e-components in the student view
*
* @private
* @type {string[]}
*/
@property({
type: Array<String>,
reflect: true,
converter: {
fromAttribute: (value: string, type: string[]) => {
return value.split(",");
},
toAttribute: (value: string[], type: string) => {
return value.toString();
},
},
})
private accessor invComponents: string[] = [];
/**
* reflected, if xAPI statements should be sent
*
* @private
* @type {boolean}
*/
@property({
type: Boolean,
reflect: true,
converter: {
fromAttribute: (value: string, type: boolean) => {
if (value === "true") {
return true;
}
return false;
},
toAttribute: (value: boolean, type: string) => {
if (value) {
return "true";
}
return "false";
},
},
})
private accessor enablexAPI: boolean = false;
/**
* reflected, the current xAPI key
*
* @private
* @type {string}
*/
@property({ type: String, reflect: true })
private accessor xAPIKey: string =
"";
/**
* SmilesDrawer instance to draw molecules in a 100x100 canvas
*
* @private
* @type {SmilesDrawer}
*/
@property({attribute: false})
private accessor smilesDrawer100: SmilesDrawer = null
/**
* SmilesDrawer instance to draw molecules in a 150x150 canvas
*
* @private
* @type {SmilesDrawer}
*/
@property({attribute: false})
private accessor smilesDrawer150: SmilesDrawer = null
/**
* wether to use the SmilesDrawer package
*
* @private
* @type {number}
*/
@property({type: Number, reflect: true})
private accessor newSmilesDrawer: number = 1
/**
* Creates an instance of ChemLab.
*
* @constructor
*/
constructor() {
super();
console.log(SmilesDrawer)
this.smilesDrawer100 = new SmilesDrawer.SvgDrawer({ width: 100, height: 100, explicitHydrogens: true, compactDrawing: false, terminalCarbons: true});
this.smilesDrawer150 = new SmilesDrawer.SvgDrawer({ width: 150, height: 150, explicitHydrogens: true, compactDrawing: false });
this.hasTouch = navigator.maxTouchPoints > 0;
this.addEventListener("fullscreenchange", () => {
this.containerDiv.style.width = this.clientWidth + "px";
this.containerDiv.style.height = this.clientHeight + "px";
this.app.resizeTo = this.containerDiv;
this.app.resize();
this.requestUpdate();
});
}
/**
* early fix for webwriter focus issues
*
* @static
* @type {*}
*/
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
/**
* all used custom elements, mainly shoelace elements and the webwriter helpOverlay
*
* @static
* @readonly
* @type {{ "sl-details": any; "sl-button": any; "sl-icon-button": any; "sl-icon": any; "sl-badge": any; "sl-button-group": any; "sl-tab": any; "sl-tab-group": any; "sl-tab-panel": any; "sl-split-panel": any; "sl-tag": any; "sl-divider": any; ... 20 more ...; "webwriter-helppopup": any; }}
*/
static get scopedElements() {
return {
"sl-details": SlDetails,
"sl-button": SlButton,
"sl-icon-button": SlIconButton,
"sl-icon": SlIcon,
"sl-badge": SlBadge,
"sl-button-group": SlButtonGroup,
"sl-tab": SlTab,
"sl-tab-group": SlTabGroup,
"sl-tab-panel": SlTabPanel,
"sl-split-panel": SlSplitPanel,
"sl-tag": SlTag,
"sl-divider": SlDivider,
"sl-range": SlRange,
"sl-select": SlSelect,
"sl-option": SlOption,
"sl-rating": SlRating,
"sl-input": SlInput,
"sl-menu": SlMenu,
"sl-menu-item": SlMenuItem,
"sl-menu-label": SlMenuLabel,
"sl-alert": SlAlert,
"sl-carousel": SlCarousel,
"sl-carousel-item": SlCarouselItem,
"sl-popup": SlPopup,
"sl-card": SlCard,
"sl-checkbox": SlCheckbox,
"sl-drawer": SlDrawer,
"sl-tooltip": SlTooltip,
"sl-dialog": SlDialog,
"sl-radio": SlRadio,
"sl-switch": SlSwitch,
"sl-radio-group": SlRadioGroup,
"webwriter-helpoverlay": HelpOverlay,
"webwriter-helppopup": HelpPopup,
};
}
connectedCallback(): void {
super.connectedCallback();
}
/**
* loads canvas and initial state of the widget upon loading, initializes ticker
*
* @protected
* @param {(PropertyValueMap<any> | Map<PropertyKey, unknown>)} _changedProperties
*/
protected firstUpdated(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
this.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
super.firstUpdated(_changedProperties);
this._canvas_init();
this.focus();
this.canvasBoundingRect = this.containerDiv.getBoundingClientRect();
if (this.hasTouch) {
let changeInput: HTMLButtonElement = this.shadowRoot.getElementById(
"changeInput"
) as HTMLButtonElement;
let toggleDrag: HTMLButtonElement = this.shadowRoot.getElementById(
"toggleDrag"
) as HTMLButtonElement;
changeInput.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="4"><path d="M12.566 26.182Q10 27.941 10 32c0 4.06 4.975 11 9.462 11h11.48C35.332 43 38 39.15 38 36.06V23.01a3 3 0 0 0-3-3h-.01A2.99 2.99 0 0 0 32 23"/><path d="M13.981 28.445V8.005a3 3 0 0 1 3.006-2.997a3.014 3.014 0 0 1 3.006 3.015v15.569"/><path stroke-linejoin="round" d="M19.993 23.008v-3.992a3.016 3.016 0 0 1 6.03 0v3.992"/><path stroke-linejoin="round" d="M26 22.716v-2.713a3 3 0 0 1 6 0v3"/></g></svg>
`;
toggleDrag.style.visibility = "visible";
}
setAnimation(this.dialogAlert, "alert.show", {
keyframes: [{ opacity: "0" }, { opacity: "1" }],
options: {
duration: 50,
},
});
setAnimation(this.dialogAlert, "alert.hide", {
keyframes: [{ opacity: "1" }, { opacity: "0" }],
options: {
duration: 50,
},
});
let bg_sprite = PIXI.Sprite.from(bg_svg);
bg_sprite.name = "bg_sprite";
bg_sprite.anchor.set(0);
bg_sprite.x = 0;
bg_sprite.y = 0;
bg_sprite.width = this.containerDiv.clientWidth;
bg_sprite.height = this.containerDiv.clientHeight;
bg_sprite.eventMode = "none";
this.app.stage.addChild(bg_sprite);
this.app.ticker.add(() => {
let indexCount: number = 0;
while (typeof this.tickerFunctionList[indexCount] != "undefined") {
this.tickerFunctionList[indexCount][1].apply(
this,
this.tickerFunctionList[indexCount][2]
);
indexCount += 1;
}
});
this._pushTickerFunction(
this._updateTickerPopup.name,
this._updateTickerPopup,
[]
);
this._pushTickerFunction(
this._updateCablePos.name,
this._updateCablePos,
[]
);
this._pushTickerFunction(
this._updateSpriteTextures.name,
this._updateSpriteTextures,
[]
);
this._listComponents();
this._listEComponents();
this.invCompSelect.setAttribute(
"value",
this.invComponents.toString().replace(",", " ")
);
this.filterComponents();
this._listMolecules();
this._listMixtures();
this._listMolConfig();
this._listMixConfig();
this._loadComponents();
this._listCompConfig();
this.panelVariantOptions.value = this.panelVariant;
this._changePanelVariant();
if (!this.hasAttribute("contenteditable")) {
this.shadowRoot.getElementById("teacherContext").remove();
} else {
this.shadowRoot.getElementById("studentContext").remove();
this.settings_drawer.innerHTML = "neues Molekül";
this._newMol();
this._listAtoms();
this.moleculePanel.position = 0;
this.shadowRoot.getElementById("addToElectrode").style.visibility =
"hidden";
this.molAmount.style.visibility = "hidden";
this.molSelect.style.visibility = "hidden";
this.shadowRoot.getElementById("comp_panel").style.display = "";
this.shadowRoot.getElementById("ecomp_panel").style.display =
"none";
this.settings_drawer.removeAttribute("disabled");
// this.curr_mol._bondTypes.forEach((key) => {
// if(key in this.dictionary && key!= "hydrogen"){
// // @ts-ignore
// let bondType: SlOption = this.shadowRoot.createElement(
// "sl-option"
// ) as SlOption;
// bondType.innerHTML = this.dictionary[key];
// bondType.value = key;
// this.bond_sel_3.appendChild(bondType);
// }
// });
}
this.smilesSwitch.checked = this.newSmilesDrawer===1
this._sendxAPiStatement("registered", "chemlab");
this._emitAlert(
"klicke zum Starten in das Fenster!",
"success",
true,
10000
);
}
/**
* puts a new function in the ticker list, creates new diagram if neccessary
*
* @private
* @param {string} name - the name of the function
* @param {Function} fn - the function reference
* @param {Array<any>} args - additional arguments of the function
*/
private _pushTickerFunction(
name: string,
fn: Function,
args: Array<any>
): void {
this.tickerFunctionList.push([name, fn, args]);
this.tickerTimeObj[name] = 0;
// console.log(this.tickerTimeObj);
if (name.includes("transfer") || name.includes("echem")) {
for (let i = 0; i < this.diagramList.length; i++) {
if (
(this.diagramList[i][1] as HTMLDivElement).children[0]
.id === name
) {
return;
}
}
// @ts-ignore
let canvasBundle: HTMLDivElement = this.shadowRoot.createElement(
"div"
) as HTMLDivElement;
canvasBundle.style.borderTopStyle = "solid";
canvasBundle.style.borderTopWidth = "2px";
canvasBundle.style.borderTopColor = "#e5e5e5";
canvasBundle.style.marginBlock = "20px";
// @ts-ignore
let canvasDiv: HTMLDivElement = this.shadowRoot.createElement(
"div"
) as HTMLDivElement;
canvasDiv.style.width = "100%";
canvasDiv.style.maxWidth = "500px";
canvasDiv.style.height = "400px";
// @ts-ignore
let canvas: HTMLCanvasElement = this.shadowRoot.createElement(
"canvas"
) as HTMLCanvasElement;
canvas.id = name;
// @ts-ignore
let canvasHeading: HTMLHeadingElement = this.shadowRoot.createElement("h5") as HTMLHeadingElement;
canvasHeading.innerText =
this.diagramList.length +
": " +
(name.includes("transfer")
? "Titration"
: name.includes("source")
? "Elektrolyse"
: "Galvanische Zelle");
canvasDiv.appendChild(canvas);
// @ts-ignore
let canvasHeadingContainer: HTMLDivElement = this.shadowRoot.createElement("div") as HTMLDivElement;
canvasHeadingContainer.style.display = "flex";
canvasHeadingContainer.style.alignItems = "baseline";
// @ts-ignore
let trashButton: HTMLButtonElement = this.shadowRoot.createElement(
"button"
) as HTMLButtonElement;
trashButton.innerHTML += `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16"><path fill="#d10000" d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5M8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5m3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0"/></svg>`;
trashButton.onclick = (event) => {
this.diagramList.forEach((diag) => {
if ((diag[1] as HTMLDivElement).children[0].id === name) {
this.diagramList.splice(diag, 1);
return;
}
});
canvasBundle.remove();
this._sendxAPiStatement("removed", "diagram-" + name);
};
canvasHeadingContainer.appendChild(canvasHeading);
canvasHeadingContainer.appendChild(trashButton);
canvasBundle.appendChild(canvasHeadingContainer);
canvasBundle.appendChild(canvasDiv);
this.canvasContainer.appendChild(canvasBundle);
let chartLabel: string;
let chartTitleX: string;
let chartTitleY: string;
if (name.includes("transfer")) {
chartLabel = "pH";
chartTitleX = "Volumen [ml]";
chartTitleY = "pH Wert";
}
if (name.includes("echem")) {
if (name.includes("source")) {
chartLabel = "n";
chartTitleX = "Zeit [s]";
chartTitleY = "umgesetzte Stoffmenge [mol]";
}
if (name.includes("voltmeter")) {
chartLabel = "Spannung";
chartTitleX = "Zeit [s]";
chartTitleY = "Zellspannung [V]";
}
}
let chart = new Chart(canvas, {
type: "line",
data: {
labels: [],
datasets: [],
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
beginAtZero: true,
title: {
text: chartTitleX,
display: true,
},
grid: {
drawOnChartArea: false,
},
},
y: {
beginAtZero: true,
title: {
text: chartTitleY,
display: true,
},
grid: {
drawOnChartArea: false,
},
},
},
},
});
chart.data.datasets.push({
label: chartLabel,
data: [],
tension: 0.4,
showLine: this.diagShowLine,
});
if (name.includes("source")) {
chart.data.datasets.push({
label: chartLabel,
data: [],
tension: 0.4,
showLine: this.diagShowLine,
});
let electrodes: chemlib.ElectricComponent[] =
chemlib.ComponentManager.isCircuit(
args[1] as chemlib.ElectricComponent
);
chart.data.datasets[0].label =
"Stoffmenge " +
electrodes[0]._material._atoms[0].getSymbol() +
" Ionen [mol]";
chart.data.datasets[1].label =
"Stoffmenge " +
electrodes[1]._material._atoms[0].getSymbol() +
" Ionen [mol]";
}
this.diagramList.push([chart, canvasDiv]);
this._sendxAPiStatement("created", "diagram-" + name);
}
}
/**
* removes a ticker function from the list
*
* @private
* @param {string} name - the name of the function
*/
private _remTickerFunction(name: string): void {
let toRemove: number = -1;
for (let i = 0; i < this.tickerFunctionList.length; i++) {
if (this.tickerFunctionList[i][0] === name) {
toRemove = i;
}
}
if (toRemove > -1) {
this.tickerFunctionList.splice(toRemove, 1);
}
}
/**
* ticker function to update popup contents regulary
*
* @private
*/
private _updateTickerPopup(): void {
if (
this.contextPopup.active &&
(this.shadowRoot.getElementById("contextInfoBox") as HTMLDivElement)
.style.visibility === "visible"
) {
this.tickerTimeObj[this._updateTickerPopup.name] += 1;
// @ts-ignore
if (
this.tickerTimeObj[this._updateTickerPopup.name] >=
Math.round(
0.5 *
chemlib.Simulation._simTimeStep *
(this.app as PIXI.Application).ticker.FPS
)
) {
this.tickerTimeObj[this._updateTickerPopup.name] = 0;
this._updatePopup();
}
}
}
/**
* ticker function to update sprite textures according to the components content
*
* @private
*/
private _updateSpriteTextures(): void {
this.app.stage.children.forEach((sprite: PIXI.Sprite) => {
let name: string = sprite.name
.split(":")[0]
.replace(
sprite.name.split(":")[0][0],
sprite.name.split(":")[0][0].toUpperCase()
);
let id: number = Number.parseInt(sprite.name.split(":")[1]);
if (Object.keys(chemlib.ComponentType).includes(name)) {
this.allChemComponents.forEach((comp) => {
if (comp._type === name.toLowerCase() && comp._id === id) {
let tex: undefined;
if (comp._content._activeMolecules.length > 0) {
if (comp._content._isSolution) {
switch (comp._type) {
case "beaker":
tex = beaker_fl_svg;
break;
case "testtube":
tex = testtube_fl_svg;
break;
case "flask":
tex = flask_fl_svg;
break;
case "burette":
tex = burette_fl_svg;
break;
default:
break;
}
sprite.texture = PIXI.Texture.from(tex);
} else {
switch (comp._type) {
case "beaker":
tex = beaker_fs_svg;
break;
case "testtube":
tex = testtube_fs_svg;
break;
case "flask":
tex = flask_fs_svg;
break;
case "burette":
tex = burette_fs_svg;
break;
default:
break;
}
sprite.texture = PIXI.Texture.from(tex);
}
} else {
switch (comp._type) {
case "beaker":
tex = beaker_svg;
break;
case "testtube":
tex = testtube_svg;
break;
case "flask":
tex = flask_svg;
break;
case "burette":
tex = burette_svg;
break;
default:
break;
}
sprite.texture = PIXI.Texture.from(tex);
}
}
});
}
});
}
/**
* ticker function to update cables and their positions
*
* @private
*/
private _updateCablePos(): void {
let index0: number;
this.app.stage.children.forEach((sprite: PIXI.Sprite) => {
index0 = this.app.stage.children.indexOf(sprite);
if (sprite.name.includes("cable")) {
let id: number = Number.parseInt(sprite.name.split(":")[1]);
this.allEComponents.forEach((eComp) => {
if (
eComp._type === chemlib.EComponentType.Cable &&
eComp._id === id
) {
let con1: chemlib.ElectricComponent | undefined =
eComp._connections[0];
let con2: chemlib.ElectricComponent | undefined =
eComp._connections[1];
if (
typeof con1 != "undefined" &&
typeof con2 != "undefined"
) {
let pos1, pos2: number[];
let index1, index2: number;
this.app.stage.children.forEach((conSprite) => {
if (
conSprite.name.includes(
con1._type + ":" + con1._id
)
) {
index1 =
this.app.stage.children.indexOf(
conSprite
);
pos1 = [conSprite.x, conSprite.y];
}
if (
conSprite.name.includes(
con2._type + ":" + con2._id
)
) {
index2 =
this.app.stage.children.indexOf(
conSprite
);
pos2 = [conSprite.x, conSprite.y];
}
});
let minIndex: number = Math.min(
index0,
index1,
index2
);
if (minIndex != index0) {
let buffer: any =
this.app.stage.children[minIndex];
this.app.stage.children[minIndex] = sprite;
this.app.stage.children[index0] = buffer;
}
sprite.height = Math.sqrt(
Math.pow(pos1[0] - pos2[0], 2) +
Math.pow(pos1[1] - pos2[1], 2)
);
sprite.x = (pos1[0] + pos2[0]) / 2;
sprite.y = (pos1[1] + pos2[1]) / 2;
sprite.rotation =
Math.atan(
(pos1[1] - pos2[1]) / (pos1[0] - pos2[0])
) + HALF_PI;
} else {
this._getObject(sprite);
this._removeSprite();
}
}
});
}
});
}
/**
* ticker function for titraton experiments, transfers content to another component dynamically on correct sprite positioning and updates the diagram
*
* @private
* @param {PIXI.Sprite} currSprite - the sprite of the burette
* @param {chemlib.ChemComponent} source - the chemlib object of the burette
*/
private _updateTickerTransfer(
currSprite: PIXI.Sprite,
source: chemlib.ChemComponent
): void {
let keyName =
"transfer" +
source._type +
":" +
source._id +
this.app.stage.children.indexOf(currSprite);
this.tickerTimeObj[keyName] += 1;
if (
this.tickerTimeObj[keyName] >=
Math.round(
chemlib.Simulation._simTimeStep *
(this.app as PIXI.Application).ticker.FPS
)
) {
this.tickerTimeObj[keyName] = 0;
if (source._content._activeMolecules.length > 0) {
let destSprite: PIXI.Sprite = undefined;
this.app.stage.children.forEach((sprite) => {
if (sprite != currSprite) {
if (
sprite.y - sprite.height / 2 >
currSprite.y + currSprite.height / 2 &&
(sprite.x < currSprite.x + currSprite.width / 2 ||
sprite.x > currSprite.x - currSprite.width / 2)
) {
if (typeof destSprite === "undefined") {
destSprite = sprite;
} else {
if (destSprite.y - sprite.y > 0) {
destSprite = sprite;
}
}
}
}
});
let destObj: chemlib.ChemComponent = undefined;
this.allChemComponents.forEach((comp) => {
if (typeof destSprite != "undefined") {
if (
destSprite.name.split(":")[0] === comp._type &&
Number.parseInt(destSprite.name.split(":")[1]) ===
comp._id
) {
destObj = comp;
}
}
});
if (typeof destObj != "undefined") {
if (source._content._activeMolecules.length > 0) {
source.transferContents({}, destObj);
for (let i = 0; i < this.diagramList.length; i++) {
if (
(
this.diagramList[i][1] as HTMLDivElement
).children[0].id.includes(
source._type + ":" + source._id
)
) {
let chart: Chart = this.diagramList[i][0];
chart.data.labels.push(
(chart.data.labels.length > 0
? Number.parseFloat(
chart.data.labels[
chart.data.labels.length - 1
] as string
)
: 0) +
source._outVol * 1000
);
chart.data.datasets.forEach((dataset) => {
dataset.data.push(
chemlib.Simulation.getpH(
destObj._content
)
);
});
chart.update();
}
}
}
}
}
}
}
/**
* ticker function for e-chem experiments, checks circuit and updates diagram
*
* @private
* @param {PIXI.Sprite} currSprite - the sprite of the e-component
* @param {chemlib.ElectricComponent} source - the e-component object
*/
private _updateTickerEChem(
currSprite: PIXI.Sprite,
source: chemlib.ElectricComponent
): void {
let keyName =
"echem" +
source._type +
":" +
source._id +
this.app.stage.children.indexOf(currSprite);
this.tickerTimeObj[keyName] += 1;
if (
this.tickerTimeObj[keyName] >=
Math.round(
chemlib.Simulation._simTimeStep *
(this.app as PIXI.Application).ticker.FPS
)
) {
this.tickerTimeObj[keyName] = 0;
let circuitComps: Array<chemlib.ElectricComponent> =
chemlib.ComponentManager.isCircuit(source);
if (circuitComps.length != 0) {
chemlib.Simulation.sEChem(
circuitComps[0],
circuitComps[1],
circuitComps[2]
);
for (let i = 0; i < this.diagramList.length; i++) {
if (
(
this.diagramList[i][1] as HTMLDivElement
).children[0].id.includes(
source._type + ":" + source._id
)
) {
let chart: Chart = this.diagramList[i][0];
chart.data.labels.push(
(chart.data.labels.length > 0
? Number.parseFloat(
chart.data.labels[
chart.data.labels.length - 1
] as string
)
: 0) + 1
);
chart.data.datasets.forEach((dataset) => {
if (
source._type ===
chemlib.EComponentType.Voltmeter
) {
dataset.data.push(source._visibleVoltage);
} else {
let datasetId: number =
chart.data.datasets.indexOf(dataset);
circuitComps[
datasetId
]._connectedMix._activeMolecules.forEach(
(mol) => {
// console.log(
// mol._atomCount,
// mol._totalCharge,
// mol._atoms[0].getName(),
// circuitComps[
// datasetId
// ]._material._atoms[0].getName(),
// circuitComps[datasetId]
// ._connectedMix._molProperties
// );
if (
mol._atomCount === 1 &&
mol._totalCharge != 0 &&
mol._atoms[0].getName() ===
circuitComps[
datasetId
]._material._atoms[0].getName()
) {
dataset.data.push(
circuitComps[datasetId]
._connectedMix
._molProperties[
mol._id + "n"
]
);
}
}
);
}
});
chart.update();
}
}
}
}
}
/**
* adds a new component
*
* @private
* @param {boolean} e - compoent is an electric component
* @param {string} type - the component type
*/
private _addComponent(e: boolean, type: string): void {
let component: chemlib.ChemComponent | chemlib.ElectricComponent = null;
if (!e) {
component = chemlib.ComponentManager.newChemComponent(
type.toLowerCase() as chemlib.ComponentType
);
this.allChemComponents.push(component);
} else {
component = chemlib.ComponentManager.newElectricComponent(
type.toLowerCase() as chemlib.EComponentType
);
this.allEComponents.push(component);
}
this._addSprite(component._type + ":" + component._id);
}
/**
* adds a new sprite to the canvas
*
* @private
* @param {string} info - a string containing the name of the object and its id
*/
private _addSprite(info: string): void {
let spriteHeight: number = 0;
let spriteWidth: number = 0;
let spriteTexture: any = null;
switch (info.split(":")[0]) {
case "beaker":
spriteHeight = 130;
spriteWidth = 100;
spriteTexture =