// Import the Lasal Runtime base element into your component
// Import SigPolymer to use Polymer methods: html, afterNextRender, beforeNextRender, debouncer, timeOut
// or if your component's getter template() returns a HTML literal
import {
	LasalRuntimeSigElement,
	SigPolymer,
} from "../../sigmatek/sig-element/sig-element.js";
/****************************************************************************************************
 * @class LasalRuntimeSigControlCylinderSelect
 * @extends {LasalRuntimeSigElement}
 ****************************************************************************************************/
export class LasalRuntimeSigControlCylinderSelect extends LasalRuntimeSigElement {
	/****************************************************************************************************
	 * When the element is loaded, importMeta() assigns reference to the path from
	 * which an element was imported to its importPath property.
	 * Relative URLs in styles are automatically re-written to be relative to the importPath property.
	 * You can use [[importPath]] in your component to load url resources
	 ****************************************************************************************************/
	static get importMeta() {
		return import.meta;
	}

	/****************************************************************************************************
	 * Return the component style and template as a template literal
	 ****************************************************************************************************/
	static get template() {
		return SigPolymer.html`
        <style include="sig-element-css">
            :host {
                display: block;
                position: absolute;
                /*To position Text outside of the container */
                overflow: visible;
                font-size: 14px;
            }

            .sig-control-cylinder-select {
                position: relative;
                box-sizing: border-box;
                height: 100%;
                width: 100%;
                background-color: var(--theme-sig-control-cylinder-select-background-color, inherit);
                background-image: var(--theme-sig-control-cylinder-select-background-image, 'none');
                background-size: var(--theme-sig-control-cylinder-select-background-size, contain);
                background-position: var(--theme-sig-control-cylinder-select-background-position, initial);     /* variable used in date / time picker */
                background-repeat: var(--theme-sig-control-cylinder-select-background-repeat, no-repeat);       /* variable used in date / time picker */
                border-width: var(--theme-sig-control-cylinder-select-border-width, 0px);
                border-style: var(--theme-sig-control-cylinder-select-border-style, none);
                border-color: var(--theme-sig-control-cylinder-select-border-color, inherit);
                border-radius: var(--theme-sig-control-cylinder-select-border-radius, 3px);
            }

            #listContainer {
                height: 100%;
                width: 100%;
            }
            :host([displaytype="round"]) #listContainer {
                transform-style: preserve-3d;
            }

            .select-wrap {
                position: relative;
                height: 100%;
                width: 100%;
                text-align: center;
                overflow: hidden;
                color: var(--theme-sig-control-cylinder-select-select-wrap-color, inherit);
            }

            .select-wrap:before,
            .select-wrap:after {
                position: absolute;
                z-index: 1;
                display: block;
                content: "";
                width: 100%;
                height: 50%;
            }

            .select-wrap .select-options {
                position: relative;
                top: 50%;
                left: 0;
                width: 100%;
                height: 0;
                margin: 0 auto;
                display: block;
                list-style: none;
                padding: 0px;
            }

            :host([displaytype="round"]) .select-wrap .select-options {
                position: absolute;
                transform-style: preserve-3d;
                transform: translateZ(-150px) rotateX(0deg);
                -webkit-font-smoothing: subpixel-antialiased;
            }

            .select-wrap .select-options .select-option {     
                top: 0;
                left: 0;
                width: 100%;
                height: 50px;
            }

            :host([displaytype="round"]) .select-wrap .select-options .select-option{
                position: absolute;
                -webkit-font-smoothing: subpixel-antialiased;
            }

            .highlight {
                position: absolute;
                box-sizing: border-box;
                top: 50%;
                width: 100%;
                background-color: var(--theme-sig-control-cylinder-select-highlight-background-color, inherit);
                
                border-color: var(--theme-sig-control-cylinder-select-highlight-border-color, inherit);
                border-width: var(--theme-sig-control-cylinder-select-highlight-border-width, 0px);
                border-style: var(--theme-sig-control-cylinder-select-highlight-border-style, none);
                
                border-top-width: var(--theme-sig-control-cylinder-select-highlight-border-top-width, 0px);
                border-top-style: var(--theme-sig-control-cylinder-select-highlight-border-top-style, solid);
                border-top-color: var(--theme-sig-control-cylinder-select-highlight-border-top-color, inherit);

                border-bottom-width: var(--theme-sig-control-cylinder-select-highlight-border-bottom-width, 0px);
                border-bottom-style: var(--theme-sig-control-cylinder-select-highlight-border-bottom-style, solid);
                border-bottom-color: var(--theme-sig-control-cylinder-select-highlight-border-bottom-color, inherit);

                border-radius: var(--theme-sig-control-cylinder-select-highlight-border-radius, 3px);
                overflow: hidden;
                color: var(--theme-sig-control-cylinder-select-highlight-color, inherit);
            }

            :host([displaytpye="flat"]) .highlight {
                position: relative;
            }

            .highlight-list {
                position: absolute;
                width: 100%;
                list-style: none;
                padding: 0px;
                margin: 0;
                transform: translateZ(1px);
            }
            :host([displaytype="flat"]) .highlight-list {
                position: relative;
            }

            .navButton {
                height: var(--theme-sig-control-cylinder-select-button-height, 10px);
                width: 100%;
                background-color: var(--theme-sig-control-cylinder-select-button-background-color, transparent);        /* variable used in date / time picker */
                background-image: var(--theme-sig-control-cylinder-select-button-background-image, none);
                background-size: contain;
                background-repeat: no-repeat;
                background-position: center;
            }

            #navButtonUp {
                background-image: var(--theme-sig-control-cylinder-select-button-background-image, url('./arrowUp.svg'));
            }

            #navButtonDown {
                 background-image: var(--theme-sig-control-cylinder-select-button-down-background-image, url('./arrowDown.svg'));
            }

            #listContainer {
                height: 100%;
            }

            :host([hasnavbuttons]) #listContainer {
                height: calc(100% - 2 * var(--theme-sig-control-cylinder-select-button-height, 10px));
            }

            :host(:not([hasnavbuttons])) .navButton {
                display: none;
            }

            .hide {
                visibility: hidden;
            }

            .select-wrap .select-options .select-option {
                top: var(--theme-sig-control-cylinder-select-select-option-top);
                height: var(--theme-sig-control-cylinder-select-select-option-height);
                line-height: var(--theme-sig-control-cylinder-select-select-option-line-height);
            }

            .highlight {
                top: var(--theme-sig-control-cylinder-select-highlight-top);
                height: var(--theme-sig-control-cylinder-select-highlight-height);
                line-height: var(--theme-sig-control-cylinder-select-highlight-line-height);
            }

            .highlight-item {
                height: var(--theme-sig-control-cylinder-select-highlight-item-height);
            }

            .unknown-value {
                position: absolute;
                width: 100%;
                top: 50%;
                transform: translateY(-50%);
            }
        </style>
        <div class="sig-control-cylinder-select clearfix">
            <template is="dom-if" if="[[hasnavbuttons]]">
                <div class="navButton" id="navButtonUp" on-tap="_navUpPointerdown"></div>
            </template>
            <div id="listContainer"></div>
            <template is="dom-if" if="[[hasnavbuttons]]">
                <div class="navButton" id="navButtonDown" on-tap="_navDownPointerdown"></div>
            </template>
        </div>
        `;
	}

