    // editor elements (gates, wires...)
    import * as editorElements from './editorElements';

    // svg elements
    import { Pattern, Rectangle, PolyLinePoint, PolyLinePoints, PolyLine } from './svgObjects';

    // network logic and simulation
    import Logic from './Logic';
    import Simulation from './Simulation';
    import { SimulationDummy } from './Simulation';

    // ui stuff
    import ContextMenu from './ui/ContextMenu';
    import FloatingMenu from './ui/FloatingMenu';
    import Tutorial from './ui/Tutorial';
    import Messages from './ui/Messages';
    import ViewBox from './ui/ViewBox';

    // mouse scroll event listerer for ui, manhattan distance for importData
    import { addMouseScrollEventListener, manhattanDistance } from './other/helperFunctions';

    // used in importData
    // note: imported from a node module
    import { PriorityQueue } from 'libstl';

    const ctrlKey = 17,
        cmdKey = 91;

    /** @module App */
    /**
     * Main class of the application. It represents an instance of the whole editor and holds
     * references to all its elements.
     */
    export default class App {
        /**
         * Initialize the Svg class
         * @param {string} canvas   query selector of the SVG element, that will contain all SVG content of the application
         * @param {number} gridSize initial size of the grid in SVG pixels
         */
        constructor(canvas, gridSize) {
            /**
             * jQuery element for the SVG document
             */
            this.$svg = $(canvas);

            /**
             * space between grid lines in SVG pixels
             * @type {number}
             */
            this.gridSize = gridSize;

            /**
             * Array of all boxes (instances of objects derived from editorElements.Box) used on canvas
             * @type {Array}
             */
            this.boxes = []; // stores all boxes

            /**
             * Array of all wires (instances of editorElements.Wire) used on canvas
             * @type {Array}
             */
            this.wires = []; // stores all wires

            /**
             * Interface for showing messages to the user
             * @type {Messages}
             */
            this.messages = new Messages();

            this.simulationEnabled = true;
            this.simulation = new SimulationDummy(); // dummy, will be overwritten on startNewSimulation

            /**
             * distance from the left top corner to the first element in the imported network
             * and distance from the left top corner to the imported black box _in grid pixels_
             * @type {number}
             */
            this.leftTopPadding = 4;

            // create the defs element, used for patterns
            this.$defs = $('<defs>');
            this.$svg.prepend(this.$defs);

            // BACKGROUND PATTERN
            let pattern = new Pattern('grid', this.gridSize, this.gridSize);

            let patternPoints = new PolyLinePoints()
                .append(new PolyLinePoint(0, 0))
                .append(new PolyLinePoint(this.gridSize, 0))
                .append(new PolyLinePoint(this.gridSize, this.gridSize));

            pattern.addChild(new PolyLine(patternPoints, 2, '#c2c3e4'));
            this.addPattern(pattern.get());

            this.background = new Rectangle(0, 0, this.width, this.height, 'url(#grid)', 'none');
            this.appendJQueryObject(this.background.get());
            this.refresh();

            // set the viewbox for future zooming and moving of the canvas
            this.$svg.attr('preserveAspectRatio', 'xMinYMin slice');
            this.viewbox = new ViewBox(0, 0, this.width, this.height);
            this.applyViewbox();

            // CONSTRUCT CONTEXT MENU
            this.contextMenu = new ContextMenu(this);

            // CONSTRUCT FLOATING MENU
            this.floatingMenu = new FloatingMenu(this);

            let target;

            // ALL EVENT CALLBACKS
            this.$svg
                .on('mousedown', event => {
                    target = this.getRealTarget(event.target);

                    if (target !== undefined) {
                        // propagate mousedown to the real target
                        target.onMouseDown(event);
                    } else {
                        // mousedown happened directly on the svg
                        this.onMouseDown(event);
                    }

                    this.hideContextMenu();
                    event.preventDefault();
                })
                .on('mousemove', event => {
                    if (target !== undefined) {
                        target.onMouseMove(event);
                    } else {
                        // mousemove happened directly on the svg
                        this.onMouseMove(event);
                    }

                    event.preventDefault();
                })
                .on('mouseup', event => {
                    if (target !== undefined) {
                        target.onMouseUp(event);
                    } else {
                        // mouseup happened directly on the svg
                        this.onMouseUp(event);
                    }

                    target = undefined;

                    event.preventDefault();
                })
                .on('contextmenu', event => {
                    this.displayContextMenu(
                        event.pageX,
                        event.pageY,
                        this.getRealJQueryTarget(event.target)
                    );
                    event.preventDefault();
                });

            $(document)
                .on('keydown', event => {
                    this.onKeyDown(event);
                })
                .on('keyup', event => {
                    this.onKeyUp(event);
                });

            // update the viewbox on window resize
            $(window).on('resize', () => {
                this.viewbox.newDimensions(this.width, this.height);
                this.applyViewbox();
            });

            addMouseScrollEventListener(canvas, event => {
                // zoom only if the ctrl key is not pressed
                if (!event.ctrlKey) {
                    this.zoom += event.delta * 0.1;

                    event.preventDefault();
                }
            });

            $(window).on('keydown', event => {
                const actions = {
                    '+': 0.1,
                    '-': -0.1
                };

                if (actions[event.key]) {
                    this.zoom += actions[event.key];
                }
            });

            /**
             * property containing an instance of [Tutorial](./module-Tutorial.html), if there is any
             * @type {Tutorial}
             */
            this.tutorial;

            // check if the user visits for the first time, if so, start the tutorial
            try {
                if (!localStorage.userHasVisited) {
                    this.startTutorial();
                }
            } catch (e) {
                console.warn(e);
            }
        }

        /**
         * Get the width of the main SVG element
         * @return {number} width of the SVG element in pixels
         */
        get width() {
            return this.$svg.width();
        }

        /**
         * Get the height of the main SVG element
         * @return {number} height of the SVG element in pixels
         */
        get height() {
            return this.$svg.height();
        }

        /**
         * Process all keydown events that are connected to the app
         * @param  {jquery.KeyboardEvent} event KeyboardEvent generated by a listener
         */
        onKeyDown(event) {
            if (event.keyCode === ctrlKey || event.keyCode === cmdKey) {
                this.$svg.addClass('grabbable');
            }
        }

        /**
         * Process all keyup events that are connected to the app
         * @param  {jquery.KeyboardEvent} event KeyboardEvent generated by a listener
         */
        onKeyUp(event) {
            if (event.keyCode === ctrlKey || event.keyCode === cmdKey) {
                this.$svg.removeClass('grabbable');
            }
        }

        /**
         * Process all mousedown events that are happening directly on the canvas
         * @param  {jquery.MouseEvent} event MouseEvent generated by a listener
         */
        onMouseDown(event) {
            // any click on canvas cancels the wire creation
            this.cancelWireCreation();

            // middle mouse or left mouse + ctrl moves the canvas
            if (event.which === 2 || (event.which === 1 && event.ctrlKey)) {
                this.$svg.addClass('grabbed');
                this.moveCanvas = {
                    left: event.pageX,
                    top: event.pageY
                };
            }
        }

        /**
         * Process all mousemove events that are happening directly on the canvas
         * @param  {jquery.MouseEvent} event MouseEvent generated by a listener
         */
        onMouseMove(event) {
            if (this.moveCanvas) {
                let left = event.pageX - this.moveCanvas.left;
                let top = event.pageY - this.moveCanvas.top;

                this.viewbox.move(left, top);

                this.applyViewbox();

                this.moveCanvas = {
                    left: event.pageX,
                    top: event.pageY
                };
            }
        }

        /**
         * Process all mouseup events that are happening directly on the canvas
         */
        onMouseUp() {
            if (this.moveCanvas) {
                this.$svg.removeClass('grabbed');
                this.moveCanvas = undefined;

                // if tutorial exists, call tutorial callback
                if (this.tutorial) {
                    this.tutorial.onCanvasMoved();
                }
            }
        }

        /**
         * Set the viewBox attribute of the SVG element and size and position attributes
         * of the rectangle with the background grid to match the values in this.viewbox
         */
        applyViewbox() {
            // adjust background
            this.background.addAttr({
                x: this.viewbox.left,
                y: this.viewbox.top,
                width: this.viewbox.width,
                height: this.viewbox.height
            });

            // set the viewBox attribute
            this.$svg.attr('viewBox', this.viewbox.str);
        }

        /**
         * Get the current zoom multiplier of the canvas
         * @return {number}
         */
        get zoom() {
            return this.viewbox.zoom;
        }

        /**
         * Set the zoom multiplier of the canvas.
         * I sets the viewbox zoom and then applies the new value by calling this.applyViewbox()
         * @param  {number} value set the zoom to this value
         */
        set zoom(value) {
            this.viewbox.zoom = value;
            this.applyViewbox();

            // if tutorial exists, call tutorial callback
            if (this.tutorial) {
                this.tutorial.onCanvasZoomed();
            }
        }

        /**
         * start the tutorial
         */
        startTutorial() {
            // instantiate the tutorial
            this.tutorial = new Tutorial(this, () => {
                // set userHasVisited to true when user closes (or finishes) the tutorial
                localStorage.userHasVisited = true;

                // unset the this.tutorial property
                this.tutorial = undefined;
            });

            // start the tutorial
            this.tutorial.start();
        }

        /**
         * Generate an object containing export data for the canvas and all elements.
         * Data from this function should cover all important information needed to import the
         * network in a different session.
         * @return {object} object containing infomration about the network
         */
        get exportData() {
            this.exportWireIdMap = new Map();
            this.exportWireId = 0;

            let data = {
                boxes: []
            };

            for (const box of this.boxes) {
                data.boxes.push(box.exportData);
            }

            return data;
        }

        /**
         * Recreate a logic network from the data provided
         * @param  {object} data object containing information about the imported network
         * @param  {number} [x]  horizontal position of the left top corner of the network in grid pixels
         * @param  {number} [y]  vertical position of the left top corner of the network in grid pixels
         */
        importData(data, x, y) {
            return new Promise(resolve => {
                let warnings = [];

                // if the x or y is undefined, set it to leftTopPadding instead
                // (cannot use x || leftTopPadding because of 0)
                x = x !== undefined ? x : this.leftTopPadding;
                y = y !== undefined ? y : this.leftTopPadding;

                this.simulationEnabled = false;

                // list of wires to be added
                let newWires = new Map();

                // find the leftmost and topmost coordinate of any box, save them to leftTopCorner
                let leftTopCorner = {
                    x: 0,
                    y: 0
                };

                for (const boxData of data.boxes) {
                    if (boxData.transform && boxData.transform.items) {
                        for (const transformInfo of boxData.transform.items) {
                            if (transformInfo.name === 'translate') {
                                if (leftTopCorner) {
                                    leftTopCorner = {
                                        x: Math.min(leftTopCorner.x, transformInfo.args[0]),
                                        y: Math.min(leftTopCorner.y, transformInfo.args[1])
                                    };
                                } else {
                                    leftTopCorner = {
                                        x: transformInfo.args[0],
                                        y: transformInfo.args[1]
                                    };
                                }
                            }
                        }
                    }
                }

                for (let boxData of data.boxes) {
                    // mapping of dataBox.name of the objects that have category "other"
                    const otherMap = {
                        input: () => this.newInput(0, 0, boxData.isOn, false),
                        output: () => this.newOutput(0, 0, false)
                    };

                    // mapping of dataBox.category
                    const boxMap = {
                        gate: () => this.newGate(boxData.name, 0, 0, false),
                        blackbox: () =>
                            this.newBlackbox(
                                boxData.inputs,
                                boxData.outputs,
                                boxData.table,
                                boxData.name,
                                0,
                                0,
                                false
                            ),
                        other: () => {
                            if (!boxData.name) throw `This network contains a box without a name.`;

                            if (!otherMap[boxData.name])
                                throw `This network contains unknown box names. (${boxData.name})`;

                            return otherMap[boxData.name]();
                        }
                    };

                    const createBox = () => {
                        if (!boxData.category) throw `This network a box without a category.`;

                        if (!boxMap[boxData.category])
                            throw `This network contains unknown box categories. (${boxData.category})`;

                        return boxMap[boxData.category]();
                    };

                    let box;

                    try {
                        box = createBox();
                    } catch (e) {
                        warnings.push(e);
                    }

                    if (box) {
                        // proccess box transforms (translation and rotation)
                        let transform = new editorElements.Transform();
                        let rotationCount = 0;

                        const transformItemMap = {
                            translate: args => {
                                transform.setTranslate(
                                    args[0] -
                                    leftTopCorner.x + // make it the relative distance from the leftmost element
                                        x, // apply the position
                                    args[1] -
                                    leftTopCorner.y + // make it the relative distance from the topmost element
                                        y // apply the position
                                );
                            },
                            rotate: args => {
                                rotationCount = (args[0] % 360) / 90;
                            }
                        };

                        if (boxData.transform && boxData.transform.items) {
                            for (const transformItem of boxData.transform.items) {
                                const { name, args } = transformItem;

                                if (!name) {
                                    warnings.push(
                                        `This network contains unnamed transform properties.`
                                    );
                                    break;
                                }

                                if (!transformItemMap[name]) {
                                    warnings.push(
                                        `This network contains unknown transform properties. (${
                                            transformItem.name
                                        })`
                                    );
                                    break;
                                }

                                transformItemMap[name](args);
                            }
                        }

                        transform.toSVGPixels(this);
                        box.setTransform(transform);

                        for (let i = 0; i < rotationCount; ++i) {
                            box.rotate(true);
                        }

                        // add all wires to the list of wires to be added
                        if (boxData.connections) {
                            for (const connection of boxData.connections) {
                                // get the artificial wire id
                                let wireId = connection.wireId;

                                // pass the values got from json into a variable that will be added into the map
                                let value = {
                                    index: connection.index,
                                    boxId: box.id
                                };

                                // add the value to the map
                                if (newWires.has(wireId)) {
                                    // if there already is a wire with this id in the map,
                                    // add the value to the end of the array of values
                                    let mapValue = newWires.get(wireId);
                                    mapValue.push(value);
                                    newWires.set(wireId, mapValue);
                                } else {
                                    // if there is no wire with this id in the map
                                    // add the wire and set the value to be the first element in the array
                                    newWires.set(wireId, [value]);
                                }
                            }
                        }
                    }
                }

                // refresh the SVG document (needed for wiring)
                this.refresh();

                // with all boxes added, we can now connect them with wires

                // priority queue for the new wires, priority being (1 / manhattanDistance) between the conenctors, higher is better
                let wireQueue = new PriorityQueue();

                // get all ids for lal the
                for (const wireInfo of newWires.values()) {
                    let connectorIds = [];

                    // create an array [connector1Id, connector2Id]
                    for (const { boxId, index } of wireInfo) {
                        connectorIds.push(this.getBoxById(boxId).connectors[index].id);
                    }

                    // create and array [{x, y}, {x, y}] containing positions for connectors 1 and 2
                    const connectorsPositions = connectorIds.map(connectorId =>
                        this.getConnectorPosition(this.getConnectorById(connectorId), true)
                    );

                    if (connectorsPositions.length === 2) {
                        let wire = this.newWire(...connectorIds, false, false);

                        // get the manhattan distance between these two connectors
                        const distance = manhattanDistance(...connectorsPositions);

                        // add connectorids to the priority queue
                        wireQueue.enqueue(wire, 1 / distance);
                    } else {
                        warnings.push(
                            `Found a wire that does not have two endings. (It had ${
                                connectorsPositions.length
                            } instead.)`
                        );
                    }
                }

                if (window.Worker) {
                    let wirePoints = [];
                    let wireReferences = [];

                    // convert the queue to an array (this is needed by the web worker)
                    while (!wireQueue.isEmpty()) {
                        const wire = wireQueue.dequeue();

                        let wireStart = this.getConnectorPosition(wire.connection.from.connector, true);
                        let wireEnd = this.getConnectorPosition(wire.connection.to.connector, true);

                        wirePoints.push([
                            {
                                x: wireStart.x / this.gridSize,
                                y: wireStart.y / this.gridSize
                            },
                            {
                                x: wireEnd.x / this.gridSize,
                                y: wireEnd.y / this.gridSize
                            }
                        ]);

                        wireReferences.push(wire);
                    }

                    // [routeWorkerFileName] replaced in the build process (defined in gulpfile) depending on devel / prod build
                    let myWorker = new Worker('js/[routeWorkerFileName]');

                    let loadingMessage = this.messages.newLoadingMessage(
                        'looking for the best wiring…'
                    );

                    myWorker.onmessage = event => {
                        const { paths } = event.data;
                        // iterate wireReferences and paths synchronously
                        wireReferences.forEach((wire, key) => {
                            wire.setWirePath(wire.pathToPolyLine(paths[key]));
                            wire.updateWireState();
                        });

                        loadingMessage.hide();
                    };

                    const message = {
                        wires: wirePoints,
                        nonRoutableNodes: this.getNonRoutableNodes(),
                        inconvenientNodes: this.getInconvenientNodes()
                    };

                    myWorker.postMessage(message);
                } else {
                    // web worker is not supported: use an interval to make the import a bit slower
                    // by dividing it into chunks, so the browser window is not entirely frozen when the wiring is happening

                    const wiresToBeRoutedAtOnce = 10;
                    const delayBetweenIterations = 200;

                    // add wires in the order from short to long
                    let wirePlacingInterval = window.setInterval(() => {
                        if (!wireQueue.isEmpty()) {
                            for (let i = 0; i < wiresToBeRoutedAtOnce; ++i) {
                                if (wireQueue.isEmpty()) {
                                    break;
                                }

                                const wire = wireQueue.dequeue();
                                wire.routeWire(true, false);
                                wire.updateWireState();
                            }
                        } else {
                            console.log('finished');
                            clearInterval(wirePlacingInterval);
                        }
                    }, delayBetweenIterations);
                }

                // refresh the SVG document
                this.refresh();

                this.simulationEnabled = true;

                resolve(warnings);
            });
        }

        /**
         * When user clicks on a connector, remember it until they click on some other connector.
         * Than call newWire with the last two connectors ids as arguments.
         * Visualize the process by displaying a grey wire between the first conenctor and the mouse pointer.
         * @param  {string} connectorId id of the connector that the user clicked on
         */
        wireCreationHelper(connectorId, mousePosition) {
            if (!this.wireCreation) {
                this.wireCreation = {
                    fromId: connectorId
                };

                this.displayCreatedWire(mousePosition);
            } else {
                if (this.wireCreation.fromId !== connectorId) {
                    this.hideCreatedWire();

                    this.newWire(this.wireCreation.fromId, connectorId);

                    this.wireCreation = undefined;
                }
            }
        }

        /**
         * helper for wireCreationHelper that displays a grey wire between the first connector and the specified mousePosition
         * @param  {Object} mousePosition object with x and y coordinates in SVG pixels
         */
        displayCreatedWire(mousePosition) {
            this.wireCreation.tempWire = new editorElements.HelperWire(
                this,
                this.wireCreation.fromId,
                mousePosition
            );

            $(window).on('mousemove.wireCreation', event => {
                event = this.viewbox.transformEvent(event);

                mousePosition = {
                    x: event.pageX,
                    y: event.pageY
                };

                this.wireCreation.tempWire.updateMousePosition(mousePosition);
            });

            this.appendElement(this.wireCreation.tempWire);
            this.moveToBackById(this.wireCreation.tempWire.id);
        }

        /**
         * helper for wireCreationHelper that hides the temporary wire when wire creation is done
         */
        hideCreatedWire() {
            $(window).off('mousemove.wireCreation');

            this.wireCreation.tempWire.get().remove();
            this.wireCreation.tempWire = undefined;
        }

        /**
         * helper for wireCreationHelper that cancels the wire creation process
         */
        cancelWireCreation() {
            if (this.wireCreation) {
                this.hideCreatedWire();
                this.wireCreation = undefined;
            }
        }

        /**
         * Run a logic simulation from the startingConnector.
         * This refreshes the states of all elements in the network whose inputs are
         * directly (or by transition) connected to startingConnector's output
         * @param  {OutputConnector} startingConnector run simulation from this output connector
         * @param  {Logic.state} state new state of the startingConnector
         */
        startNewSimulation(startingConnector, state) {
            if (this.simulationEnabled) {
                this.simulation = new Simulation(this);
                this.simulation.notifyChange(startingConnector.id, state);
                this.simulation.run();
            }
        }

        /**
         * Create a new gate on the specified position
         * @param  {string}  name           type of the gate (and, or ...)
         * @param  {number}  x              horizontal position of the gate in SVG pixels
         * @param  {number}  y              vertical position of the gate in SVG pixels
         * @param  {boolean} [refresh=true] if true, this.refresh() will be called after adding the gate
         * @return {editorElements.Gate}    instance of Gate that has been newly added
         */
        newGate(name, x, y, refresh = true) {
            return this.newBox(x, y, new editorElements.Gate(this, name, x, y), refresh);
        }

        /**
         * Create an input box on the specified position
         * @param  {number}  x              horizontal position of the gate in SVG pixels
         * @param  {number}  y              vertical position of the gate in SVG pixels
         * @param  {boolean} [isOn=false]   state of the input box (default is false (off))
         * @param  {boolean} [refresh=true] if true, this.refresh() will be called after adding the input box
         * @return {editorElements.InputBox}    instance of the InputBox that has been newly added
         */
        newInput(x, y, isOn = false, refresh = true) {
            return this.newBox(x, y, new editorElements.InputBox(this, isOn), refresh);
        }

        /**
         * Create an output box on the specified position
         * @param  {number}  x              horizontal position of the gate in SVG pixels
         * @param  {number}  y              vertical position of the gate in SVG pixels
         * @param  {boolean} [refresh=true] if true, this.refresh() will be called after adding the output box
         * @return {editorElements.InputBox}    instance of the OutputBox that has been newly added
         */
        newOutput(x, y, refresh = true) {
            return this.newBox(x, y, new editorElements.OutputBox(this), refresh);
        }

        /**
         * Add a new Box to the canvas
         * @param  {number}  x              horizontal position of the box in SVG pixels
         * @param  {number}  y              vertical position of the box in SVG pixels
         * @param  {editorElements.Box}  object         instance of an object derived from the editorElements.Box class
         * @param  {Boolean} [refresh=true] if true, this.refresh() will be called after adding the box
         * @return {editorElements.Box}                 return the instance of the newly added object
         */
        newBox(x, y, object, refresh = true) {
            let index = this.boxes.length;

            this.boxes[index] = object;

            // translate the gate if x and y has been specified
            if (x && y) {
                let tr = new editorElements.Transform();
                tr.setTranslate(x, y);

                this.boxes[index].svgObj.addAttr({ transform: tr.get() });
            }

            this.appendElement(this.boxes[index], refresh);

            // if tutorial exists, call tutorial callback
            if (this.tutorial) {
                this.tutorial.onElementAdded(this.boxes[index].name);
            }

            return this.boxes[index];
        }

        /**
         * Remove a box from canvas based on the provided ID
         * @param {string} boxId id of the box that should be removed
         */
        removeBox(boxId) {
            let $gate = $('#' + boxId);

            // find the gate in svg's list of gates
            let gateIndex = -1;
            for (let i = 0; i < this.boxes.length; i++) {
                if (this.boxes[i].svgObj.id === boxId) {
                    gateIndex = i;
                    break;
                }
            }

            if (gateIndex > -1) {
                // remove all wires connected to this gate
                for (let i = 0; i < this.boxes[gateIndex].connectors.length; i++) {
                    this.removeWiresByConnectorId(this.boxes[gateIndex].connectors[i].id);
                }

                // remove the gate
                this.boxes.splice(gateIndex, 1);
                $gate.remove();

                // if tutorial exists, call tutorial callback
                if (this.tutorial) {
                    this.tutorial.onElementRemoved();
                }
            } else {
                console.error('Trying to remove an nonexisting box. Box id:', boxId);
            }
        }

        /**
         * Remove all boxes from the canvas
         */
        cleanCanvas() {
            // cannot simply iterate through the array because removeBox works with it

            // create an array of ids
            const ids = this.boxes.map(box => box.id);

            // remove all boxes by their ids
            for (const id of ids) {
                this.removeBox(id);
            }
        }

        /**
         * Create a new wire connecting the provided connectors
         * @param  {string}  fromId         id of the connector that the wire is attached to
         * @param  {string}  toId           id of the connector that the wire is attached to
         * @param  {Boolean} [refresh=true] if refresh is set to true, the SVG document will be reloaded after adding the wire
         * @return {editorElements.Wire}    instance of editorElements.Wire that has been added to the canvas
         */
        newWire(fromId, toId, refresh = true, route = true) {
            // wire must connect two distinct connectors
            if (fromId === toId) return undefined;

            let connectors = [this.getConnectorById(fromId), this.getConnectorById(toId)];

            // input connectors can be connected to one wire max
            connectors.forEach(conn => {
                if (conn.isInputConnector) this.removeWiresByConnectorId(conn.id);
            });
            let index = this.wires.length;

            try {
                this.wires[index] = new editorElements.Wire(this, fromId, toId, refresh, route);
            } catch (e) {
                this.messages.newErrorMessage(e);
                return undefined;
            }

            connectors.forEach(conn => {
                conn.addWireId(this.wires[index].svgObj.id);
            });

            this.appendElement(this.wires[index], refresh);
            this.moveToBackById(this.wires[index].svgObj.id);

            if (refresh) this.wires[index].updateWireState();

            return this.wires[index];
        }

        /**
         * get the coordinates of the specified connector
         * @param  {Connector}  connector      instance of {@link Connector}
         * @param  {Boolean} [snapToGrid=true] if true, the connector position will be snapped to the grid
         * @return {Object}                    point - object containing numeric attributes `x` and `y`
         */
        getConnectorPosition(connector, snapToGrid = true) {
            // connector.svgObj.id has to be called, else the getCoordinates does not work on the first call in Firefox 55
            const dummy = connector.svgObj.id; // eslint-disable-line no-unused-vars

            let $connector = connector.svgObj.$el;

            let position = $connector.position();

            position.left = this.viewbox.transformX(position.left);
            position.top = this.viewbox.transformY(position.top);

            let width = $connector.attr('width');
            let height = $connector.attr('height');

            let x = position.left + width / 2;
            let y = position.top + height / 2;
            if (snapToGrid) {
                x = this.snapToGrid(x);
                y = this.snapToGrid(y);
            }

            return { x: x, y: y };
        }

        /**
         * creates a new blackbox
         * @param  {number} x       horizontal position of the blackbox in SVG pixels
         * @param  {number} y       vertical position of the gate in SVG pixels
         * @param  {number} inputs  number of input pins of this blackbox
         * @param  {number} outputs number of output pins of this blackbox
         * @param  {Array} table   Array of arrays, each inner array contains list of [Logic.state](./module-Logic.html#.state)s,
         *                          that describe the combination of input pin and output pin states in the order from the top to bottom for both input and output connectors.
         *                          If we had an AND array as a blackbox, one of the states could be `[Logic.state.on, Logic.state.off, Logic.state.off]`
         *                          which means that if the first input connector is in the `on` state and the second connector is in the `off` state,
         *                          the state of the output connector will be `off`.
         *                          The array can be described as `[state for input conn 1, state for input conn 2, ..., state for output conn 1, state for output conn 2 ...]`.
         * @param  {string}  name   a name that will be displayed on the blackbox
         * @param  {boolean} [refresh=true] if true, this.refresh() will be called after adding the gate
         *
         * @return {editorElements.Blackbox} instance of {@link Blackbox} that has been added to the canvas
         */
        newBlackbox(inputs, outputs, table, name, x, y, refresh = true) {
            const index = this.boxes.length;

            this.boxes[index] = new editorElements.Blackbox(
                this,
                inputs,
                outputs,
                (...inputStates) => {
                    for (const line of table) {
                        const lineInputStates = line.slice(0, inputs);

                        // if every input state matches the corresponding input state in this line of the truth table
                        if (inputStates.every((value, index) => value === lineInputStates[index])) {
                            // return the rest of the line as output
                            return line.slice(inputs);
                        }
                    }
                    // if nothing matches, set all outputs to undefined
                    return Array.from(new Array(outputs), () => Logic.state.unknown);
                },
                name
            );

            if (x && y) {
                let tr = new editorElements.Transform();
                tr.setTranslate(x, y);

                this.boxes[index].svgObj.addAttr({ transform: tr.get() });
            }

            this.appendElement(this.boxes[index], refresh);

            return this.boxes[index];
        }

        /**
         * Find the correct instance of editorElements.Wire in the app's wires by the provided id
         * @param  {string} wireId id of the wire
         * @return {editorElements.Wire} instance of the wire
         */
        getWireById(wireId) {
            for (const wire of this.wires) {
                if (wire.svgObj.id === wireId) {
                    return wire;
                }
            }

            return false;
        }

        /**
         * Find all wires that are connected to the specified connector
         * @param  {string} connectorId id of the connector
         * @return {Set} set of ID's of the wires connected to this connector
         */
        getWiresByConnectorId(connectorId) {
            let connector = this.getConnectorById(connectorId);
            return connector.wireIds;
        }

        /**
         * Remove wire that has the provided ID
         * @param  {string} wireId ID of the wire that should be removed
         */
        removeWireById(wireId) {
            for (let i = 0; i < this.wires.length; ++i) {
                if (this.wires[i].svgObj.id === wireId) {
                    let { connectors } = this.wires[i];

                    for (let connector of connectors) {
                        connector.removeWireIdAndUpdate(wireId);
                    }

                    // start simulation from the input connector to
                    // refresh the network after this wire

                    let inputConnector = this.wires[i].connection.to.connector;
                    this.startNewSimulation(inputConnector, inputConnector.state);

                    this.wires[i].svgObj.$el.remove();
                    this.wires.splice(i, 1);

                    break;
                }
            }
        }

        /**
         * Remove all wires that are connected to the connector provided by its ID
         * @param  {string} connectorId ID of the connector
         */
        removeWiresByConnectorId(connectorId) {
            let connector = this.getConnectorById(connectorId);

            connector.wireIds.forEach(wireId => {
                let wire = this.getWireById(wireId);

                let { from, to } = wire.connection;

                // get the other connector that is the wire connected to
                let otherConnector = connectorId === from.id ? to.connector : from.connector;

                // delete the wire record from the other connector
                otherConnector.wireIds.delete(wireId);

                // remove the wire representation using jQuery
                $('#' + wireId).remove();

                // if otherConnector is an input connector, set its state to unknown
                if (otherConnector.isInputConnector) {
                    otherConnector.setState(Logic.state.unknown);
                    this.startNewSimulation(otherConnector, Logic.state.unknown);
                }
            });

            // clear the list of wire Ids
            connector.wireIds.clear();
            // if connector is an input connector, set its state to unknown
            if (connector.isInputConnector) {
                connector.setState(Logic.state.unknown);
                this.startNewSimulation(connector, Logic.state.unknown);
            }
        }

        /**
         * Find the correct instance of editorElements.Box in the app's boxes by the provided id
         * @param  {string} boxId id of the box
         * @return {editorElements.Box} instance of the box
         */
        getBoxById(boxId) {
            for (let i = 0; i < this.boxes.length; i++) {
                if (this.boxes[i].svgObj.id === boxId) {
                    return this.boxes[i];
                }
            }
            return undefined;
        }

        /**
         * Find the correct instance of editorElements.Box in the app's boxes by ID of a connector that belongs to this box
         * @param  {string} boxId id of the connector
         * @return {editorElements.Box} instance of the box
         */
        getBoxByConnectorId(connectorId) {
            for (let i = 0; i < this.boxes.length; i++) {
                if (this.boxes[i].getConnectorById(connectorId) !== undefined) {
                    return this.boxes[i];
                }
            }
            return false;
        }

        /**
         * Get instance of a connector based on it's ID (and also on an instance of editorElements.Wire if provided)
         *
         * The wire variable is used as heuristic: When we know the wire, we have to check only
         * two gates instead of all of them
         * @param  {string} connectorId id of the connector
         * @param  {editorElements.Wire} [wire]      instance of the Wire that is connected to this connector
         * @return {editorElements.Connector}        instance of the connector
         */
        getConnectorById(connectorId, wire = undefined) {
            if (wire !== undefined) {
                // we know the wire -- we can check only gates at the ends of this wire
                const { from, to } = wire.connection;

                if (from.id === connectorId) return from.connector;

                if (to.id === connectorId) return to.connector;
            } else {
                // we do not know the wire -- we have to check all gates
                for (const box of this.boxes) {
                    const connector = box.getConnectorById(connectorId);
                    if (connector) {
                        return connector;
                    }
                }
            }

            return undefined;
        }

        /**
         * Get the logical jQuery target based on the factual jQuery target.
         *
         * If the object, that user interacted with, is not a connector and is in a group,
         * return the group jQuery object instead of the original jQuery object.
         * @param  {target} target jQuery target of the object user interacted with
         * @return {target}        jQuery target of the object user wanted to interact with
         */
        getRealJQueryTarget(target) {
            let $target = $(target);
            if (!$target.hasClass('connector') && $target.parents('g').length > 0) {
                $target = $target.parent();
                while ($target.prop('tagName') !== 'G' && $target.prop('tagName') !== 'g') {
                    $target = $target.parent();
                }
            }
            return $target;
        }

        // returns the editorElement that user interacted with, the "target" argument is a jQuery element
        /**
         * Get instance of some object from editorElement based on the jQuery target
         * @param  {target} target jQuery target that user interacted with
         * @return {editorElements.NetworkElement} instance of an object derived from editorElements.NetworkElement that the user interacted with
         */
        getRealTarget(target) {
            if (target === undefined) {
                return undefined;
            }

            // eventy se museji zpracovat tady, protoze v SVG se eventy nepropaguji
            let $target = $(target);

            if ($target.hasClass('connector')) {
                // this is a connector, don't traverse groups
                return this.getConnectorById($target.attr('id'));
            } else if ($target.parents('g').length > 0) {
                // this element is in a group and it is not a connector

                // traversing up the DOM tree until we find the closest group
                let $parentGroup = $target.parent();
                while ($parentGroup.prop('tagName') !== 'G' && $parentGroup.prop('tagName') !== 'g') {
                    $parentGroup = $parentGroup.parent();
                }

                // try to match the jQuery element to the logical element using DOM classes

                if ($parentGroup.hasClass('box')) {
                    // return the corresponding box
                    return this.getBoxById($parentGroup.attr('id'));
                } else if ($parentGroup.hasClass('wire')) {
                    // return the corresponding wire
                    return this.getWireById($parentGroup.attr('id'));
                } else {
                    // found a group that contains the target, but this group does not match any known element types
                    return undefined;
                }
            } else {
                // element does not match any known element types
                return undefined;
            }
        }

        /**
         * Add an element to the canvas
         * @param  {editorElements.NetworkElement}  element Element that will be added on the canvas
         * @param  {Boolean} [refresh=true] if true, the SVG document will be reloaded after adding this element
         */
        appendElement(element, refresh = true) {
            this.appendJQueryObject(element.get(), refresh);
        }

        /**
         * Append a jQuery element to the SVG document (helper for this.appendElement)
         * @param  {object}  object         jQuery element that will be added to the SVG document
         * @param  {Boolean} [refresh=true] if true, the SVG document will be reloaded after adding this element
         */
        appendJQueryObject(object, refresh = true) {
            this.$svg.append(object);
            if (refresh) this.refresh();
        }

        /**
         * Add a new pattern to the definitions element in the SVG document
         * @param {svgObj.Pattern} pattern pattern that will be added to the <devs> element in the SVG document
         */
        addPattern(pattern) {
            this.$defs.append(pattern);
            this.refresh();
        }

        /**
         * Reload the SVG document (needed to display a newly appended jQuery object)
         */
        refresh() {
            this.$svg.html(this.$svg.html());
            console.log('SVG document has been reloaded.');
        }

        /**
         * Display the context menu on the specified position
         * @param  {number} x       horizontal position in CSS pixels
         * @param  {number} y       vertical position in CSS pixels
         * @param  {jQuery.element} $target the item user clicked on (used to display "remove this element"-type items in the menu)
         */
        displayContextMenu(x, y, $target) {
            this.contextMenu.display(x, y, $target);

            // if tutorial exists, call tutorial callback
            if (this.tutorial) {
                this.tutorial.onContextMenuOpened();
            }
        }

        /**
         * hide the context menu
         */
        hideContextMenu() {
            this.contextMenu.hide();
        }

        /**
         * snap a value to a grid
         * @param  {number} value value in SVG pixels
         * @return {number}       the value rounded to the closest number divisible by the grid size
         */
        snapToGrid(value) {
            return Math.round(value / this.gridSize) * this.gridSize;
        }

        /**
         * convert grid pixels to SVG pixels
         * @param  {number} value distance in grid pixels
         * @return {number}       distance in SVG pixels
         */
        gridToSVG(value) {
            return value * this.gridSize;
        }

        /**
         * convert SVG pixels to grid pixels
         * @param {number} value distance in SVG pixels
         * @return {number}      distance in grud pixels
         */
        SVGToGrid(value) {
            return value / this.gridSize;
        }

        /**
         * static function for snapping a value to a grid
         * @param  {number} value value in SVG pixels
         * @param  {number} gridSize size of the grid in SVG pixels
         * @return {number}       the value rounded to the closest number divisible by the grid size
         */
        static snapToGrid(value, gridSize) {
            return Math.round(value / gridSize) * gridSize;
        }

        /**
         * move an element to the front in the canvas
         * @param  {string} objId id of the element
         */
        moveToFrontById(objId) {
            this.$svg.append($('#' + objId));
        }

        /**
         * move an element to the back in the canvas
         * @param  {string} objId id of the element
         */
        moveToBackById(objId) {
            $('#' + this.background.id).after($('#' + objId));
        }

        /**
         * get set of nodes, that cannot be used for wiring at any circumstances
         * @return {Set} set of nodes (objects containing x and y coordinates) that are not suitable for wiring
         */
        getNonRoutableNodes() {
            let blockedNodes = new Set();
            // for each box
            for (const box of this.boxes) {
                const translate = box.getGridPixelTransform().getTranslate();

                // for each item in blockedNodes (set of blocked nodes with coordinates relative
                // to the left upper corner of rect; unit used is "one gridSize") convert the coordinates
                // to absolute (multiple with gridSize and add position of rect) and add the result to the set
                for (const node of box.blockedNodes) {
                    blockedNodes.add({
                        x: translate.x + node.x,
                        y: translate.y + node.y
                    });
                }
            }

            // FOR DEBUG ONLY: display the non routable nodes
            /*

            if(this.nodeDisplay) {
                for (const rectangleId of this.nodeDisplay) {
                    $(`#${rectangleId}`).remove();
                }
            }

            this.nodeDisplay = [];

            let first = true;

            for (const node of blockedNodes) {
                const x = this.gridToSVG(node.x);
                const y = this.gridToSVG(node.y);

                const w = 4;
                const p = w / 2;

                const nodeRectangle = new Rectangle(x - p, y - p, w, w, first ? "blue" : "red", "none")
                this.nodeDisplay.push(nodeRectangle.id);
                this.appendElement(nodeRectangle, false);

                first = false;
            }

            this.refresh();

            // */
            // END FOR DEBUG ONLY

            // return the set
            return blockedNodes;
        }

        /**
         * get set of nodes, that are inconvenient for wiring, but can be used, just are not preferred
         * @return {Set} set of nodes (objects containing x and y coordinates) that are not preferred for wiring
         */
        getInconvenientNodes(ignoreWireId) {
            let inconvenientNodes = new Set();
            // for each wire

            for (const wire of this.wires) {
                if (ignoreWireId === undefined || ignoreWireId !== wire.id) {
                    if (wire.inconvenientNodes) {
                        for (const node of wire.inconvenientNodes) {
                            inconvenientNodes.add(node);
                        }
                    }
                }
            }

            // FOR DEBUG ONLY: display the inconvenient nodes
            /*

            if(this.inconvenientNodeDisplay) {
                for (const rectangleId of this.inconvenientNodeDisplay) {
                    $(`#${rectangleId}`).remove();
                }
            }

            this.inconvenientNodeDisplay = [];

            for (const node of inconvenientNodes) {
                const x = this.gridToSVG(node.x);
                const y = this.gridToSVG(node.y);

                const w = 4;
                const p = w / 2;

                const nodeRectangle = new Rectangle(x - p, y - p, w, w, "orange", "none")
                this.inconvenientNodeDisplay.push(nodeRectangle.id);
                this.appendElement(nodeRectangle, false);
            }

            this.refresh();

            // */
            // END FOR DEBUG ONLY

            // return the set
            return inconvenientNodes;
        }
    }