	/****************************************************************************************************
	 * define the custom tag name
	 *
	 * @readonly
	 * @static
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	static get is() {
		return "sig-control-cylinder-select";
	}

	/****************************************************************************************************
	 * define the main porperties of the component
	 * reflected property names should always be lower case so it can be used directly on the element as an attribute
	 *
	 * @readonly
	 * @static
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	static get properties() {
		return {
			value: {
				type: Number,
				value: null,
				observer: "_valueChanged",
				notify: true,
			},
			hasnavbuttons: {
				type: Boolean,
				value: false,
				reflectToAttribute: true,
				observer: "_redraw"
			},
			listfont: {
				type: Object,
				value: () => {
					return {};
				},
			},
			highlightfont: {
				type: Object,
				value: () => {
					return {};
				},
			},
			/**
			 * Extra font objects, to set the fonts
			 * when there is no possibility to set the variable to a scheme
			 */
			listfontobj: {
				type: Object,
				value: () => {
					return {};
				},
				observer: "_updateListFont",
			},
			highlightfontobj: {
				type: Object,
				value: () => {
					return {};
				},
				observer: "_updateHighlightFont",
			},
			min: {
				type: Number,
				value: 0,
				observer: "_minChanged",
			},
			max: {
				type: Number,
				value: 10,
				observer: "_maxChanged",
			},
			stepsize: {
				type: Number,
				value: 1,
				observer: "_stepsizeChanged",
			},
			count: {
				type: Number,
				value: 0,
			},
			shown: {
				type: Number,
				value: 5,
				observer: "_redraw",
			},
			type: {
				type: String,
				value: "infinite",
				observer: "_redraw",
			},
			source: {
				type: Array,
				value: () => {
					return [];
				},
			},
			dontreloadsource: {
				type: Boolean,
				value: false,
			},
			sourcetextlist: {
				type: Object,
				value: () => {
					return {};
				},
			},
			sensitivity: {
				type: Number,
				value: 1.5,
			},
			displaytype: {
				type: String,
				value: "flat",
				reflectToAttribute: true,
				observer: "_redraw",
			},
		};
	}

	/****************************************************************************************************
	 * Called when the element has been created,
	 * but before property values are set and local DOM is initialized.
	 *
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	constructor() {
		super();

		this.propApi = window.sigApi.properties;
		this.smApi = window.sigApi.stationManager;

		this.rendered = false;
		this.queue = [];
		this.queueBusy = false;

		this.easing = {
			easeOutCubic: function (pos) {
				return Math.pow(pos - 1, 3) + 1;
			},
			easeOutQuart: function (pos) {
				return -(Math.pow(pos - 1, 4) - 1);
			},
		};

		//save default values to use as preview in design mode, if a datapoint is configured
		this.previewMin = this.min;
		this.previewMax = this.max;

		this.halfCount = 0;
		this.queaterCount = 0;
		this.a = 0;
		this.minV = 0;
		this.selected = null;
		this.exceedA = 0;
		this.moveT = 0;
		this.moving = false;
		this.elems = {};
		this.events = {
			touchstart: null,
			touchmove: null,
			touchend: null,
		};
		this.itemHeightCalc = 0;
		this.itemHeight = 0;
		this.itemAngleCalc = 0;
		this.itemAngle = 0;
		this.radius = 0;
		this.scroll = 0;

		this.touchstart = false;
		this.eventListenersAdded = false;

		this.listContainerOffsetHeight = undefined;

		this.touchData = {
			startY: 0,
			yArr: [],
		};

		this.unit = undefined;

		// add required properties
		this.sigAddRequiredProperty("value");
		this.sigAddRequiredProperty("hasnavbuttons");
		this.sigAddRequiredProperty("min");
		this.sigAddRequiredProperty("max");
		this.sigAddRequiredProperty("stepsize");
	}

	/****************************************************************************************************
	 * Observer for the min property
	 *
	 * @param {*} newval
	 * @param {*} oldval
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _minChanged(newval, oldval) {
		if (!this.rendered) {
			this.queue.push({
				method: "_minChanged",
				params: [newval, oldval, false],
			});
		}
		else {
			//redraw component
			this._render();			
		}
	}

	/****************************************************************************************************
	 * Observer for the max property
	 *
	 * @param {*} newval
	 * @param {*} oldval
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _maxChanged(newval, oldval) {
		if (!this.rendered) {
			this.queue.push({
				method: "_maxChanged",
				params: [newval, oldval, false],
			});
		}
		else {
			//redraw component
			this._render();			
		}
	}

	/****************************************************************************************************
	 * Observer for the stepsize property
	 *
	 * @param {*} newval
	 * @param {*} oldval
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_stepsizeChanged(newval, oldval) {
		if (!this.rendered) {
			this.queue.push({
				method: "_stepsizeChanged",
				params: [newval, oldval, false],
			});
		}
		else {
			//redraw component
			this._render();
		}
	}

	/****************************************************************************************************
	 * called after property default values are set and local DOM is initialized
	 *
	 * @overwritten
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	ready() {
		super.ready();
		this.isReady = true;

		if (this.isdesignmode) {
			SigPolymer.afterNextRender(this, () => {
				this._render();
			});
		} else {
			if (this.propsReady) {
				this._render();
			}
		}
	}

	/****************************************************************************************************
	 * gets called if required properties are set
	 *
	 * * @memberof LasalRuntimeSigPointerRotateElement
	 ****************************************************************************************************/
	sigOnRequiredPropertiesReady() {
		super.sigOnRequiredPropertiesReady();
		this.propsReady = true;

		if (this.isReady) {
			this._render();
		}
	}

	/****************************************************************************************************
	 * Eventlisteners are added here
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_addEventListeners() {
		if (!this.eventListenersAdded) {
			this.sigAddEventListener(
				"LANGUAGE_CHG",
				(maID, miID, message) => {
					this._applyFonts();
					this._setTexts();
				},
				"runtimeEvent"
			);

			this.touchData = {
				startY: 0,
				yArr: [],
			};
			// initializes the event listeners
			for (let eventName in this.events) {
				// this.events[eventName] is set to a function that calls this._eventName
				this.events[eventName] = ((eventName) => {
					return (e) => {
						if (
							this.elems.el.contains(e.target) ||
							e.target === this.elems.el
						) {
							this.sigPreventDefault(e);
							if (this.source.length) {
								this["_" + eventName](e, this.touchData);
							}
						}
					};
				})(eventName);
			}

			this.sigAddEventListener(
				"down",
				this.events.touchstart,
				"polymerEvent",
				this.elems.el
			);
			this.sigAddEventListener(
				"track",
				this.events.touchmove,
				"polymerEvent",
				this.elems.el
			);
			this.sigAddEventListener(
				"up",
				this.events.touchend,
				"polymerEvent",
				this.elems.el
			);

			this.eventListenersAdded = true;
		}
	}

	/****************************************************************************************************
	 * Sets the Texts after a language switch
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _setTexts() {
		this.source = await this._loadSource();
		this._buildLists(this.source);
		this._preselectValue();
	}

	/****************************************************************************************************
	 * Redraw the Control when the designer is resized
	 * Needed because some properties are used when building the list.
	 * The Control height for example
	 *
	 * @overwritten
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	designerOnBoundsChanged() {
		if (this.rendered === true) {
			SigPolymer.afterNextRender(this, () => {
				this._render();
			});
		}	
	}

	/****************************************************************************************************
	 * The Control is build here
	 * Because now the Control should have a useable height and width
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_render() {
		const controlBounds = {
			height: this.offsetHeight,
			width: this.offsetWidth,
			left: this.offsetLeft,
			top: this.offsetTop,
		};
		const buttonHeightProp = this.style.getPropertyValue(
			"--theme-sig-control-cylinder-select-button-height"
		);
		let buttonHeight;
		if (this.value) {
			const valProp = this.propApi.getProperty(this.id, "value");
			if (valProp) {
				const unitProp = valProp.getUnitProp();
				if (unitProp) {
					this.unit = unitProp;
				}
			}
		}
		if (this.hasnavbuttons) {
			if (buttonHeightProp.indexOf("%") > 0) {
				buttonHeight =
					(controlBounds.height * parseFloat(buttonHeightProp)) / 100;
			} else {
				buttonHeight = parseFloat(buttonHeightProp);
			}
			if (isNaN(buttonHeight)) {
				buttonHeight = 0;
			}
			this.listContainerOffsetHeight = controlBounds.height - 2 * buttonHeight;
		} else {
			this.listContainerOffsetHeight = controlBounds.height;
		}

		this._buildComponent();
	}

	/****************************************************************************************************
	 * loads the source.
	 * If a text list was set, the text values are used, otherwise a range of numbers is returned
	 *
	 * @returns {Array} - an array with objects, representing the entries that are shown in the cylinder
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _loadSource() {
		let ret = [];
		let textListValues;

		// getting textlist is not supported in designer
		if (!this.isdesignmode) {
			textListValues = await this.propApi
				.getSchemeDataWithProperty(this.id, "sourcetextlist")
				.catch(() => {});
		}

		if (textListValues) {
			// map the Array to only contain the data we need
			textListValues = textListValues.map((value) => {
				return {
					text: value.value,
					value: value.compare.value,
				};
			});

			// sort by compare value to make sure the entries are in the correct order
			textListValues = textListValues.sort((a, b) => {
				return a.value - b.value;
			});
			ret = textListValues;
		} else {
			if (this.isdesignmode) {
				// check if datapoint is configured by checking if min or max is a string
				if (typeof this.min === 'string') {
					this.min = this.previewMin;
				}			
				if (typeof this.max === 'string') {
					this.max = this.previewMax;
				}				
			}
			// else use the numbers between min and max
			const length = this.max - this.min;

			// in case an invalid stepsize is set (it would cause an infinite loop) the stepsize is set to 1 (default)
			if (this.stepsize <= 0) {
				this.stepsize = 1;
			}

			for (let i = 0; i <= length; i += this.stepsize) {
				const el = {
					value: this.min + i,
					text: `${this.min + i}`,
				};
				if (this.unit) {
					el.text = this.unit.formatValue(this.unit.convertValueBack(el.value));
				}
				ret.push(el);
			}
		}
		return ret;
	}

	/****************************************************************************************************
	 * If there isn't already a loaded source, load the source
	 *
	 * @returns {Array} an array with objects, representing the entries that are shown in the cylinder
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _getSource() {
		let ret = [];
		if (this.source && this.dontreloadsource) {
			return this.source;
		}
		ret = await this._loadSource();
		return ret;
	}

	/****************************************************************************************************
	 * Initializes the variables needed for the scrolling
	 *
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_initializeVariables() {
		if (this.isdesignmode) {
			// check if datapoint is configured by checking if min or max is a string
			if (typeof this.min === 'string') {
				this.min = this.previewMin;
			}			
			if (typeof this.max === 'string') {
				this.max = this.previewMax;
			}			
		}		
		this.count = (this.max - this.min) / this.stepsize;
		this.count = Math.max(this.count, this.shown * 4);
		this.count = this.count - (this.count % 4);

		this.halfCount = this.count / 2;
		this.quarterCount = this.count / 4;
		this.a = this.sensitivity * 10;
		this.minV = Math.sqrt(1 / this.a);
		this.selected = this.source[0];

		this.exceedA = 10;
		this.moveT = 0;
		this.moving = false;

		this.elems = {
			el: this.$.listContainer,
			circleList: null,
			circleItems: null,

			highlight: null,
			highlightList: null,
			highListItems: null,
		};

		if (this.listContainerOffsetHeight === undefined) {
			this.listContainerOffsetHeight = this.elems.el.offsetHeight;
		}

		this.itemHeightCalc = (this.listContainerOffsetHeight * 3) / this.count;
		this.itemHeight = this.listContainerOffsetHeight / this.shown;
		this.itemAngleCalc = 360 / this.count;
		this.itemAngle = 270 / (this.shown * 2);
		this.radius =
			this.itemHeightCalc / Math.tan((this.itemAngleCalc * Math.PI) / 180);

		this.scroll = 0;

		this.touchstarted = false;
	}

	/****************************************************************************************************
	 * Preselects the value, or the first element if this.value isn't set
	 *
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _preselectValue() {
		// get the datapoint value
		// i've found this to be a bit more reliable than to just use this.value ?!
		const dp = this.propApi.getProperty(this.id, "value");
		if (dp) {
			// if a datapoint was added to this.value, get its value
			const dpValue = dp.getValue();
			if (dpValue) {
				// if we have such a value, select it
				this.select(parseFloat(dp.getValue()));
			} else if (this.value || this.value === 0) {
				// if this.value is set, select that instead
				this.select(this.value);
			} else {
				//  handling if the station is offline
				//  when first loading the component
				const datapoint = dp.getDatapoint();
				if (datapoint) {
					const station = datapoint.getStation();
					if (station || station === 0) {
						if (
							this.smApi.getStationStatus(station) === 1000 &&
							this.source.length
						) {
							// if a datapoint was added
							// but the station is offline
							// select the first value in the source list
							this.select(this.source[0].value);
						}
					}
				}
			}
		} else {
			if (this.source.length) {
				// if we have a source list...
				if (this.value || this.value === 0) {
					// ... and a value ...
					const idx = this.source.findIndex((val) => {
						return val.value === this.value;
					});
					if (idx != -1) {
						// ...and that value is found in the source list, select it
						this.select(this.value);
					} else {
						// ... and the value isn't found in the source list, select the first element in the source list
						this.select(this.source[0].value);
					}
				} else {
					// if we don't have a value
					// select the first element in the source list
					this.select(this.source[0].value);
				}
			}
		}
		return 1;
	}

	/****************************************************************************************************
	 * Builds the component
	 *
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _buildComponent() {
		//reset render state
		this.rendered = false;
		if (this.currentlyBuilding) {
			const buildAlreadyQueued = this.queue.reduce((acc, val) => {
				return acc || val.method === "_buildComponent";
			}, false);
			if (buildAlreadyQueued) {
				return;
			} else {
				this.queue.push({ method: "_buildComponent", params: [] });
				return;
			}
		}
		this.currentlyBuilding = true;

		// get source
		// since this can be a text list this whole thing needs to be async

		this.source = await this._getSource();

		// inializing variables
		this._initializeVariables();
		// building the circleList and highlightList
		const listsBuilt = this._buildLists(this.source);
		if (listsBuilt === 1) {
			// adding the eventlisteners
			this._addEventListeners();
			// applying fonts
			this._applyFonts();
			const valueSelected = await this._preselectValue();
			if (valueSelected === 1) {
				this.rendered = true;
				this.currentlyBuilding = false;
				this.dispatchEvent(new Event("cylinder-rendered"));
				// now do all the things that could not be done before
				await this._doQueue();
			} else {
				this._log("Error selecting value", "ERROR");
				this.currentlyBuilding = false;
			}
		} else {
			this._log("Error building lists", "ERROR");
			this.currentlyBuilding = false;
		}
		this.currentlyBuilding = false;
		return true;
	}

	/****************************************************************************************************
	 * Navigates to the previous value
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_navUpPointerdown() {
		// animate to the previous value
		this._animateToScroll(this.scroll, this.scroll + 1, 0.1)
			.then((res) => {
				// select the value
				this._selectByScroll(this.scroll);
			})
			.catch((rej) => {
				this._log("Error scrolling", "ERROR");
			});
	}

	/****************************************************************************************************
	 * Navigates to the next value
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_navDownPointerdown() {
		// animate to next value
		this._animateToScroll(this.scroll, this.scroll - 1, 0.1)
			.then((res) => {
				// seöect the value
				this._selectByScroll(this.scroll);
			})
			.catch((rej) => {
				this._log("Error scrolling", "ERROR");
			});
	}

	/****************************************************************************************************
	 * Normalizes the scroll value to a value between
	 * 0 and the number of elements
	 *
	 * @param {Number} scroll - A Number rpresenting the scroll position
	 * @return {Number} - A Number between 0 and the source length
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_normalizeScroll(scroll) {
		let normalizedScroll = scroll;

		while (normalizedScroll < 0) {
			normalizedScroll += this.source.length;
		}
		normalizedScroll = normalizedScroll % this.source.length;
		return normalizedScroll;
	}

	/****************************************************************************************************
	 * Jumps to the scroll value
	 *
	 * @param {*} scroll
	 * @return the scroll position after it was normalized
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_moveTo(scroll) {
		if (!Object.keys(this.elems).length) {
			return;
		}
		let addToCircleScroll = 0;
		if (this.type === "infinite") {
			scroll = this._normalizeScroll(scroll);
		}

		if (this.displaytype === "flat" && this.type === "infinite") {
			addToCircleScroll = this.quarterCount;
		}
		// set the y position of the circlelist (background)
		if (this.displaytype === "flat") {
			this.elems.circleList.style.top = `calc(50% + ${
				-(scroll + addToCircleScroll + 0.5) * this.itemHeight
			}px)`;
		} else {
			this.elems.circleList.style.transform = `translate3d(0, 0, ${-this
				.radius}px) rotateX(${this.itemAngle * scroll}deg)`;
		}

		// set the y position of the highlightlist
		if (this.type === "infinite") {
			this.elems.highlightList.style.top = `${
				-(scroll + 1) * this.itemHeight
			}px`;
		} else {
			this.elems.highlightList.style.top = `${-scroll * this.itemHeight}px`;
		}

		// height of the listContainer
		if (this.listContainerOffsetHeight === undefined) {
			this.listContainerOffsetHeight = this.elems.el.offsetHeight;
		}
		const offsetHeight = this.listContainerOffsetHeight;

		// for each element in the circle List, calculate if it is visible or hidden
		[...this.elems.circleItems].forEach((itemElem) => {
			// if the difference between the elements index and the scroll position is bigger
			// than half the items that fit into the container
			// hide the element
			// else show it.
			if (
				Math.abs(itemElem.dataset.index - scroll) >
				offsetHeight / this.itemHeight / 2
			) {
				itemElem.classList.add("hide");
			} else {
				itemElem.classList.remove("hide");
			}
		});
		return scroll;
	}

	/****************************************************************************************************
	 * Scrolles with a starting speed initV
	 *
	 * @param {init} initv
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _animateMoveByInitV(initV) {
		let initScroll; // start position
		let finalScroll; // end position

		let totalScrollLen;
		let acceleration;
		let time;

		if (this.type === "normal") {
			// if we are not scrolling infinitely
			if (this.scroll < 0 || this.scroll > this.source.length - 1) {
				// if the scroll position is less than 0 or
				// bigger than the length of the source List ...
				acceleration = this.exceedA;
				initScroll = this.scroll;
				//..set the final scroll to either 0 or the last element in the source list ...
				finalScroll = this.scroll < 0 ? 0 : this.source.length - 1;
				totalScrollLen = initScroll - finalScroll;

				time = Math.sqrt(Math.abs(totalScrollLen / acceleration));
				initV = acceleration * time;
				initV = this.scroll > 0 ? -initV : initV;
				// ... and animate to that position
				await this._animateToScroll(initScroll, finalScroll, time);
			} else {
				initScroll = this.scroll; // start position
				acceleration = initV > 0 ? -this.a : this.a; // acceleration
				time = Math.abs(initV / acceleration); // time until speed reaches 0
				totalScrollLen = initV * time + (acceleration * time * time) / 2; // the length of the scroll
				finalScroll = Math.round(this.scroll + totalScrollLen); // rounded up to ensure an integer
				finalScroll =
					finalScroll < 0
						? 0
						: finalScroll > this.source.length - 1
						? this.source.length - 1
						: finalScroll; // make sure finalScroll is valid

				totalScrollLen = finalScroll - initScroll;
				time = Math.sqrt(Math.abs(totalScrollLen / acceleration));
				// animate to the final scroll
				await this._animateToScroll(
					this.scroll,
					finalScroll,
					time,
					"easeOutCubic"
				);
			}
		} else {
			// if we are scrolling infinitely
			initScroll = this.scroll;

			acceleration = initV > 0 ? -this.a : this.a; // acceleration
			time = Math.abs(initV / acceleration); // time until speed reaches 0
			totalScrollLen = initV * time + (acceleration * time * time) / 2; // the length of the scroll
			finalScroll = Math.round(this.scroll + totalScrollLen); // rounded up to ensure an integer
			await this._animateToScroll(
				this.scroll,
				finalScroll,
				time,
				"easeOutCubic"
			);
		}

		// finally select the value to which we just scrolled
		this._selectByScroll(this.scroll);
	}

	/****************************************************************************************************
	 * Animates from initScroll to finalScroll in t time
	 *
	 * @param {Number} initScroll Starting position
	 * @param {Number} finalScroll End position
	 * @param {Number} t Time
	 * @param {string} [easingName="easeOutCubic"] Name of the easing function
	 * @return {Promise}  Resolves when the scrolling is finished
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_animateToScroll(initScroll, finalScroll, t, easingName = "easeOutCubic") {
		// if there is no need to animate, just move to the end position
		if (initScroll === finalScroll || t === 0) {
			this._moveTo(finalScroll);
			return;
		}

		t = Math.min(t, 1);
		let start = new Date().getTime() / 1000; // start Time
		let pass = 0; // passed time
		let totalScrollLen = finalScroll - initScroll; // total length of the scroll

		// returns a promise that resolves when the passed time reached t
		return new Promise((resolve, reject) => {
			this.moving = true;
			let tick = () => {
				// the time passed since the start
				pass = new Date().getTime() / 1000 - start;

				if (pass < t) {
					// if the passed time is less than t move a bit
					this.scroll = this._moveTo(
						initScroll + this.easing[easingName](pass / t) * totalScrollLen
					);
					this.moveT = requestAnimationFrame(tick);
				} else {
					// else resolve the promise
					// cancel the animation frame
					// move to the final scroll position
					// code after the resolve still gets executed
					resolve();
					this._stop();
					this.scroll = this._moveTo(initScroll + totalScrollLen);
				}
			};
			tick();
		});
	}

	/****************************************************************************************************
	 * Stops the _animatToScroll animation
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_stop() {
		this.moving = false;
		cancelAnimationFrame(this.moveT);
	}

	/****************************************************************************************************
	 * Selects a value based on its scrolling position
	 *
	 * @param {*} scroll  a value between 0 an this.count
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_selectByScroll(scroll) {
		// normalizes scrol; makes sure it is an integer
		scroll = this._normalizeScroll(scroll) | 0;

		// iIf the scroll is bigger than the source length
		// (shouldn't be possible after normalizing)
		// set it to the last element in the source list
		if (scroll > this.source.length - 1) {
			scroll = this.source.length - 1;
			this._moveTo(scroll);
		}

		// make sure scroll is an integer
		scroll = Math.round(scroll);
		// move to that position
		this._moveTo(scroll);
		this.scroll = scroll;
		this.selected = this.source[scroll];
		if (parseFloat(this.value) !== parseFloat(this.selected.value)) {
			this.value = this.selected.value;
		}
		this.onChange && this.onChange(this.selected);
	}

	/****************************************************************************************************
	 * Handle the case of a value being set which isn't included in the wheel
	 *
	 * @param {*} value the value (which is out of bounds)
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_wrongValueDisplay(value) {
		// generate new wheel including wrong value
		let sourceNew = this.source;
		const el = {
			value: value,
			text: `${value}`,
		};
		if (this.unit) {
			el.text = this.unit.formatValue(this.unit.convertValueBack(el.value));
		}

		// handle prepend or append
		if (value > this.max) {
			sourceNew.push(el);
		} else if (value < this.min) {
			sourceNew.unshift(el);
		}

		const built = this._buildLists(sourceNew);
		if (built) this.select(value);
	}

	/****************************************************************************************************
	 * Selects a value based on its scrolling position and jumps to that value
	 *
	 * @param value A value between 0 and this.count
	 * @return {*}
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	select(value) {
		if (value === null) {
			return;
		} else if (value <= this.max && value >= this.min) {
			if (this.source.length === 0) return;
			// if the value is valid - check if the last or the first element of source is temp and remove if necessary
			// remove all temp values outside the maximum
			for (let i = this.source.length - 1; i >= 0; i--) {
				if (this.source[i].value > this.max) {
					// as it is the last one always pop
					this.source.pop();
				} else {
					// not bigger than max anymore
					break;
				}
			}
			// remove all temp value outside the minimum
			for (let i = 0; i < this.source.length; i++) {
				if (this.source[i].value < this.min) {
					// as it is the first one always shift
					this.source.shift();
				} else {
					// not lower than min anymore
					break;
				}
			}
			this._buildLists(this.source);
		}
		// iterate over the source list, to find the value
		for (let i = 0; i < this.source.length; i++) {
			// if the value has been found
			if (this.source[i].value === value) {
				window.cancelAnimationFrame(this.moveT);
				let initScroll = this._normalizeScroll(this.scroll);
				let finalScroll = i;
				let t = Math.sqrt(Math.abs((finalScroll - initScroll) / this.a));
				// select that value
				this._selectByScroll(i);
				// return if value has been found
				return;
			}
		}

		// if we are here, the value hasn't been found

		if (!isNaN(value)) this._wrongValueDisplay(value);

		this._log(
			`can not select value: ${value}, ${value} match nothing in current source`,
			"DEBUG"
		);
	}

	/****************************************************************************************************
	 * The touch start/mousedown event handler
	 *
	 * @param {Event} e - the touch event
	 * @param {Object} touchData - information to previous touch events
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_touchstart(e, touchData) {
		this.sigPreventDefault(e);
		this.touchstarted = true;

		const selectOptions = this.$.listContainer.querySelector(".select-options");
		const highlightList = this.$.listContainer.querySelector(".highlight-list");
		const unknownValue = this.$.listContainer.querySelector(".unknown-value");
		// make sure the selectOptions and highlightList are visible and the unknown value div is hidden
		if (selectOptions) {
			selectOptions.classList.remove("hide");
		}
		if (highlightList) {
			highlightList.classList.remove("hide");
		}
		if (unknownValue) {
			unknownValue.classList.add("hide");
		}

		// handle the rotation
		let eventY = e.detail.y;
		let rotation = this.rotation;
		if (this.parent) {
			rotation = this.parent.rotation;
		}
		if (this.parent && this.parent.parent) {
			rotation = this.parent.parent.rotation;
		}
		switch (rotation) {
			case 0:
				eventY = e.detail.y;
				break;
			case 90:
				eventY = e.detail.x;
				break;
			case 180:
				eventY = e.detail.y;
				break;
			case 270:
				eventY = e.detail.x;
				break;
			default:
				eventY = e.detail.y;
		}
		touchData.startY = eventY;
		touchData.yArr = [[eventY, new Date().getTime()]];
		touchData.touchScroll = this.scroll;
		this._stop();
	}

	/****************************************************************************************************
	 * The touchmove/mousemove event handler
	 *
	 * @param {Event} e - the touch event
	 * @param {Object} touchData informatin to previous touch events
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_touchmove(e, touchData) {
		this.sigPreventDefault(e);
		let eventY = e.detail.y;
		let rotation = this.rotation;
		if (this.parent) {
			rotation = this.parent.rotation;
		}
		if (this.parent && this.parent.parent) {
			rotation = this.parent.parent.rotation;
		}
		switch (rotation) {
			case 0:
				eventY = e.detail.y;
				break;
			case 90:
				eventY = e.detail.x;
				break;
			case 180:
				eventY = e.detail.y;
				break;
			case 270:
				eventY = e.detail.x;
				break;
			default:
				eventY = e.detail.y;
		}
		touchData.yArr.push([eventY, new Date().getTime()]);
		if (touchData.length > 5) {
			touchData.unshift();
		}

		let scrollAdd = (touchData.startY - eventY) / this.itemHeight;
		switch (rotation) {
			case 0:
				scrollAdd = (touchData.startY - eventY) / this.itemHeight;
				break;
			case 90:
				scrollAdd = (eventY - touchData.startY) / this.itemHeight;
				break;
			case 180:
				scrollAdd = (eventY - touchData.startY) / this.itemHeight;
				break;
			case 270:
				scrollAdd = (touchData.startY - eventY) / this.itemHeight;
				break;
			default:
				scrollAdd = (touchData.startY - eventY) / this.itemHeight;
		}
		let moveToScroll = scrollAdd + this.scroll;

		if (this.type === "normal") {
			if (moveToScroll < 0) {
				moveToScroll *= 0.3;
			} else if (moveToScroll > this.source.length) {
				moveToScroll =
					this.source.length + (moveToScroll - this.source.length) * 0.3;
			}
		} else {
			moveToScroll = this._normalizeScroll(moveToScroll);
		}

		touchData.touchScroll = this._moveTo(moveToScroll);
	}

	/****************************************************************************************************
	 * The touchend/mouseup event handler
	 *
	 * @param {Event} e the touch event
	 * @param {Object} touchData information to previous touch events
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_touchend(e, touchData) {
		this.sigPreventDefault(e);
		if (!this.touchstarted) {
			return;
		}
		this.touchstarted = false;
		let v;

		if (touchData.yArr.length <= 1) {
			v = 0;
		} else {
			let startTime = touchData.yArr[touchData.yArr.length - 2][1];
			let endTime = touchData.yArr[touchData.yArr.length - 1][1];
			let startY = touchData.yArr[touchData.yArr.length - 2][0];
			let endY = touchData.yArr[touchData.yArr.length - 1][0];

			// calculates the speed.
			v = (((startY - endY) / this.itemHeight) * 1000) / (endTime - startTime);
			let rotation = this.rotation;
			if (this.parent) {
				rotation = this.parent.rotation;
			}
			if (rotation === 90 || rotation === 180) {
				v =
					(((endY - startY) / this.itemHeight) * 1000) / (endTime - startTime);
			}
			let sign = v > 0 ? 1 : -1;

			v = Math.abs(v) > 30 ? 30 * sign : v;
		}
		if (isNaN(v)) {
			v = 0;
		}
		this.scroll = touchData.touchScroll;
		this._animateMoveByInitV(v);
	}

	/****************************************************************************************************
	 * Fills the #listContainer with the this.source Elements
	 *
	 * @param {Array} source - the array that contains the values that are shown
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_buildLists(source) {
		if (!source.length) {
			return;
		}
		if (!this.elems || !this.elems.el) return;
		if (this.currentlyBuilding) {
			this.queue.push({
				method: "_buildLists",
				params: [source],
			});
		}

		let template = `
                    <div class="select-wrap">
                    <ul class="select-options" style=${
											this.displaytype === "flat"
												? '""'
												: `transform: translate3d(0, 0, ${-this
														.radius}px) rotateX(0deg);`
										}>
                        {{circleListHTML}}
                        <!-- <li class="select-option">a0</li> -->
                    </ul>
                    <div class="highlight">
                        <ul class="highlight-list">
                        <!-- <li class="highlight-item"></li> -->
                        {{highListHTML}}
                        </ul>
                        <div class="unknown-value hide" style="display:hidden"></div>
                    </div>
                    </div>
                `;
		// if the scroll type is infinite, expand the source values accordingly
		if (this.type === "infinite") {
			let concatSource = [].concat(source);
			while (concatSource.length < this.halfCount) {
				concatSource = concatSource.concat(source);
			}
			source = concatSource;
		}
		this.source = source;
		let sourceLength = source.length;

		// list HTML
		let circleListHTML = "";
		for (let i = 0; i < source.length; i++) {
			circleListHTML += `<li class="select-option"
                      style="
                        ${
													this.displaytype === "flat"
														? ""
														: `transform: rotateX(${
																-this.itemAngle * i
														  }deg) translate3d(0, 0, ${this.radius}px);`
												}
                      "
                      data-index="${i}"
                      >${source[i].text}</li>`;
		}

		// highlight HTML
		let highListHTML = "";
		for (let i = 0; i < source.length; i++) {
			highListHTML += `<li class="highlight-item">
                          ${source[i].text}
                        </li>`;
		}

		if (this.type === "infinite") {
			// list head and tail
			for (let i = 0; i < this.quarterCount; i++) {
				// head
				circleListHTML =
					`<li class="select-option"
                        style="
                            ${
															this.displaytype === "flat"
																? ""
																: `transform: rotateX(${
																		this.itemAngle * (i + 1)
																  }deg) translate3d(0, 0, ${this.radius}px);`
														}
                        "
                        data-index="${-i - 1}"
                        >${source[sourceLength - i - 1].text}</li>` +
					circleListHTML;
				// tail
				circleListHTML += `<li class="select-option"
                        style="
                            ${
															this.displaytype === "flat"
																? ""
																: `transform: rotateX(${
																		-this.itemAngle * (i + sourceLength)
																  }deg) translate3d(0, 0, ${this.radius}px);`
														}
                        "
                        data-index="${i + sourceLength}"
                        >${source[i].text}</li>`;
			}

			// highlight head and tail
			highListHTML =
				`<li class="highlight-item">
                            ${source[sourceLength - 1].text}
                        </li>` + highListHTML;
			highListHTML += `<li class="highlight-item">${source[0].text}</li>`;
		}

		this.updateStyles({
			"--theme-sig-control-cylinder-select-select-option-top": `${
				this.itemHeight * -0.5
			}px`,
			"--theme-sig-control-cylinder-select-select-option-height": `${this.itemHeight}px`,
			"--theme-sig-control-cylinder-select-select-option-line-height": `${this.itemHeight}px`,
			"--theme-sig-control-cylinder-select-highlight-item-height": `${this.itemHeight}px`,
		});

		// substitute the {{circleListHTML}} and {{highlightListHTML}} in the template with the actual values
		this.elems.el.innerHTML = template
			.replace("{{circleListHTML}}", circleListHTML)
			.replace("{{highListHTML}}", highListHTML);
		this.elems.circleList = this.elems.el.querySelector(".select-options");
		this.elems.circleItems = this.elems.el.querySelectorAll(".select-option");

		this.elems.highlight = this.elems.el.querySelector(".highlight");
		this.elems.highlightList = this.elems.el.querySelector(".highlight-list");
		this.elems.highlightitems =
			this.elems.el.querySelectorAll(".highlight-item");

		if (this.type === "infinite" && this.displaytype !== "flat") {
			this.elems.highlightList.style.top = -this.itemHeight + "px";
		}

		this.updateStyles({
			"--theme-sig-control-cylinder-select-highlight-top": `calc(50% - ${
				this.itemHeight / 2
			}px)`,
			"--theme-sig-control-cylinder-select-highlight-height":
				this.itemHeight + "px",
			"--theme-sig-control-cylinder-select-highlight-line-height":
				this.itemHeight + "px",
		});

		return 1;
	}

	/****************************************************************************************************
	 * Applies the fonts for the highlight and the list
	 *
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_applyFonts() {
		// get the font Objects from the api
		let listFont = this.propApi.getProperty(this.id, "listfont");

		let highlightFont = this.propApi.getProperty(this.id, "highlightfont");

		if (!listFont) {
			listFont = this.listfontobj;
		}
		if (!highlightFont) {
			highlightFont = this.highlightfontobj;
		}
		// the elements to which to apply the font
		const list = this.$.listContainer.querySelectorAll(".select-wrap")[0];
		const highlight = this.$.listContainer.querySelectorAll(".highlight")[0];

		// apply the fonts if we have them
		if (listFont && Object.keys(listFont).length > 0) {
			this._applyFontObjectToNode(list, listFont.getValue());
		}
		if (highlightFont && Object.keys(highlightFont).length > 0) {
			this._applyFontObjectToNode(highlight, highlightFont.getValue());
		}
	}

	/****************************************************************************************************
	 * Observer for this.value
	 *
	 * @param {*} newval
	 * @param {*} oldval
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_valueChanged(newval, oldval) {
		if (this.isdesignmode) {
			this.value = this.source.length ? this.source[0].value : this.min;
		} else {
			this.select(parseFloat(newval));
			this._dispatchEvent("value-changed");
		}
	}

	/****************************************************************************************************
	 * Updates the list font if the object changes
	 *
	 * @param {*} newval
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_updateListFont(newval) {
		if (!this.rendered) {
			this.queue.push({
				method: "_updateListFont",
				params: [newval],
			});
			return;
		}
		if (newval && Object.keys(newval).length > 0) {
			const list = this.$.listContainer.querySelectorAll(".select-wrap")[0];
			this._applyFontObjectToNode(list, newval.getValue());
		}
	}

	/****************************************************************************************************
	 * Updates the list font if the object changes
	 *
	 * @param {*} newval
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_updateHighlightFont(newval) {
		if (!this.rendered) {
			this.queue.push({
				method: "_updateHighlightFont",
				params: [newval],
			});
			return;
		}
		if (newval && Object.keys(newval).length > 0) {
			const highlight = this.$.listContainer.querySelectorAll(".highlight")[0];
			this._applyFontObjectToNode(highlight, newval.getValue());
		}
	}

	/****************************************************************************************************
	 * Functions can be added to this.queue
	 * Needed if a value changes, but to apply the changes the component needs to be rendered
	 * -> add to queue,
	 * _doQueue is called after the component is rendered, and executes the commands
	 *
	 * @return {*}
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	async _doQueue() {
		if (!this.rendered) {
			return;
		}
		if (this.currentlyBuilding) {
			return;
		}
		if (this.queueBusy) {
			return;
		}
		this.queueBusy = true;

		this.queue = this.queue.reduce((acc, el) => {
			if (!acc) {
				acc = [];
			}

			let override = false;
			for (let i = 0; i < acc.length; i++) {
				if (acc[i].method === el.method) {
					override = true;
					acc[i] = el;
				}
			}
			if (!override) {
				acc.push(el);
			}

			return acc;
		}, []);

		while (this.queue.length > 0) {
			const cmd = this.queue.shift();
			await this[cmd.method](...cmd.params);
		}
		this.queueBusy = false;
		// preselect value in case of built lists was in queue
		this._preselectValue();
	}

	/****************************************************************************************************
	 * redraws the component in the design mode
	 *
	 * @private
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	_redraw() {
		if (this.isdesignmode && this.rendered) {
			this._buildComponent();
		}
	}

	/****************************************************************************************************
	 * called after the element is attached to the document
	 * can be called multiple times during the lifetime of an element
	 * the first attached callback is guaranteed not to fire until after ready
	 *
	 * @overwritten
	 * @memberof LasalRuntimeSigControlCylinderSelect
	 ****************************************************************************************************/
	connectedCallback() {
		super.connectedCallback();
		if (this.rendered && this.registeredEvents.length === 0) {
			this.eventListenersAdded = false;
			this._addEventListeners();
		}
	}
}
customElements.define(
	LasalRuntimeSigControlCylinderSelect.is,
	LasalRuntimeSigControlCylinderSelect
);
