PMA.UI Documentation by Pathomation

components/js/annotations.js

import * as olFormat from 'ol/format';
import * as olInteraction from 'ol/interaction';
import Overlay from 'ol/Overlay';
import * as Observable from 'ol/Observable';
import { Icon, Fill, Stroke, Circle, RegularShape, Style } from 'ol/style';
import { MultiPoint, Polygon, Circle as GeomCircle } from 'ol/geom';
import { fromCircle } from 'ol/geom/Polygon';
import Collection from 'ol/Collection';
import { pointerMove, click, singleClick, noModifierKeys, shiftKeyOnly, never, altKeyOnly, primaryAction } from 'ol/events/condition';
import { checkBrowserCompatibility } from '../../view/helpers';
import { Viewport } from '../../view/viewport';
import { Resources } from '../../resources/resources';
import { DefaultFillColor } from '../../view/definitions';
import { Annotation, AnnotationState, AnnotationTools } from '../../view/definitions';
import { Events } from './components';
import $ from 'jquery';
import * as jscolor from '../../../lib/jscolor/jscolor';
import { ol } from '../../view/definitionsOl';
import union from "@turf/union";
import difference from "@turf/difference";
import booleanIntersects from "@turf/boolean-intersects";
import booleanContains from "@turf/boolean-contains";
import ol_interaction_Transform from "../../view/interactions/transform";
import ol_interaction_Brush from "../../view/interactions/brush";
import MagicWandInteraction from "../../view/interactions/magicWand";
import Feature from "ol/Feature";

var ellipseSides = 32;

function removeDuplicatePoints(points, tolerance = 1e-6) {
    // Helper function to calculate the distance between two points
    function distance(p1, p2) {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
    }

    // Create an array to store the filtered points
    const filteredPoints = [];

    // Iterate through each point in the array
    for (let i = 0; i < points.length; i++) {
        const currentPoint = points[i];
        let isDuplicate = false;

        // Compare current point with all previously added points
        for (let j = 0; j < filteredPoints.length; j++) {
            const existingPoint = filteredPoints[j];

            // If the distance between the points is less than the tolerance, mark as duplicate
            if (distance(currentPoint, existingPoint) < tolerance) {
                isDuplicate = true;
                break;
            }
        }

        // If not a duplicate, add the current point to the filtered points
        if (!isDuplicate) {
            filteredPoints.push(currentPoint);
        }
    }

    return filteredPoints;
}

function getCoordinates(feature) {
    if (!feature.getGeometry) {
        return [];
    }

    let geom = feature.getGeometry().getCoordinates();
    if (!geom) {
        return [];
    }

    if (geom.length === 1 && geom[0] instanceof Array) {
        geom = geom[0];
    }

    geom = geom.map(c => { return { x: c[0], y: c[1] } });
    if (geom.length > 1 && geom[0].x === geom[geom.length - 1].x && geom[0].y === geom[geom.length - 1].y) {
        geom.pop();
    }

    return removeDuplicatePoints(geom);
}

function isSelfIntersecting(polygon) {
    // Helper function to check if two line segments (p1, p2) and (q1, q2) intersect
    function doLinesIntersect(p1, p2, q1, q2) {
        // Check if point p is on segment (q1, q2)
        function onSegment(p, q1, q2) {
            return (
                Math.min(q1.x, q2.x) <= p.x && p.x <= Math.max(q1.x, q2.x) &&
                Math.min(q1.y, q2.y) <= p.y && p.y <= Math.max(q1.y, q2.y)
            );
        }

        // Calculate orientation of ordered triplet (p, q, r)
        function orientation2(p, q, r) {
            const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
            if (Math.abs(val) <= 0.00001) {
                return 0;  // Collinear
            }

            return val;
        }

        // Calculate orientation of ordered triplet (p, q, r)
        function orientation(p, q, r) {
            const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
            if (Math.abs(val) <= 0.00001) {
                return 0;  // Collinear
            }

            return val > 0 ? 1 : 2;   // Clockwise or counterclockwise
        }

        const o1 = orientation(p1, p2, q1);
        const o2 = orientation(p1, p2, q2);
        const o3 = orientation(q1, q2, p1);
        const o4 = orientation(q1, q2, p2);

        // General case: If orientations are different, they intersect
        if (o1 !== o2 && o3 !== o4) {
            console.log(orientation2(p1, p2, q1));
            console.log(orientation2(p1, p2, q2));
            console.log(orientation2(q1, q2, p1));
            console.log(orientation2(q1, q2, p2));
            return true;
        }

        // Special cases: Check if the segments are collinear and overlap
        if (o1 === 0 && onSegment(q1, p1, p2)) {
            return true;
        }

        if (o2 === 0 && onSegment(q2, p1, p2)) {
            return true;
        }

        if (o3 === 0 && onSegment(p1, q1, q2)) {
            return true;
        }

        if (o4 === 0 && onSegment(p2, q1, q2)) {
            return true;
        }

        return false;
    }

    // Iterate over all edges of the polygon and check for intersection
    const n = polygon.length;
    for (let i = 0; i < n; i++) {
        const p1 = polygon[i];
        const p2 = polygon[(i + 1) % n];  // Next point, wrap around to the first

        for (let j = i + 2; j < n; j++) {
            // Ensure that non-adjacent segments are compared (i+1 and i-1 should not be checked)
            if (i === 0 && j === n - 1) {
                continue;  // Skip the check between the first and last edge
            }

            const q1 = polygon[j];
            const q2 = polygon[(j + 1) % n];  // Next point, wrap around to the first

            if (doLinesIntersect(p1, p2, q1, q2)) {
                return true;  // Intersection found
            }
        }
    }

    return false;  // No intersection
}



function supportsColorPicker() {
    var colorInput;
    colorInput = $('<input type="color" value="!" />')[0];
    return colorInput.type === 'color' && colorInput.value !== '!';
}

function toggleDragPanInteraction(map, enabled) {
    map.getInteractions().forEach(function (element) {
        if (element instanceof olInteraction.DragPan) {
            element.setActive(enabled);
            return;
        }
    });
}

function getStrokeColor() {
    if (this.element === null) {
        return "#000000";
    }

    var scolor = $(this.element).find("li.draw a.active").data("color");
    if (!scolor) {
        return "#000000";
    } else {
        return scolor;
    }
}

function getPenSize() {
    if (this.element === null) {
        return 1;
    }

    var width = parseInt($(this.element).find("li.draw a.active").data("size"));
    if (isNaN(width) || width < 1) {
        width = 1;
    }

    return width;
}

function getAnnotationStyle(strokeColor, width, fillColor, opt_icon, feature, styleGeometryFunction) {
    var imageStyle = null;

    if (!fillColor) {
        fillColor = DefaultFillColor;
    }

    if (!strokeColor) {
        strokeColor = getStrokeColor.call(this);
    }

    if (!width) {
        width = getPenSize.call(this);
    }

    var fill = new Fill({
        color: fillColor
    });

    var stroke = new Stroke({
        color: strokeColor,
        width: width
    });

    if (!opt_icon) {
        imageStyle = new Circle({
            fill: fill,
            stroke: stroke,
            radius: 5
        });
    } else {
        imageStyle = new Icon({
            anchor: [0.5, 0.5],
            anchorXUnits: 'fraction',
            anchorYUnits: 'fraction',
            opacity: 1,
            src: this.viewport.options.annotations.imageBaseUrl + opt_icon,
            scale: isNaN(this.viewport.options.annotations.imageScale) ? 1 : this.viewport.options.annotations.imageScale
        });
    }

    return new Style({
        image: imageStyle,
        fill: fill,
        stroke: stroke,
        text: this.viewport.getAnnotationTextStyle(feature),
        geometry: styleGeometryFunction
    });
}

// fixes the y-coordinates of annotations by inverting it
function annotationTransform(input, output, dimension) {
    for (var i = 0; i < input.length; i += dimension) {
        var x = input[i];
        var y = input[i + 1];

        if (this.flip.vertically !== true) {
            y = this.extent[3] - y;
        }

        if (this.flip.horizontally === true) {
            x = this.extent[2] - x;
        }

        output[i] = x;
        output[i + 1] = y;
    }
}

function styleCircleGeometryFunction(feature) {
    var g = feature.getGeometry();

    if (g.getType() === "Point") {
        return new GeomCircle(g.getFirstCoordinate(), this.size[0] / 2);
    }

    return g;
}

function styleBoxGeometryFunction(feature) {
    var g = feature.getGeometry();

    if (g.getType() === "Point") {
        var dx = Math.abs(this.size[0] / 2);
        var dy = Math.abs(this.size[1] / 2);
        var c = g.getFirstCoordinate();

        return new Polygon([
            [
                [c[0] - dx, c[1] - dy],
                [c[0] - dx, c[1] + dy],
                [c[0] + dx, c[1] + dy],
                [c[0] + dx, c[1] - dy],
                [c[0] - dx, c[1] - dy]
            ]
        ]);
    }

    return g;
}

function multiPointGeometryFunction(coordinates, geometry) {
    if (!geometry) {
        geometry = new MultiPoint(coordinates);
    } else {
        geometry.setCoordinates(coordinates);
    }

    return geometry;
}

function boxGeometryFunction(coordinates, geometry) {
    var options = this;
    if (options.size) {
        // when we are drawing a rectangle with fixed size, the geometry type is "Point" coordinates has just one X,Y pair, which is our center
        var dx = Math.abs(options.size[0] / 2);
        var dy = Math.abs(options.size[1] / 2);
        var c = coordinates;

        return new Polygon([
            [
                [c[0] - dx, c[1] - dy],
                [c[0] - dx, c[1] + dy],
                [c[0] + dx, c[1] + dy],
                [c[0] + dx, c[1] - dy],
                [c[0] - dx, c[1] - dy]
            ]
        ]);
    }



    var start = coordinates[0];
    var end = coordinates[1];

    if (!start || !end) {
        return null;
    }
    let coords = [
        [start, [start[0], end[1]], end, [end[0], start[1]], start]
    ];

    if (!geometry) {
        geometry = new Polygon(coords);
    } else {
        geometry.setCoordinates(coords);
    }

    return geometry;
}

function circleGeometryFunction(coordinates, opt_geometry) {
    if (this.size) {
        // when we are drawing a rectangle with fixed size, the geometry type is "Point" coordinates has just one X,Y pair, which is our center
        // the size option gives us the circle's diameter
        return new GeomCircle(coordinates, this.size[0] / 2.0);
    }

    var circle = opt_geometry ? /** @type {Circle} */ (opt_geometry) : new GeomCircle([NaN, NaN]);
    var c = coordinates[0];
    var dx = coordinates[0][0] - coordinates[1][0];
    var dy = coordinates[0][1] - coordinates[1][1];
    var radius = Math.sqrt(dx * dx + dy * dy);

    circle.setCenterAndRadius(c, radius);
    return circle;
}

function ellipseGeometryFunction(coordinates, geometry) {
    var start = coordinates[0];
    var end = coordinates[1];

    if (!start || !end) {
        return null;
    }

    var radiusY = (start[1] - end[1]) / 2;
    var s = [start[0], start[1] + radiusY];
    var e = end;

    var center = [(e[0] + s[0]) / 2, (e[1] + s[1]) / 2];
    var radius = [center[0] - e[0], center[1] - e[1]];
    var ellipseCoords = [];
    var step = 2 * Math.PI / ellipseSides;

    for (var i = 0; i < ellipseSides; i++) {
        var t = i * step;
        var p = [Math.round(center[0] + radius[0] * Math.cos(t)), Math.round(center[1] + radius[1] * Math.sin(t))];

        ellipseCoords.push(p);
    }

    ellipseCoords.push(ellipseCoords[0]);

    if (!geometry) {
        geometry = new Polygon([ellipseCoords]);
    } else {
        geometry.setCoordinates([ellipseCoords]);
    }

    return geometry;
}

// 3d cross product
function crossProduct(v1, v2) {
    return [
        (v1[1] * v2[2]) - (v1[2] * v2[1]),
        (v1[2] * v2[0]) - (v1[0] * v2[2]),
        (v1[0] * v2[1]) - (v1[1] * v2[0])
    ];
}

function vectorLength(v) {
    if (v.length === 3) {
        return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    }

    return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
}

function normalizeVector(v, length) {
    if (v.length === 2) {
        v.push(0);
    }

    if (!length) {
        length = vectorLength(v);
    }

    if (length > 0.00001) {
        var inv = 1.0 / length;
        v[0] *= inv;
        v[1] *= inv;
        v[2] *= inv;
    }

    if (v.length === 3) {
        return [v[0], v[1], v[2]];
    }

    return [v[0], v[1]];
}

// eslint-disable-next-line no-unused-vars
function vectorMultScalar(v, scalar) {
    if (v.length === 3) {
        return [scalar * v[0], scalar * v[1], scalar * v[2]];
    }

    return [scalar * v[0], scalar * v[1]];
}

// calculates v2 - v1
function vectorDiff(v1, v2) {
    if (v1.length === 3 && v2.length === 3) {
        return [v2[0] - v1[0], v2[1] - v1[1], v2[2] - v1[2]];
    }

    return [v2[0] - v1[0], v2[1] - v1[1]];
}

function arrowGeometryFunction(coordinates, geometry) {
    var start = coordinates[1];
    var end = coordinates[0];

    var axisZ = [0, 0, 1];

    var diff = vectorDiff(start, end);
    var len = vectorLength(diff);
    var normDirection = normalizeVector(diff, len);

    // a normalized vector, vertical to the arrow axis
    var vertical = crossProduct(normDirection, axisZ);

    // Back        Body          Spearhead
    //                          C
    //                          |\
    //    A____________________B| \
    //    |                     |  \D
    //    |_____________________|  /
    //    G                    F| /
    //                          |/
    //                          E

    // body width is set to 15% of the total length
    var bodyWidth = len * 0.15;
    var bodyLength = len * 0.75;
    // var spearheadLength = len * 0.25;
    var spearheadWidth = len * 0.15;

    var a = [start[0] - (vertical[0] * bodyWidth / 2), start[1] - (vertical[1] * bodyWidth / 2)];
    var g = [start[0] + (vertical[0] * bodyWidth / 2), start[1] + (vertical[1] * bodyWidth / 2)];

    var b = [a[0] + normDirection[0] * bodyLength, a[1] + normDirection[1] * bodyLength];
    var f = [g[0] + normDirection[0] * bodyLength, g[1] + normDirection[1] * bodyLength];

    var c = [b[0] - (vertical[0] * spearheadWidth / 2), b[1] - (vertical[1] * spearheadWidth / 2)];
    var e = [f[0] + (vertical[0] * spearheadWidth / 2), f[1] + (vertical[1] * spearheadWidth / 2)];

    var d = [end[0], end[1]];

    let coords = [
        [a, b, c, d, e, f, g, a]
    ];

    if (!geometry) {
        geometry = new Polygon(coords);
    } else {
        geometry.setCoordinates(coords);
    }

    return geometry;
}

function showDrawingControls(show, annotationType, feature) {
    if (this.drawingControlsContainer && this.viewport.element.contains(this.drawingControlsContainer)) {
        this.viewport.element.removeChild(this.drawingControlsContainer);
    }

    this.drawingControlsContainer = null;
    this.endDrawingButton = null;
    this.removeLastButton = null;
    this.continueDrawingButton = null;
    this.cancelDrawingButton = null;
    this.counterButton = null;

    if (this.changeFeatureKey != null) {
        Observable.unByKey(this.changeFeatureKey);
    }

    if (!show) {
        return;
    }

    this.changeFeatureKey = null;
    var self = this;
    this.drawingControlsContainer = document.createElement("div");
    this.drawingControlsContainer.className = "ol-control pma-ui-viewport-annotations-drawing";

    this.cancelDrawingButton = document.createElement("button");
    this.cancelDrawingButton.innerHTML = Resources.translate("Cancel");

    $(this.cancelDrawingButton).click(function (evt) {
        evt.preventDefault();
        showDrawingControls.call(self, false, annotationType);
        self.finishDrawing(false, annotationType);
    });


    // do not add the toolbar while drawing a compound polygon
    if (!(annotationType === Annotation.CompoundFreehand && self.drawing)) {
        this.viewport.element.appendChild(this.drawingControlsContainer);
    }

    if (annotationType === Annotation.MultiPoint) {
        this.removeLastButton = document.createElement("button");
        this.removeLastButton.innerHTML = Resources.translate("Remove last point");
        this.drawingControlsContainer.appendChild(this.removeLastButton);

        $(this.removeLastButton).click(function (evt) {
            evt.preventDefault();
            if (self.draw) {
                self.draw.removeLastPoint();
            }
        });

        this.endDrawingButton = document.createElement("button");
        this.endDrawingButton.innerHTML = Resources.translate("Finish");

        $(this.endDrawingButton).click(function (evt) {
            evt.preventDefault();
            showDrawingControls.call(self, false, Annotation.MultiPoint);
            self.finishDrawing(true);
        });

        this.drawingControlsContainer.appendChild(this.endDrawingButton);

        this.counterButton = document.createElement("button");
        this.counterButton.innerHTML = "0";

        if (feature) {
            if (feature.getGeometry() && feature.getGeometry().getCoordinates()) {
                this.counterButton.innerHTML = feature.getGeometry().getCoordinates().length;
            }

            if (this.changeFeatureKey) {
                Observable.unByKey(this.changeFeatureKey);
            }

            this.changeFeatureKey = feature.on("change", function () {
                if (self.counterButton) {
                    self.counterButton.innerHTML = feature.getGeometry().getCoordinates().length - 1;
                }
            });
        }

        this.drawingControlsContainer.insertBefore(this.counterButton, this.removeLastButton);
    } else if (annotationType === Annotation.CompoundFreehand) {
        if (!self.drawing) {
            this.continueDrawingButton = document.createElement("button");
            this.continueDrawingButton.innerHTML = Resources.translate("Draw");
            this.drawingControlsContainer.appendChild(this.continueDrawingButton);

            $(this.continueDrawingButton).click(function (evt) {
                evt.preventDefault();
                self.startDrawing({
                    type: Annotation.CompoundFreehand,
                    color: self.lastAnnotationStyle.color,
                    fillColor: self.lastAnnotationStyle.fillColor,
                    lineThickness: self.lastAnnotationStyle.penSize,
                    iconRelativePath: self.lastAnnotationStyle.iconPath,
                    rejectSelfIntersecting: self.lastDrawingOptions.rejectSelfIntersecting,
                });

                if (self.continueDrawingButton) {
                    self.continueDrawingButton.style.display = "none";
                }
            });
        }

        this.removeLastButton = document.createElement("button");
        this.removeLastButton.innerHTML = Resources.translate("Remove last");


        $(this.removeLastButton).click(function (evt) {
            evt.preventDefault();
            if (self.compoundFreehandList.length > 0) {
                var ft = self.compoundFreehandList.pop();
                self.deleteAnnotation(ft.getId());
            }
        });

        this.endDrawingButton = document.createElement("button");
        this.endDrawingButton.innerHTML = Resources.translate("Finish");

        $(this.endDrawingButton).click(function (evt) {
            evt.preventDefault();
            showDrawingControls.call(self, false, Annotation.CompoundFreehand);
            self.finishDrawing(true);
            self.fireEvent(Events.AnnotationDrawingFinished, { cancelled: false });

            if (self.compoundFreehandList.length > 0) {
                self.mergeSelection(self.compoundFreehandList);
                self.compoundFreehandList = [];
            }
        });

        this.drawingControlsContainer.appendChild(this.endDrawingButton);
        this.drawingControlsContainer.appendChild(this.removeLastButton);
    }

    this.drawingControlsContainer.appendChild(this.cancelDrawingButton);
}

function showEditingControls(show, editType, feature) {
    if (this.editingControlsContainer && this.viewport.element.contains(this.editingControlsContainer)) {
        this.viewport.element.removeChild(this.editingControlsContainer);
    }

    this.editingControlsContainer = null;
    this.endEditingButton = null;
    this.cancelEditingButton = null;
    this.saveEditingButton = null;
    this.wandCounterButton = null;
    this.wandUndoEditingButton = null;
    this.wandSensitivityIncreaseEditingButton = null;
    this.wandSensitivityDecreaseEditingButton = null;
    this.brushSizeIncreaseEditingButton = null;
    this.brushSizeDecreaseEditingButton = null;
    this.brushTypeCircleEditingButton = null;
    this.brushTypeSquareEditingButton = null;

    if (!show) {
        return;
    }

    var self = this;
    this.editingControlsContainer = document.createElement("div");
    this.editingControlsContainer.className = "ol-control pma-ui-viewport-annotations-drawing";

    this.cancelEditingButton = document.createElement("button");
    this.cancelEditingButton.innerHTML = Resources.translate("Stop editing");

    $(this.cancelEditingButton).click(function (evt) {
        evt.preventDefault();
        showEditingControls.call(self, false, editType);
        self.stopTool();
    });

    this.viewport.element.appendChild(this.editingControlsContainer);

    if (editType === AnnotationTools.Modify && feature && feature.getGeometry() && feature.getGeometry().getType() === "MultiPoint") {
        this.addPointEditingButton = document.createElement("button");
        this.addPointEditingButton.innerHTML = Resources.translate("Add point(s)");

        $(this.addPointEditingButton).click(function (evt) {
            evt.preventDefault();
            $(self.addPointEditingButton).blur();
            $(self.removePointEditingButton).removeClass("active");
            $(self.addPointEditingButton).addClass("active");
            $(self.movePointEditingButton).removeClass("active");
            self.edit.condition_ = function (event) {
                if (event.type !== "pointerdown" || !feature) return primaryAction(event);
                var newPoint = event.coordinate;
                var coordinates = feature.getGeometry().getCoordinates();
                coordinates.push(newPoint);
                feature.getGeometry().setCoordinates(coordinates);
                feature.metaData.State = feature.metaData.State === AnnotationState.Added ? AnnotationState.Added : AnnotationState.Modified;
            }
        });

        this.removePointEditingButton = document.createElement("button");
        this.removePointEditingButton.innerHTML = Resources.translate("Remove point(s)");

        $(this.removePointEditingButton).click(function (evt) {
            evt.preventDefault();
            $(self.removePointEditingButton).blur();
            $(self.removePointEditingButton).addClass("active");
            $(self.addPointEditingButton).removeClass("active");
            $(self.movePointEditingButton).removeClass("active");
            self.edit.condition_ = function (event) {
                if (event.type !== "pointerdown") return altKeyOnly(event) && singleClick(event);
                var f = event.map.getFeaturesAtPixel(event.pixel, {
                    hitTolerance: 10
                });
                if (!f || !Array.isArray(f) || f.length <= 1) return altKeyOnly(event) && singleClick(event);
                var point = f.find(x => x.getGeometry().getType() === "Point");
                if (point) {
                    var coordinates = feature.getGeometry().getCoordinates();
                    var pointCoordinates = point.getGeometry().getCoordinates();
                    var removeIndex = -1;
                    for (var i = 0; i < coordinates.length; i++) {
                        if (pointCoordinates[0] === coordinates[i][0] && pointCoordinates[1] === coordinates[i][1]) {
                            removeIndex = i;
                        }
                    }
                    if (removeIndex >= 0) {
                        if (feature.getGeometry().getCoordinates().length > 2) {
                            coordinates.splice(removeIndex, 1);
                            feature.getGeometry().setCoordinates(coordinates);
                            feature.metaData.State = feature.metaData.State === AnnotationState.Added ? AnnotationState.Added : AnnotationState.Modified;
                        }
                    }
                }
            }
        });

        this.movePointEditingButton = document.createElement("button");
        this.movePointEditingButton.innerHTML = Resources.translate("Move point(s)");
        this.movePointEditingButton.classList.add("active");

        $(this.movePointEditingButton).click(function (evt) {
            evt.preventDefault();
            $(self.movePointEditingButton).blur();
            $(self.removePointEditingButton).removeClass("active");
            $(self.addPointEditingButton).removeClass("active");
            $(self.movePointEditingButton).addClass("active");
            self.edit.condition_ = primaryAction;
        });

        this.editingControlsContainer.appendChild(this.addPointEditingButton);
        this.editingControlsContainer.appendChild(this.removePointEditingButton);
        this.editingControlsContainer.appendChild(this.movePointEditingButton);
    }

    if (editType === AnnotationTools.Wand) {
        this.wandSensitivityIncreaseEditingButton = document.createElement("button");
        this.wandSensitivityIncreaseEditingButton.innerHTML = "&nbsp;+&nbsp;";
        this.wandSensitivityIncreaseEditingButton.title = Resources.translate("Increase color sensitivity");
        $(this.wandSensitivityIncreaseEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.setColorSensitivity(self.edit.getColorSensitivity() + 1);
            if (self.wandCounterButton) {
                self.wandCounterButton.innerHTML = self.edit.getColorSensitivity();
            }
        });


        this.wandCounterButton = document.createElement("button");
        this.wandCounterButton.innerHTML = "15";
        this.wandSensitivityIncreaseEditingButton.title = Resources.translate("Current color sensitivity");

        this.wandSensitivityDecreaseEditingButton = document.createElement("button");
        this.wandSensitivityDecreaseEditingButton.innerHTML = "&nbsp;-&nbsp;";
        this.wandSensitivityDecreaseEditingButton.title = Resources.translate("Decrease color sensitivity");
        $(this.wandSensitivityDecreaseEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.setColorSensitivity(self.edit.getColorSensitivity() - 1);
            if (self.wandCounterButton) {
                self.wandCounterButton.innerHTML = self.edit.getColorSensitivity();
            }
        });

        this.wandUndoEditingButton = document.createElement("button");
        this.wandUndoEditingButton.innerHTML = Resources.translate("Undo");
        this.wandUndoEditingButton.title = Resources.translate("Undo previous change to selection");
        $(this.wandUndoEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.historyUndo();
        });

        this.wandInvertSelectionEditingButton = document.createElement("button");
        this.wandInvertSelectionEditingButton.innerHTML = Resources.translate("Invert selection");
        $(this.wandInvertSelectionEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.invertSelection();
        });

        this.saveEditingButton = document.createElement("button");
        this.saveEditingButton.innerHTML = Resources.translate("Create annotation from selection");
        $(this.saveEditingButton).click(function (evt) {
            evt.preventDefault();
            self.saveMagicWandAnnotation();
        });

        this.editingControlsContainer.appendChild(this.wandSensitivityIncreaseEditingButton);
        this.editingControlsContainer.appendChild(this.wandCounterButton);
        this.editingControlsContainer.appendChild(this.wandSensitivityDecreaseEditingButton);
        this.editingControlsContainer.appendChild(this.wandUndoEditingButton);
        this.editingControlsContainer.appendChild(this.wandInvertSelectionEditingButton);
        this.editingControlsContainer.appendChild(this.saveEditingButton);
    }

    if (editType === AnnotationTools.Brush || editType === AnnotationTools.Eraser) {
        this.brushSizeDecreaseEditingButton = document.createElement("button");
        this.brushSizeDecreaseEditingButton.innerHTML = "&nbsp;-&nbsp;";
        this.brushSizeDecreaseEditingButton.title = Resources.translate("Decrease brush tip size");
        $(this.brushSizeDecreaseEditingButton).click(function (evt) {
            evt.preventDefault();
            var size = self.edit.getBrushSize();
            size /= 1.5;
            size = size <= 0 ? 1 : size;
            self.edit.setBrushSize(size);
            self.brushSizeInfoEditingButton.innerHTML = getFormattedBrushTip.call(self);
        });
        this.editingControlsContainer.appendChild(this.brushSizeDecreaseEditingButton);

        this.brushSizeInfoEditingButton = document.createElement("button");
        this.brushSizeInfoEditingButton.innerHTML = getFormattedBrushTip.call(this);
        this.brushSizeInfoEditingButton.disabled = true;
        this.brushSizeInfoEditingButton.style = "pointer-events: none;";
        this.brushSizeInfoEditingButton.title = Resources.translate("Brush tip info");
        $(this.brushSizeInfoEditingButton).click(function (evt) {
            evt.preventDefault();
        });
        this.editingControlsContainer.appendChild(this.brushSizeInfoEditingButton);

        this.brushSizeIncreaseEditingButton = document.createElement("button");
        this.brushSizeIncreaseEditingButton.innerHTML = "&nbsp;+&nbsp;";
        this.brushSizeIncreaseEditingButton.title = Resources.translate("Increase brush tip size");
        $(this.brushSizeIncreaseEditingButton).click(function (evt) {
            evt.preventDefault();
            var size = self.edit.getBrushSize();
            size *= 1.5;
            self.edit.setBrushSize(size);
            self.brushSizeInfoEditingButton.innerHTML = getFormattedBrushTip.call(self);
        });
        this.editingControlsContainer.appendChild(this.brushSizeIncreaseEditingButton);

        this.brushTypeCircleEditingButton = document.createElement("button");
        this.brushTypeCircleEditingButton.innerHTML = "&nbsp;&#9675;&nbsp;";
        this.brushTypeCircleEditingButton.title = Resources.translate("Change brush tip to circle");
        $(this.brushTypeCircleEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.setBrushType('circle');
            self.brushSizeInfoEditingButton.innerHTML = getFormattedBrushTip.call(self);
        });
        this.editingControlsContainer.appendChild(this.brushTypeCircleEditingButton);

        this.brushTypeSquareEditingButton = document.createElement("button");
        this.brushTypeSquareEditingButton.innerHTML = "&nbsp;&#9633;&nbsp;";
        this.brushTypeSquareEditingButton.title = Resources.translate("Change brush tip to square");
        $(this.brushTypeSquareEditingButton).click(function (evt) {
            evt.preventDefault();
            self.edit.setBrushType('square');
            self.brushSizeInfoEditingButton.innerHTML = getFormattedBrushTip.call(self);
        });
        this.editingControlsContainer.appendChild(this.brushTypeSquareEditingButton);
    }

    this.editingControlsContainer.appendChild(this.cancelEditingButton);
}

function createMeasureTooltip() {
    var el = document.createElement('div');
    el.className = 'pma-ui-viewport-tooltip pma-ui-viewport-tooltip-measure';
    var measureTooltip = new Overlay({
        element: el,
        offset: [0, -15],
        positioning: 'bottom-center'
    });

    this.viewport.map.addOverlay(measureTooltip);
    measureTooltip.element = el;

    // var self = this;
    // addEvent(el, 'click', function () {
    //     self.viewport.map.removeOverlay(measureTooltip);
    //     self.viewport.annotationsLayer.getSource().removeFeature(measureTooltip.sketch);

    //     el.parentNode.removeChild(el);
    //     el = null;
    // });

    return measureTooltip;
}

/**
 * Instructs the viewport to enter annotation drawing mode
 * @param {Annotations~startDrawingOptions} options - Options to start drawing
 * @ignore
 */
function addInteraction(options) {
    if (options.type !== Annotation.CompoundFreehand) {
        this.compoundFreehandList = [];
    }

    showDrawingControls.call(this, false, options.type);

    var type = options.type;
    var shouldHideControls = options.hideControls === true;
    var maxPoints, minPoints, geometryFunction, finishCondition, condition = noModifierKeys,
        freehandCondition = shiftKeyOnly;

    this.stopDrawingOnMouseUp = false;

    this.lastAnnotationStyle = {
        color: options.color,
        fillColor: options.fillColor,
        penSize: options.lineThickness,
        iconPath: options.iconRelativePath,
    };

    this.lastDrawingOptions = {
        drawingType: options.type,
        rejectSelfIntersecting: options.rejectSelfIntersecting,
    };

    if (this.selectionAdded === true) {
        this.selectionAdded = false;
        this.hoverInteraction.getFeatures().clear();
        this.viewport.map.removeInteraction(this.hoverInteraction);

        this.selectInteraction.getFeatures().clear();
        this.viewport.map.removeInteraction(this.selectInteraction);
    }

    var styleGeometryFunction;
    switch (type) {
        case Annotation.Rectangle:
            geometryFunction = boxGeometryFunction.bind(options);
            if (options.size) {
                maxPoints = 1;
                type = "Point";
                styleGeometryFunction = styleBoxGeometryFunction.bind(options);
            }
            else {
                maxPoints = 2;
                type = "LineString";
            }

            break;
        case Annotation.Arrow:
            geometryFunction = arrowGeometryFunction;
            maxPoints = 2;
            type = "LineString";
            break;
        case Annotation.Line:
            maxPoints = 2;
            type = "LineString";
            break;
        case Annotation.Icon:
            type = "Point";
            break;
        case Annotation.MultiPoint:
            geometryFunction = multiPointGeometryFunction;
            type = "LineString";
            finishCondition = never;
            minPoints = 1;
            break;
        case Annotation.Freehand:
        case Annotation.CompoundFreehand:
            // disable dragging when drawing freehand
            toggleDragPanInteraction(this.viewport.map, false);
            type = "LineString";
            freehandCondition = noModifierKeys;
            condition = singleClick;
            this.stopDrawingOnMouseUp = true;
            break;
        case Annotation.ClosedFreehand:
            // disable dragging when drawing freehand
            toggleDragPanInteraction(this.viewport.map, false);
            type = "Polygon";
            freehandCondition = noModifierKeys;
            condition = singleClick;
            this.stopDrawingOnMouseUp = true;
            break;
        case Annotation.Ellipse:
            geometryFunction = ellipseGeometryFunction.bind(options);
            maxPoints = 2;
            type = "LineString";
            break;
        case Annotation.Circle:
            if (options.size) {
                maxPoints = 1;
                type = "Point";
                styleGeometryFunction = styleCircleGeometryFunction.bind(options);
            }

            geometryFunction = circleGeometryFunction.bind(options);
            break;
    }

    var tmpStl = getAnnotationStyle.call(this, options.color, options.lineThickness, options.fillColor, options.iconRelativePath, null, styleGeometryFunction);

    this.draw = new olInteraction.Draw({
        source: this.viewport.annotationsLayer.getSource(),
        type: type,
        geometryFunction: geometryFunction,
        maxPoints: maxPoints,
        minPoints: minPoints,
        style: tmpStl,
        condition: condition,
        finishCondition: finishCondition,
        freehandCondition: freehandCondition
    });

    var updateExisting = false;
    if (options.feature) {
        updateExisting = true;
        this.draw.extend(options.feature);
        this.drawing = true;
        options.feature.drawingType = options.type;
        options.feature.color = options.feature.metaData.Color;
        options.feature.penSize = options.feature.metaData.LineThickness;
        options.feature.fillColor = options.feature.metaData.FillColor;

        showDrawingControls.call(this, true, options.type, options.feature);
        options.feature = null;
    }

    this.viewport.map.addInteraction(this.draw);

    if (options.enableSnapping === true) {
        this.snapInteraction = new olInteraction.Snap({
            source: this.viewport.annotationsLayer.getSource()
        });
        this.viewport.map.addInteraction(this.snapInteraction);
    }
    else if (this.snapInteraction) {
        this.viewport.map.removeInteraction(this.snapInteraction);
        this.snapInteraction = null;
    }

    if (this.viewport.getAnnotationLabelsVisible() === true) {
        var tooltip = createMeasureTooltip.call(this);
    }

    var self = this;
    var featureChangeKey = null; // used to unhook the event later

    this.draw.on('drawstart', function (evt) {
        // only called when a new annotation is created, not when extending an existing one
        self.drawing = true;
        self.shouldRejectDrawing = false;
        showDrawingControls.call(self, !shouldHideControls, options.type, evt.feature);

        evt.feature.setId((Math.random() * 10000) | 0);

        /** @type {ol.Coordinate|undefined} */
        var tooltipCoord = evt.coordinate;

        featureChangeKey = evt.feature.on("change", function () {
            var evtArgs = {
                temporary: evt.feature.temporary,
                feature: evt.feature
            };

            self.fireEvent(Events.AnnotationDrawing, evtArgs);
        });

        if (tooltip) {
            evt.feature.getGeometry().on('change', function (e) {
                var geom = e.target;
                var output;
                switch (options.type) {
                    case Annotation.Rectangle:
                    case Annotation.Ellipse:
                    case Annotation.Polygon:
                    case Annotation.ClosedFreehand:
                        output = self.viewport.formatArea(self.viewport.calculateArea(geom));
                        tooltipCoord = geom.getInteriorPoint().getCoordinates();
                        break;
                    case Annotation.Line:
                    case Annotation.LineString:
                    case Annotation.Freehand:
                    case Annotation.Circle:
                        output = self.viewport.formatLength(self.viewport.calculateLength(geom));
                        tooltipCoord = geom.getLastCoordinate();
                        break;
                    default:
                        break;
                }

                tooltip.element.innerHTML = output;
                tooltip.setPosition(tooltipCoord);
            });
        }

        evt.feature.notes = options.notes;
        evt.feature.drawingType = options.type;
        evt.feature.temporary = true;
        evt.feature.penSize = options.lineThickness;
        evt.feature.color = options.color;
        evt.feature.fillColor = options.fillColor;
        evt.feature.icon = options.iconRelativePath;
    });

    this.draw.on('drawend', function (evt) {
        self.drawing = false;
        showDrawingControls.call(self, evt.feature.drawingType === Annotation.CompoundFreehand, evt.feature.drawingType, evt.feature);

        if (evt.feature.drawingType === Annotation.CompoundFreehand) {
            if (self.drawingControlsContainer) {
                var c = evt.feature.getGeometry().getLastCoordinate();
                var pixelCoord = self.viewport.map.getPixelFromCoordinate(c);

                self.drawingControlsContainer.style.left = (pixelCoord[0] + 25) + "px";
                self.drawingControlsContainer.style.top = (pixelCoord[1] - 50) + "px";
            }
        }

        if (!self.getEnabled()) {
            evt.preventDefault();
            return false;
        }

        if (tooltip) {
            self.viewport.map.removeOverlay(tooltip);
            // tooltip = null;
        }

        if (self.shouldRejectDrawing) {
            removeInteraction.call(self);
        }
        else {
            // remove the interaction with a delay, otherwise the last double click
            // will result in a zoom in
            setTimeout(function () {
                removeInteraction.call(self);
            }, 100);
        }

        if (evt.feature.drawingType === Annotation.CompoundFreehand) {
            self.compoundFreehandList.push(evt.feature);
        }

        Observable.unByKey(featureChangeKey);

        addFeature.call(self, evt.feature, updateExisting, options.rejectSelfIntersecting);
        if (evt.feature.drawingType !== Annotation.CompoundFreehand) {
            self.fireEvent(Events.AnnotationDrawingFinished, { cancelled: false });
        }
    });

    this.draw.on('drawabort', function (evt) {
        self.drawing = false;
        showDrawingControls.call(self, false, evt.feature.drawingType);

        if (!self.getEnabled()) {
            evt.preventDefault();
            return false;
        }

        if (tooltip) {
            self.viewport.map.removeOverlay(tooltip);
            // tooltip = null;
        }

        if (self.shouldRejectDrawing) {
            removeInteraction.call(self);
        }
        else {
            // remove the interaction with a delay, otherwise the last double click
            // will result in a zoom in
            setTimeout(function () {
                removeInteraction.call(self);
            }, 100);
        }

        Observable.unByKey(featureChangeKey);
        self.fireEvent(Events.AnnotationDrawingFinished, { cancelled: true });
    });
}

function updateWktFromFeature(feature) {
    let self = this;
    var extent = self.viewport.map.getView().getProjection().getExtent();
    var extentObj = { extent: extent, flip: self.viewport.options.flip };

    var geomClone = feature.getGeometry().clone();
    geomClone.applyTransform(annotationTransform.bind(extentObj));

    var geometryWkt = "";

    if (feature.drawingType === "Freehand" || feature.drawingType === "ClosedFreehand") {
        geometryWkt = self.format.writeGeometry(geomClone.simplify(2));
    }
    else if (feature.drawingType === "Circle") {
        geometryWkt = self.format.writeGeometry(fromCircle(geomClone));
    }
    else {
        geometryWkt = self.format.writeGeometry(geomClone);
    }

    feature.metaData.Geometry = geometryWkt;
}

function addFeature(feature, updateExisting, rejectSelfIntersecting = false) {
    var self = this;
    var stl = getAnnotationStyle.call(self, feature.color, feature.penSize, feature.fillColor, feature.icon, feature);
    if (feature.getId() === undefined) {
        feature.setId((Math.random() * 10000) | 0);
    }

    stl.getText().setText(self.viewport.getAnnotationFormattedLabel(feature));
    feature.setStyle(stl);

    feature.originalStyle = stl;
    feature.temporary = false;

    // disable the toolbar once finished drawing
    if (self.element !== null) {
        $(self.element).find("li.draw a").removeClass("active");
        $(self.element).find(".color-picker").hide();
    }

    var evtArgs = {
        temporary: feature.temporary,
        feature: feature
    };

    var extent = self.viewport.map.getView().getProjection().getExtent();
    var extentObj = { extent: extent, flip: self.viewport.options.flip };

    var geomClone = feature.getGeometry().clone();
    geomClone.applyTransform(annotationTransform.bind(extentObj));

    var geometryWkt = "";

    if (feature.drawingType === "Freehand" || feature.drawingType === "ClosedFreehand") {
        geometryWkt = self.format.writeGeometry(geomClone.simplify(2));
    }
    else if (feature.drawingType === "Circle") {
        geometryWkt = self.format.writeGeometry(fromCircle(geomClone));
    }
    else {
        geometryWkt = self.format.writeGeometry(geomClone);
    }

    var dimensions = 2;
    switch (feature.drawingType) {
        case Annotation.Arrow:
        case Annotation.Icon:
        case Annotation.Point:
        case Annotation.MultiPoint:
            dimensions = 0;
            break;
        case Annotation.Line:
        case Annotation.Freehand:
        case Annotation.CompoundFreehand:
        case Annotation.LineString:
            dimensions = 1;
            break;
        case Annotation.Rectangle:
        case Annotation.Ellipse:
        case Annotation.Polygon:
        case Annotation.ClosedFreehand:
        case Annotation.Circle:
            dimensions = 2;
            break;
    }

    if (!updateExisting) {
        feature.metaData = {
            AnnotationID: null,
            Context: null,
            Classification: Resources.translate("Generic"),
            Color: feature.icon ? feature.icon : feature.originalStyle.getStroke().getColor(),
            Image: self.viewport.imageInfo.Filename,
            LayerID: 1,
            Notes: feature.notes ? feature.notes : "",
            UpdateInfo: "",
            State: AnnotationState.Added,
            FillColor: feature.originalStyle.getFill().getColor(),
            Dimensions: dimensions,
            LineThickness: feature.penSize,
            CreatedOn: new Date(),
            CreatedBy: null,
            UpdatedOn: null,
            UpdatedBy: null,
        };
    }
    else if (feature.metaData.State !== AnnotationState.Added) {
        feature.metaData.UpdatedOn = new Date();
        feature.metaData.UpdatedBy = null;
        feature.metaData.State = AnnotationState.Modified;
    }

    updateAreaLength.call(self, feature);

    feature.metaData.DrawingType = feature.drawingType;
    if (feature.metaData.DrawingType == Annotation.MultiPoint) {
        feature.metaData.PointCount = feature.getGeometry().getCoordinates ? feature.getGeometry().getCoordinates().length : 0;
    }

    feature.metaData.Geometry = geometryWkt;

    if (updateExisting && feature.metaData.State !== AnnotationState.Added) {
        feature.metaData.State = AnnotationState.Modified;
    }

    // fire with a time out to give the chance to OL to add the feature in the list
    setTimeout(function () {
        if (feature.metaData.Geometry.indexOf("MULTIPOINT") === -1 && feature.metaData.Length === 0 && (self.viewport.imageInfo.MicrometresPerPixelX > 0 && self.viewport.imageInfo.MicrometresPerPixelY > 0)) {
            self.deleteAnnotation(feature.getId());
            return;
        }
        if (geometryWkt.indexOf("MULTIPOINT EMPTY") !== -1) {
            self.deleteAnnotation(feature.getId());
        }
        else {
            stl.getText().setText(self.viewport.getAnnotationFormattedLabel(feature));
            feature.setStyle(stl);

            if (updateExisting) {
                if (self.viewport.getAnnotationLabelsVisible()) {
                    self.viewport.showAnnotationsLabels(self.viewport.getAnnotationLabelsVisible());
                }
                self.fireEvent(Events.AnnotationModified, evtArgs);
            }
            else {
                if (!self.shouldRejectDrawing && feature) {
                    let checkForSelfIntersection =
                        feature.drawingType === Annotation.Rectangle ||
                        feature.drawingType === Annotation.Freehand ||
                        feature.drawingType === Annotation.ClosedFreehand ||
                        (feature.drawingType === Annotation.CompoundFreehand && feature.getGeometry().getType() === "Polygon") ||
                        feature.drawingType === Annotation.Polygon ||
                        feature.drawingType === Annotation.LineString;

                    if (rejectSelfIntersecting && checkForSelfIntersection) {
                        const coords = getCoordinates(feature);
                        if (isSelfIntersecting(coords)) {
                            self.deleteAnnotation(feature.getId());
                            return;
                        }
                    }

                    self.fireEvent(Events.AnnotationAdded, evtArgs);
                }
            }
        }
    }, 10);
}

function getFormattedBrushTip() {
    var radius, area, isPixels = false, size = this.edit.getBrushSize(), type = this.edit.getBrushType();
    if (this.viewport.imageInfo.MicrometresPerPixelX) {
        radius = size * this.viewport.imageInfo.MicrometresPerPixelX;
    } else {
        isPixels = true;
        radius = size;
    }

    if (type === 'circle') {
        area = Math.PI * Math.pow(radius, 2);
    } else {
        area = Math.pow(radius * 2, 2);
    }

    return `&nbsp;Area:&nbsp;${formatArea(area, isPixels)}&nbsp;`;
}

function formatArea(val, isPixel) {
    if (isPixel) {
        return val + ' px<sup>2</sup>';
    }

    var output;
    if (val > 100000) {
        output = Math.round(val * 10 / 1000000) / 10 + ' mm<sup>2</sup>';
    }
    else {
        output = Math.round(val * 10) / 10 + ' μm<sup>2</sup>';
    }

    return output;
}

function updateAreaLength(feature) {
    var tmpGeom = feature.getGeometry();
    var dimensions = feature.metaData.Dimensions;
    if (dimensions > 0) {
        var length = this.viewport.calculateLength(tmpGeom);
        feature.metaData.Length = length;
        feature.metaData.FormattedLength = this.viewport.formatLength(length);

        if (dimensions > 1) {
            var area = this.viewport.calculateArea(tmpGeom);
            feature.metaData.Area = area;
            feature.metaData.FormattedArea = this.viewport.formatArea(area);
        }
    }
}

function removeInteraction() {
    if (this.draw) {
        toggleDragPanInteraction(this.viewport.map, true);
        this.viewport.map.removeInteraction(this.draw);
        this.draw = null;
    }

    if (this.edit) {
        toggleDragPanInteraction(this.viewport.map, true);
        this.viewport.map.removeInteraction(this.edit);
        this.edit = null;
    }

    if (this.selectionAdded !== true) {
        this.selectionAdded = true;
        this.selectInteraction.getFeatures().clear();
        this.viewport.map.addInteraction(this.selectInteraction);
        this.hoverInteraction.getFeatures().clear();
        this.viewport.map.addInteraction(this.hoverInteraction);
    }
}

function updateCurrentInteraction() {
    if (!this.draw) {
        return;
    }

    if (this.element !== null) {
        var el = $(this.element).find("li.draw a.active");
        if (el.length === 1) {
            el.removeClass("active");
            el.click();
        }
    }
}

function drawClick(element) {
    if (!this.getEnabled()) {
        return;
    }

    this.finishDrawing(true);

    if (element.hasClass("active")) {
        element.removeClass("active");
        return;
    }

    var tp = element.data("type");
    var notes = "";
    if (tp === "Text") {
        tp = "Point";
        notes = prompt(Resources.translate("Enter text"));
        if (!notes) {
            return;
        }
    }

    var selection = this.getSelection();
    this.startDrawing({
        type: tp,
        color: element.data("color"),
        lineThickness: element.data("size"),
        iconRelativePath: element.data("icon"),
        feature: (selection && selection.length > 0 ? selection[0] : null),
        fillColor: DefaultFillColor,
        notes: notes
    });

    element.addClass("active");

    if (this.jsColorPicker !== null) {
        this.jsColorPicker.fromString(element.data("color"));
    } else {
        $(this.element).find(".color-picker input[type='color']").val(element.data("color"));
    }

    $(this.element).find(".color-picker").show();
}

function createControls() {
    if (this.element === null) {
        return;
    }

    var html = "<ul class='pma-ui-annotations'>";
    if (supportsColorPicker()) {
        html += "<li class='option color-picker'><input type='color' value='#000000' /></li>";
    } else {
        html += "<li class='option color-picker'><input type='button' value=' ' /><input type='hidden' value='#000000' /></li>";
    }

    html += "<li class='option draw'><a data-type='Freehand' data-size='2' data-color='#008000' style='color: #008000' class='size-2' href='#' title='" + Resources.translate("Freehand size 1") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Freehand' data-size='4' data-color='#ff0000' style='color: #ff0000' class='size-4' href='#' title='" + Resources.translate("Freehand size 2") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Freehand' data-size='8' data-color='#0000ff' style='color: #0000ff' class='size-8' href='#' title='" + Resources.translate("Freehand size 3") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
    html += "<li class='option draw'><a data-type='ClosedFreehand' data-size='2' data-color='#008000' style='color: #008000' class='size-2' href='#' title='" + Resources.translate("Closed Freehand size 1") + "'><i class='fa fa-pencil-square' aria-hidden='true'></i></a></li>";
    html += "<li class='option draw'><a data-type='CompoundFreehand' data-size='2' data-color='#008080' style='color: #008080' class='size-2' href='#' title='" + Resources.translate("Compound Freehand size 2") + "'><i class='fa fa-chain' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Point' data-size='3' data-color='#f00ff0' style='color: #f00ff0' class='size-8' href='#' title='" + Resources.translate("Point size 3") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
    html += "<li class='option draw'><a data-type='Text' data-size='1' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Text") + "'><i class='fa fa-font' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Icon' data-size='1' data-color='#ffffff' data-icon='wim.jpg' style='background: url(\"http://www.smartcode.gr/annotations/wim.jpg\") center center no-repeat; background-size: contain' class='size-8' href='#' title='" + Resources.translate("Wim") + "'></a></li>";
    // html += "<li class='option draw'><a data-type='Icon' data-size='1' data-color='#ffffff' data-icon='yves.jpg' style='background: url(\"http://www.smartcode.gr/annotations/yves.jpg\") center center no-repeat; background-size: contain' class='size-8' href='#' title='" + Resources.translate("Yves") + "'></a></li>";
    // html += "<li class='option draw'><a data-type='Rectangle' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + Resources.translate("Rectangle") + "'><i class='fa fa-square-o' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Circle' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + Resources.translate("Circle") + "'><i class='fa fa-circle-o' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='Ellipse' data-size='2' data-color='#000000' style='color: #000000' class='size-8' href='#' title='" + Resources.translate("Ellipse") + "'><i class='fa fa-circle' aria-hidden='true'></i></a></li>";
    html += "<li class='option draw'><a data-type='Arrow' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Arrow") + "'><i class='fa fa-arrow-right' aria-hidden='true'></i></a></li>";
    html += "<li class='option draw'><a data-type='Line' data-size='1' data-color='#F00F00' style='color: #F00F00' class='size-2' href='#' title='" + Resources.translate("Measure") + "'><i class='fa fa-minus' aria-hidden='true'></i></a></li>";
    // html += "<li class='option draw'><a data-type='LineString' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Polyline") + "'>L</a></li>";
    // html += "<li class='option draw'><a data-type='Polygon' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Polygon") + "'>P</a></li>";
    // html += "<li class='option draw'><a data-type='MultiPoint' data-size='2' data-color='#000000' style='color: #000000' class='size-2' href='#' title='" + Resources.translate("MultiPoint") + "'>MP</a></li>";
    // html += "<li class='option delete'><a style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Delete") + "'><i class='fa fa-trash' aria-hidden='true'></i></a></li>";
    // html += "<li class='option save'><a style='color: #000000' class='size-2' href='#' title='" + Resources.translate("Save") + "'><i class='fa fa-floppy-o' aria-hidden='true'></i></a></li>";



    html += "</ul>";

    var self = this;
    var el = $(this.element);

    el.html(html);

    if (!supportsColorPicker()) {
        self.jsColorPicker = new jscolor(el.find("li.color-picker input")[0], { valueElement: el.find("li.color-picker input[type=hidden]")[0], hash: true, closable: true, closeText: Resources.translate("Close") });
    }

    el.find("li.color-picker input").change(function () {
        var newcol = $(this).val();

        el.find("li.draw a.active").data("color", newcol);
        el.find("li.draw a.active").css("color", newcol);
        updateCurrentInteraction.call(self);
    });

    el.find("li.option.delete a").click(function (evt) {
        evt.preventDefault();
        var d = self.getSelection();
        if (d && d.length > 0) {
            self.deleteAnnotation(d[0].getId());
        }
    });

    el.find("li.option.save a").click(function (evt) {
        evt.preventDefault();
        self.saveAnnotations();
    });

    el.find("li.draw a").click(function (ev) {
        ev.preventDefault();
        var e = $(this);
        drawClick.call(self, e);
    });
}

var selectImageCache = {};
var hoverImageCache = {};

function addBoundingBox(image, cache, color) {
    if (!cache[image.src]) {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');

        canvas.width = image.width;
        canvas.height = image.height;
        context.drawImage(image, 0, 0, image.width, image.height);

        context.strokeStyle = color;
        context.lineWidth = 10;
        context.strokeRect(0, 0, canvas.width, canvas.height);

        cache[image.src] = canvas;
    }

    return cache[image.src];
}

function createSelectInteraction() {
    var self = this;
    let sqrt2 = Math.sqrt(2);
    let radius = 5;

    const selectStyleFn = function (strokeColor, cache, bboxColor, f) {
        if (f.originalStyle) {
            let oStyle = f.originalStyle.clone();
            let oFill = oStyle.getFill();
            if (!oFill) {
                oFill = new Fill();
            }

            oFill.setColor('rgba(255, 255, 255, 0)');
            oStyle.getStroke().setColor(strokeColor);
            oStyle.getStroke().setWidth(oStyle.getStroke().getWidth() + 3);

            let originalImage = oStyle.getImage();
            if (originalImage instanceof ol.style.Icon) {
                const image = originalImage.getImage();
                if (!image) {
                    return;
                }

                const selectIconImage = addBoundingBox(image, cache, bboxColor);
                oStyle.setImage(new Icon({
                    img: selectIconImage,
                    imgSize: [selectIconImage.width, selectIconImage.height],
                    scale: originalImage.getScale(),
                }));
            }
            else {
                if (originalImage && originalImage.getSize) {
                    let sz = originalImage.getSize();
                    let side = sz[0] > sz[1] ? sz[0] : sz[1];
                    radius = ((sqrt2 * side) / 2).toFixed(0);
                }

                oStyle.setImage(new Circle({
                    fill: new Fill({ color: 'rgba(255, 255, 255, 0.7)' }),
                    stroke: new Stroke({ color: strokeColor, width: 4, }),
                    radius: radius
                }));
            }

            return oStyle;
        }

        return new Style({
            stroke: new Stroke({ color: strokeColor, width: 3, }),
            fill: new Fill({ color: 'rgba(255, 255, 255, 0.7)' }),
            image: new RegularShape({
                fill: new Fill({ color: 'rgba(255, 255, 255, 0.7)' }),
                stroke: new Stroke({ color: strokeColor, width: 4 }),
                points: 4,
                radius: 5,
                angle: Math.PI / 4
            })
        });
    };

    this.selectInteraction = new olInteraction.Select({
        condition: click,
        layers: [this.viewport.annotationsLayer],
        style: selectStyleFn.bind(this, 'rgba(0, 153, 255, 1)', selectImageCache, 'dodgerblue')
    });

    let selection = this.selectInteraction.getFeatures();

    this.hoverInteraction = new olInteraction.Select({
        condition: pointerMove,
        layers: [this.viewport.annotationsLayer],
        filter: function (f) {
            if (selection.getArray().indexOf(f) !== -1) {
                return false;
            }

            return true;
        },
        style: selectStyleFn.bind(this, "#ffff00", hoverImageCache, 'yellow')
    });

    this.pointerInteraction = new olInteraction.Pointer({
        handleMoveEvent: function (evt) {
            if (self.editing || self.drawing || self.draw || self.edit) {
                return;
            }
            var hit = self.viewport.map.forEachFeatureAtPixel(evt.pixel, function () {
                return true;
            });
            if (hit) {
                self.viewport.map.getTargetElement().style.cursor = 'pointer';
            } else {
                self.viewport.map.getTargetElement().style.cursor = '';
            }
        }
    });

    this.selectInteraction.on('select', function () {
        self.fireEvent(Events.AnnotationsSelectionChanged, selection.getArray());
    });

    // add the interactions initially
    this.selectionAdded = true;
    this.selectInteraction.getFeatures().clear();
    this.viewport.map.addInteraction(this.selectInteraction);
    this.hoverInteraction.getFeatures().clear();
    this.viewport.map.addInteraction(this.hoverInteraction);
    this.viewport.map.addInteraction(this.pointerInteraction);
}

function saveAnnotationsNew(added, edited, layerId) {
    var i, a;
    var toAdd = [];
    //this.stopTool();

    if (added && added.length) {
        for (i = 0; i < added.length; i++) {
            a = added[i];
            if (!a.metaData.Notes) {
                a.metaData.Notes = Resources.translate(' ');
            }

            if (!a.metaData.Classification) {
                a.metaData.Classification = Resources.translate('no classification');
            }

            if (a.metaData.AnnotationID === null) {
                a.metaData.AnnotationID = 0;
            }

            toAdd.push(a.metaData);
        }
    }

    var toEdit = edited.map(function (x) { return x.metaData; });

    var toDelete = [];
    if (this.deletedAnnotations && this.deletedAnnotations.length > 0) {
        for (i = 0; i < this.deletedAnnotations.length; i++) {
            if (layerId !== -1 && this.deletedAnnotations[i].metaData.LayerID !== layerId) {
                continue;
            }

            // if the annotation ID is null, it means that the annotation was never saved in the first place
            if (this.deletedAnnotations[i].metaData.AnnotationID !== null) {
                toDelete.push(this.deletedAnnotations[i].metaData);
            }
        }
    }

    var self = this;
    this.context.saveAnnotations(
        this.serverUrl,
        this.path,
        toAdd,
        toEdit,
        toDelete,
        function (sessionId, annotationIds) {
            if (added && added.length) {
                if (annotationIds == null || annotationIds.length != added.length) {
                    console.error("Annotations saved but cannot update annotation ids");
                    return;
                }

                for (i = 0; i < annotationIds.length; i++) {
                    added[i].metaData.AnnotationID = annotationIds[i];
                    added[i].setId(annotationIds[i]);
                    added[i].metaData.State = AnnotationState.Pristine;
                }
            }

            if (edited && edited.length) {
                for (i = 0; i < edited.length; i++) {
                    edited[i].metaData.State = AnnotationState.Pristine;
                }
            }

            self.deletedAnnotations = [];

            self.fireEvent(Events.AnnotationsSaved, { success: true });
        },
        function () {
            self.fireEvent(Events.AnnotationsSaved, { success: false });
            console.error("Saving annotations failed");
            console.log(arguments);
        });
}

// saves all newly added annotations and then calls saveEditedAnnotations 
function saveAddedAnnotations(added, edited) {
    if (!added || added.length === 0) {
        saveEditedAnnotations.call(this, edited);
        return;
    }

    var a = added.pop();
    var self = this;

    if (!a.metaData.Notes) {
        a.metaData.Notes = Resources.translate(' ');
    }

    if (!a.metaData.Classification) {
        a.metaData.Classification = Resources.translate('no classification');
    }

    self.context.addAnnotation(
        self.serverUrl,
        self.path,
        a.metaData.Classification,
        a.metaData.LayerID,
        a.metaData.Notes,
        a.metaData.Geometry,
        a.metaData.Color,
        function (sessionId, annotationId) {
            a.metaData.AnnotationID = annotationId;
            a.setId(annotationId);
            a.metaData.State = AnnotationState.Pristine;

            saveAddedAnnotations.call(self, added, edited);
        },
        function () {
            self.fireEvent(Events.AnnotationsSaved, { success: false });
            console.error("Saving annotation (add) failed");
            console.log(arguments);
        });
}

// saves all modified annotations and then calls saveDeletedAnnotations 
function saveEditedAnnotations(edited) {
    if (!edited || edited.length === 0) {
        saveDeletedAnnotations.call(this);
        return;
    }

    var a = edited.pop();
    var self = this;

    self.context.updateAnnotation(
        self.serverUrl,
        self.path,
        a.metaData.LayerID,
        a.metaData.AnnotationID,
        a.metaData.Notes,
        a.metaData.Geometry,
        a.metaData.Color,
        function () {
            a.metaData.State = AnnotationState.Pristine;
            saveEditedAnnotations.call(self, edited);
        },
        function () {
            self.fireEvent(Events.AnnotationsSaved, { success: false });
            console.error("Saving annotation (edit) failed");
            console.log(arguments);
        });
}

// saves all delete annotations and then AnnotationsSaved event
function saveDeletedAnnotations() {
    if (!this.deletedAnnotations || this.deletedAnnotations.length === 0) {
        this.fireEvent(Events.AnnotationsSaved, { success: true });
        return;
    }

    var entity = this.deletedAnnotations.pop();
    var self = this;

    if (entity.metaData.AnnotationID === null) {
        // if the annotation ID is null, it means that the annotation was never saved in the first place
        saveDeletedAnnotations.call(self);
        return;
    }

    self.context.deleteAnnotation(
        self.serverUrl,
        self.path,
        entity.metaData.LayerID,
        entity.metaData.AnnotationID,
        function () {
            saveDeletedAnnotations.call(self);
        },
        function () {
            self.fireEvent(Events.AnnotationsSaved, { success: false });
            console.error("Saving annotation (delete) failed");
            console.log(arguments);
        });
}

function checkPMACore2(cb) {
    var self = this;
    if (self.isPMACore2 !== null) {
        if (typeof cb === "function") {
            cb.call(self, self.isPMACore2);
        }

        return;
    }

    this.context.getVersionInfo(self.serverUrl, function (version) {
        if (version && version.substring(0, "1.".length) === "1.") {
            self.isPMACore2 = false;
        } else {
            self.isPMACore2 = true;
        }

        if (typeof cb === "function") {
            cb.call(self, self.isPMACore2);
        }
    }, function () {
        console.error("Cannot reach server");
    });
}

export
    /**
     * Provides programmatic interaction with a {@link Viewport|PMA.UI.View.Viewport} instance to manipulate annotations
     * @class
     * @memberof PMA.UI.Components
     * @alias Annotations
     * @param  {Object} options
     * @param  {Context} options.context
     * @param  {Viewport} options.viewport
     * @param  {string} options.serverUrl - PMA.core server URL
     * @param  {string} options.path - Path of an slide to save annotations for
     * @param  {boolean} options.enabled
     * @tutorial 05-annotations
     * @fires PMA.UI.Components.Events.AnnotationAdded
     * @fires PMA.UI.Components.Events.AnnotationDrawing
     * @fires PMA.UI.Components.Events.AnnotationDrawingFinished
     * @fires PMA.UI.Components.Events.AnnotationDeleted
     * @fires PMA.UI.Components.Events.AnnotationModified
     * @fires PMA.UI.Components.Events.AnnotationsSaved
     * @fires PMA.UI.Components.Events.AnnotationsSelectionChanged
     */
    class Annotations {
    constructor(options) {
        // options: context, element, viewport, serverUrl, path, enabled
        if (!checkBrowserCompatibility()) {
            return;
        }

        // if the element is null, no controls are created
        if (options.element instanceof HTMLElement) {
            this.element = options.element;
        } else if (typeof options.element == "string") {
            var el = document.querySelector(options.element);
            if (!el) {
                console.error("Invalid selector for element");
            } else {
                this.element = el;
            }
        } else {
            this.element = null;
        }

        if (this.element) {
            this.element.style.pointerEvents = "auto";
        }

        if (!options.viewport.map) {
            console.error("Invalid viewport instance");
            return;
        }

        if (!options.viewport.annotationsLayer) {
            console.error("Annotations must be enabled in the viewport for this to work");
            return;
        }

        this.serverUrl = options.serverUrl;
        this.path = options.path;
        this.context = options.context;
        this.viewport = options.viewport;
        this.drawing = false;
        this.selectionAdded = false;
        this.stopDrawingOnMouseUp = false;
        this.format = new olFormat.WKT();
        this.jsColorPicker = null;
        this.isPMACore2 = null;

        checkPMACore2.call(this);

        this.listeners = {};
        this.listeners[Events.AnnotationAdded] = [];
        this.listeners[Events.AnnotationDrawing] = [];
        this.listeners[Events.AnnotationDrawingFinished] = [];
        this.listeners[Events.AnnotationDeleted] = [];
        this.listeners[Events.AnnotationModified] = [];
        this.listeners[Events.AnnotationsSaved] = [];
        this.listeners[Events.AnnotationsSelectionChanged] = [];
        this.listeners[Events.AnnotationEditingStarted] = [];
        this.listeners[Events.AnnotationEditingEnded] = [];

        createSelectInteraction.call(this);

        createControls.call(this);

        var self = this;
        this.viewport.map.on('pointerup', function () {
            if (self.drawing && self.stopDrawingOnMouseUp === true) {
                self.finishDrawing(true);
            }
        });

        this.setEnabled(options.enabled === true);
        this.compoundFreehandList = [];
        this.lastAnnotationStyle = null;
        this.lastDrawingOptions = null;

        // used to control whether or not an annotation should actually be added after drawing is finished
        // e.g. if finishDrawing(false) is invoked, the annotation should not be added and no events should fire
        this.shouldRejectDrawing = false;
    }

    /**
     * Annotation entity
     * @typedef {Object} Annotations~annotationEntity
     * @property {Number} LayerID - The layer id
     * @property {string} Geometry - The annotation geometry in wkt format
     * @property {string} [Context] - Optional context for the annotation
     * @property {string} [Notes] - Optional notes for the annotation
     * @property {string} [Classification] - Optional classification string (Necrosis, tumor etc)
     * @property {string} [Color] - Optional stroke color (e.g. #ff0000)
     * @property {string} [UpdateInfo] - Optional update info
     * @property {string} [FillColor] - Optional fill color (e.g. #ff0000)
     * @property {Number} [Dimensions] - Optional dimensionality of the annotation
     * @property {Number} [LineThickness=1] - Optional stroke line thickness
     */
    /**
     * Replaces the currently loaded annotations with the provided ones (without saving them to the server)
     * @param  {Annotations~annotationEntity[]} annotations - Array of annotation objects
     */
    replaceAnnotations(annotations) {
        if (!annotations || !(annotations instanceof Array)) {
            annotations = [];
        }

        for (let i = 0; i < annotations.length; i++) {
            annotations[i].AnnotationID = null;
            annotations[i].Image = this.viewport.image;
        }

        this.finishDrawing(true);

        // Clear all existing annotations and mark them as deleted
        var current = this.viewport.getAnnotations();
        this.deletedAnnotations = this.deletedAnnotations || [];
        for (let i = 0; i < current.length; i++) {
            switch (current[i].metaData.State) {
                case AnnotationState.Pristine:
                case AnnotationState.Modified:
                    this.deletedAnnotations.push(current[i]);
                    break;
            }
        }

        // load the provided annotations
        var annotationsSource = this.viewport.annotationsLayer.getSource();
        annotationsSource.clear();

        var features = this.viewport.initializeFeatures(annotations, this.viewport.mainLayer.getSource().getProjection());
        for (let i = 0; i < features.length; i++) {
            features[i].metaData.State = AnnotationState.Added;
        }

        annotationsSource.addFeatures(features);

        this.viewport.redraw();
    }

    /**
     * Adds an annotation to the current ones (without saving them to the server)
     * @param  {Annotations~annotationEntity} annotation - An annotation object
     * @param  {PMA.UI.Types.AnnotationState} [annotationState] - Optional annotation state. Default: Added.
     */
    addAnnotation(annotation, annotationState) {
        if (!annotationState) {
            annotationState = AnnotationState.Added;
        }

        annotation.AnnotationID = null;
        annotation.Image = this.viewport.image;

        this.finishDrawing(true);

        // load the provided annotations
        var annotationsSource = this.viewport.annotationsLayer.getSource();

        var features = this.viewport.initializeFeatures([annotation], this.viewport.mainLayer.getSource().getProjection());
        for (let i = 0; i < features.length; i++) {
            features[i].metaData.State = annotationState;
        }

        annotationsSource.addFeatures(features);

        var evtArgs = {
            temporary: false,
            feature: features && features.length ? features[0] : null
        };

        this.fireEvent(Events.AnnotationAdded, evtArgs);
        return evtArgs.feature;
    }

    getActive() {
        if (this.element === null) {
            return false;
        }

        return $(this.element).find("li.draw a.active").length > 0;
    }

    /**
     * Gets the state of the annotation component
     * @returns {boolean}
     */
    getEnabled() {
        return this.enabled;
    }

    /**
     * Enables or disables annotation drawing
     * @param  {boolean} enabled
     */
    setEnabled(enabled) {
        this.enabled = enabled === true;

        this.selectInteraction.setActive(this.enabled);
        this.hoverInteraction.setActive(this.enabled);
        this.pointerInteraction.setActive(this.enabled);
        if (!this.getEnabled()) {
            this.finishDrawing(false);

            if (this.element !== null) {
                $(this.element).find("li.draw a").removeClass("active");
                $(this.element).find("li.draw a").addClass("disabled");
                $(this.element).find(".color-picker").hide();
            }
        }
        else {
            if (this.element !== null) {
                $(this.element).find("li.draw a").removeClass("disabled");
            }
        }
    }

    /**
     * Options parameter required to start drawing
     * @typedef {Object} Annotations~startDrawingOptions
     * @property {PMA.UI.Types.Annotation} type - The type of the annotation to start drawing
     * @property {string} color - Annotation outline color as HTML hex string
     * @property {string} fillColor - Annotation fill color as HTML hex string
     * @property {Number} lineThickness - The line thickness
     * @property {string} notes - Text to add to the annotation
     * @property {string} [iconRelativePath] - Relative path to an image that will be used when drawing a point. The base URL is defined by the imageBaseUrl property of the {@link Viewport~annotationOptions|annotations} initialization option supplied in the {@link Viewport|PMA.UI.View.Viewport} constructor
     * @property {ol.Feature} [feature] - An existing {@link http://openlayers.org/en/master/apidoc/ol.Feature.html | feature} to edit. If this argument has a value, the viewport goes into edit mode, instead of drawing a new annotation
     * @property {Number[]} [size] - An optional array of [width, height] in microns. Applies only to circles and rectangles. When drawing a circle only width is taken into account and it's the diameter of the circle.
     * @property {boolean} [enableSnapping=false] - Enable snapping.
     * @property {boolean} [rejectSelfIntersecting=false] - Whether or not to reject self intersecting annotations.
     */
    /**
     * Instructs the viewport to enter annotation drawing mode
     * @param {Annotations~startDrawingOptions} options - Options to start drawing
     */
    startDrawing(options) {
        if (typeof options === "string") {
            // function is called with old arguments without options object
            // to keep backwards compatibility wrap the arguments to the new options object
            options = {
                type: arguments[0],
                color: arguments[1],
                lineThickness: arguments[2],
                iconRelativePath: arguments[3],
                feature: arguments[4],
                fillColor: DefaultFillColor,
                notes: "" // notes was missing in the old parameter list
            };
        }

        // in case a property name 'penWidth' is given to startDrawing, 
        // it will be assigned to 'lineThickness' to keep backwards compatibility
        if (Object.prototype.hasOwnProperty.call(options, 'penWidth')) {
            options.lineThickness = options.penWidth;
            delete options.penWidth;
        }

        if (!this.getEnabled()) {
            return;
        }

        if (this.drawing) {
            console.error("Drawing already in progress. Finish drawing before starting a new one.");
            return;
        }

        if (this.editing) {
            console.error("Editing already in progress. Finish editing before starting drawing.");
            return;
        }

        if (!options.fillColor) {
            options.fillColor = DefaultFillColor;
        }

        if (options.size) {
            if (!options.size.length || options.size.length != 2) {
                options.size = undefined;
            }
            else if (options.size[0] <= 0 || options.size[1] <= 0) {
                options.size = undefined;
            }
            else if (!this.viewport.imageInfo || !this.viewport.imageInfo.MicrometresPerPixelX || !this.viewport.imageInfo.MicrometresPerPixelY) {
                options.size = undefined;
            }
            else {
                options.size = [
                    options.size[0] / this.viewport.imageInfo.MicrometresPerPixelX,
                    options.size[1] / this.viewport.imageInfo.MicrometresPerPixelY
                ];
            }
        }

        options.rejectSelfIntersecting = !!options.rejectSelfIntersecting;

        this.finishDrawing(false);
        addInteraction.call(this, options);
    }
    /**
     * Exits drawing mode
     * @param  {boolean} accept - True to accept the annotation that was currently being drawn
     * @param {PMA.UI.Types.Annotation} annotationType - The annotation type that was drawing
     */
    finishDrawing(accept, annotationType) {
        this.shouldRejectDrawing = !accept;
        var mustRemove = accept === false;

        if (this.drawing) {
            if (this.draw != null) {
                if (mustRemove) {
                    this.draw.abortDrawing();
                } else {
                    this.draw.finishDrawing();
                }
            }
        } else {
            mustRemove = false;
        }

        removeInteraction.call(this);

        if (this.element !== null) {
            $(this.element).find(".color-picker").hide();
            $(this.element).find("li.draw a").removeClass("active");
        }

        if (annotationType == Annotation.CompoundFreehand) {
            showDrawingControls.call(this, false, Annotation.CompoundFreehand);
            var source = this.viewport.annotationsLayer.getSource();
            if (this.compoundFreehandList.length > 0) {
                while (this.compoundFreehandList.length > 0) {
                    var ft = this.compoundFreehandList.pop();
                    source.removeFeature(ft);
                }
            }

            this.fireEvent(Events.AnnotationDrawingFinished, { cancelled: accept === false });
        }
    }
    /**
     * Key returned by {@link listen}
     * @typedef {Object} EventKey
     * @property {PMA.UI.Components.Events} type - The name of the event to listen to
     * @property {function} listener - The function to call when the event occurs
     */
    /**
     * Attaches an event listener
     * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
     * @param {function} callback - The function to call when the event occurs
     * @returns {EventKey}
     */
    listen(eventName, callback) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " is not a valid event");
            return;
        }

        this.listeners[eventName].push(callback);
        return { type: eventName, listener: callback };
    }
    /**
     * Dettaches an event listener
     * @param {EventKey} key - The key returned by {@link listen}
     */
    unlistenByKey(key) {
        if (!key || !key.type || !key.listener) {
            console.error("No valid key provided");
            return;
        }

        if (!Object.prototype.hasOwnProperty.call(this.listeners, key.type)) {
            console.error(key.type + " is not a valid event");
            return;
        }

        const index = this.listeners[key.type].indexOf(key.listener);
        if (index !== -1) {
            this.listeners[key.type].splice(index, 1);
        }
    }
    // fires an event
    fireEvent(eventName, eventArgs) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            this.listeners[eventName][i].call(this, eventArgs);
        }
    }
    /**
     * Get the currently selected annotations
     * @return {Array.<{metaData: { PointCount: Number } }>} Array of PMA.core annotation instances
     */
    getSelection() {
        return this.selectInteraction.getFeatures().getArray();
    }
    /**
     * Saves all the annotations to PMA.core
     * @param {number} [layerId=undefined] - If supplied, then only the contents of the particular layer are saved.
     */
    saveAnnotations(layerId) {
        if (isNaN(parseInt(layerId))) {
            layerId = -1;
        }

        layerId = Math.max(layerId, -1);

        var current = this.viewport.getAnnotations();

        var added = [];
        var updated = [];

        for (var i = 0; i < current.length; i++) {
            if (!current[i].metaData || !current[i].metaData.State) {
                continue;
            }

            if (layerId > -1 && current[i].metaData.LayerID !== layerId) {
                continue;
            }

            switch (current[i].metaData.State) {
                case AnnotationState.Added:
                    updateWktFromFeature.call(this, current[i]);
                    added.push(current[i]);
                    break;
                case AnnotationState.Pristine:
                    break;
                case AnnotationState.Modified:
                    updateWktFromFeature.call(this, current[i]);
                    // if (current[i].metaData.AnnotationID) {
                    updated.push(current[i]);
                    // } else {
                    //     current[i].metaData.State = AnnotationState.Added;
                    //     added.push(current[i]);
                    // }
                    break;
            }
        }

        checkPMACore2.call(this, function () {
            if (this.isPMACore2) {
                saveAnnotationsNew.call(this, added, updated, layerId);
            } else {
                saveAddedAnnotations.call(this, added, updated);
            }
        });
    }
    /**
     *  Returns whether any annotation has unsaved changes
     * @returns {bool}
     */
    hasChanges() {
        if (this.deletedAnnotations && this.deletedAnnotations.length > 0) {
            return true;
        }

        var current = this.viewport.getAnnotations();

        for (var i = 0; i < current.length; i++) {
            switch (current[i].metaData.State) {
                case AnnotationState.Added:
                    return true;
                case AnnotationState.Pristine:
                    break;
                case AnnotationState.Modified:
                    return true;
            }
        }

        return false;
    }

    /**
     * Deletes an annotation
     * @param  {number} id - The id of the annotation to delete
     * @fires PMA.UI.Components.Events.AnnotationDeleted
     */
    deleteAnnotation(id) {
        if (id === null || isNaN(id)) {
            return;
        }

        var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
        if (f) {
            this.clearHighlight();
            this.clearSelection();
            this.viewport.annotationsLayer.getSource().removeFeature(f);

            if (f.metaData.State === AnnotationState.NotTracked) {
                return;
            }

            f.metaData.State = AnnotationState.Deleted;

            if (!this.deletedAnnotations) {
                this.deletedAnnotations = [];
            }

            this.deletedAnnotations.push(f);
            this.fireEvent(Events.AnnotationDeleted, { annotationId: id, feature: f });
        }
    }
    /**
     * Renders the requested annotation using the highlight style
     * @param  {Number} id - The id of the annotation to render
     */
    highlightAnnotation(id) {
        if (id === null || isNaN(id)) {
            return;
        }

        var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
        if (f) {
            var feats = this.hoverInteraction.getFeatures();
            feats.clear();
            feats.push(f);
        }
    }
    /**
     * Clears all highlighted annotations
     */
    clearHighlight() {
        this.hoverInteraction.getFeatures().clear();
    }
    /**
     * Adds the requested annotation in the selection list
     * @param  {Number} id - The id of the annotation to select
     */
    selectAnnotation(id) {
        if (id === null || isNaN(id)) {
            return;
        }

        var f = this.viewport.annotationsLayer.getSource().getFeatureById(id);
        if (f) {
            var feats = this.selectInteraction.getFeatures();
            feats.clear();
            feats.push(f);
        }
    }
    /**
     * Clears all selected annotations
     */
    clearSelection() {
        this.selectInteraction.getFeatures().clear();
    }
    /**
     * Merges the selected annotations into one geometry
     * @param  {Array.<ol.Feature>} [selection=null] - An array of annotations to merge. If this parameter is not supplied, the currently selected annotations are used.
     */
    mergeSelection(selection) {
        if (!selection) {
            selection = this.getSelection();
        }

        if (!selection || selection.length < 1) {
            return;
        }

        var tmpSelection = [];
        for (var i = 0; i < selection.length; i++) {
            var g = selection[i].getGeometry();
            if (g.getFirstCoordinate) {
                tmpSelection.push({
                    id: selection[i].getId(),
                    geometry: g,
                    first: g.getFirstCoordinate(),
                    last: g.getLastCoordinate(),
                    coordinates: g.getCoordinates(),
                    sqDistanceFirst: 0,
                    sqDistanceLast: 0,
                    sqDistanceFirstFirst: 0,
                    sqDistanceLastFirst: 0
                });
            }
        }

        for (i = 0; i < tmpSelection.length; i++) {
            this.deleteAnnotation(tmpSelection[i].id);
        }

        var coordinates = tmpSelection[0].coordinates;
        var first = coordinates[0];
        var last = coordinates[coordinates.length - 1];

        tmpSelection.splice(0, 1);

        while (tmpSelection.length > 0) {
            var minDistance = -1;
            var minIndex = -1;
            for (i = 0; i < tmpSelection.length; i++) {
                tmpSelection[i].sqDistanceFirst = Math.pow(last[0] - tmpSelection[i].first[0], 2) + Math.pow(last[1] - tmpSelection[i].first[1], 2);
                tmpSelection[i].sqDistanceLast = Math.pow(last[0] - tmpSelection[i].last[0], 2) + Math.pow(last[1] - tmpSelection[i].last[1], 2);

                tmpSelection[i].sqDistanceFirstFirst = Math.pow(first[0] - tmpSelection[i].first[0], 2) + Math.pow(first[1] - tmpSelection[i].first[1], 2);
                tmpSelection[i].sqDistanceLastFirst = Math.pow(first[0] - tmpSelection[i].last[0], 2) + Math.pow(first[1] - tmpSelection[i].last[1], 2);

                if (minDistance == -1 || tmpSelection[i].sqDistanceFirst < minDistance) {
                    minDistance = tmpSelection[i].sqDistanceFirst;
                    minIndex = i;
                }

                if (minDistance == -1 || tmpSelection[i].sqDistanceLast < minDistance) {
                    minDistance = tmpSelection[i].sqDistanceLast;
                    minIndex = i;
                }
            }

            var minItem = tmpSelection.splice(minIndex, 1)[0];

            var reversed = minItem.sqDistanceLast < minItem.sqDistanceFirst;
            for (var c = 0; c < minItem.coordinates.length; c++) {
                if (!reversed) {
                    coordinates.push(minItem.coordinates[c]);
                } else {
                    coordinates.push(minItem.coordinates[minItem.coordinates.length - 1 - c]);
                }
            }

            first = coordinates[0];
            last = coordinates[coordinates.length - 1];
        }

        coordinates.push(coordinates[0]);

        var resultGeometry = new Polygon([coordinates]);

        var feature = new Feature({
            geometry: resultGeometry,
        });

        feature.color = this.lastAnnotationStyle.color;
        feature.fillColor = this.lastAnnotationStyle.fillColor;
        feature.penSize = this.lastAnnotationStyle.penSize;
        feature.icon = this.lastAnnotationStyle.iconPath;

        var rejectSelfIntersecting = false;
        if (this.lastDrawingOptions) {
            feature.drawingType = this.lastDrawingOptions.drawingType;
            if (this.lastDrawingOptions.rejectSelfIntersecting === true) {
                rejectSelfIntersecting = true;
            }
        }

        addFeature.call(this, feature, false, rejectSelfIntersecting);

        this.viewport.annotationsLayer.getSource().addFeatures([feature]);

        this.clearSelection();
    }

    /**
     * Takes two or more annotations and adds a combined annotation. Original annotations will be deleted. Style is based on first annotation in array.
     * @param  {Array.<ol.Feature>} annotations - An array of annotations to combine with at least two elements.
     */
    booleanUnion(annotations) {
        if (!annotations) {
            console.error("No annotations provided");
            return;
        }

        if (annotations.length < 2) {
            console.error("This operation requires at least 2 annotations");
            return;
        }

        var tmpSelection = [];
        var featuresArray = [];

        var format = new olFormat.GeoJSON();
        var formatOptions = {
            featureProjection: this.viewport.map.getView().getProjection(),
        };

        var metaNotes = "";
        annotations.forEach(element => {
            metaNotes += element.metaData.Notes ? element.metaData.Notes + " " : "";
        });

        var metaColor = annotations[0].metaData.Color ? annotations[0].metaData.Color : annotations[0].color;
        var metaFillColor = annotations[0].metaData.FillColor ? annotations[0].metaData.FillColor : annotations[0].fillColor;
        var metaPenSize = annotations[0].metaData.LineThickness ? annotations[0].metaData.LineThickness : annotations[0].penSize;
        var metaIcon = annotations[0].icon;

        for (var i = 0; i < annotations.length; i++) {
            var g = annotations[i];
            if (g.getGeometry().getType() !== "Polygon" && g.getGeometry().getType() !== "MultiPolygon") {
                console.error("Selection contains non-polygon geometry");
                return;
            }
            var gJson = format.writeFeatureObject(g, formatOptions);
            tmpSelection.push(gJson);
            /*
            try {
                gJson = unkinkPolygon(cleanCoords(gJson));
            } catch {
                console.error("Selection contains non-polygon geometry");
                return;
            }
            gJson.features.forEach(feat => {
                featuresArray.push(feat);
            });
            */
            featuresArray.push(gJson);
        }

        var unifiedFeatureJson = union(...featuresArray);
        if (unifiedFeatureJson.type === "FeatureCollection") {
            throw "Feature collection detected";
        }

        if (unifiedFeatureJson.geometry.type === "MultiPolygon") {
            unifiedFeatureJson.geometry.coordinates.forEach((coords) => {
                var feat = { 'type': 'Polygon', 'coordinates': coords };

                var feature = format.readFeature(feat, formatOptions);

                feature.color = metaColor;
                feature.fillColor = metaFillColor;
                feature.penSize = metaPenSize;
                feature.icon = metaIcon;
                feature.notes = metaNotes ? metaNotes : "";

                addFeature.call(this, feature, false);

                this.viewport.annotationsLayer.getSource().addFeatures([feature]);
            });
        } else {
            var feature = format.readFeature(unifiedFeatureJson, formatOptions);

            feature.color = metaColor;
            feature.fillColor = metaFillColor;
            feature.penSize = metaPenSize;
            feature.icon = metaIcon;
            feature.notes = metaNotes ? metaNotes : "";

            addFeature.call(this, feature, false);

            this.viewport.annotationsLayer.getSource().addFeatures([feature]);
        }

        tmpSelection.forEach(geom => {
            this.deleteAnnotation(geom.id);
        });
    }

    /**
     * Finds the difference between two annotations by clipping the second annotation from the first. Original annotations will be deleted. Style is based on inputPolygonFeature.
     * @param  {ol.Feature} inputPolygonFeature - Base annotation feature to perform boolean difference.
     * @param  {ol.Feature} polygonFeatureToDifferentiate - Annotation feature to difference from inputPolygonFeature
     */
    booleanDifference(inputPolygonFeature, polygonFeatureToDifferentiate) {
        if (!inputPolygonFeature || !polygonFeatureToDifferentiate) {
            console.error("No annotations provided");
            throw "No annotations provided";
        }

        var format = new olFormat.GeoJSON();
        var formatOptions = {
            featureProjection: this.viewport.map.getView().getProjection(),
        };

        var metaNotes = "";
        metaNotes += inputPolygonFeature.metaData.Notes ? inputPolygonFeature.metaData.Notes + " " : "";
        metaNotes += polygonFeatureToDifferentiate.metaData.Notes ? polygonFeatureToDifferentiate.metaData.Notes + " " : "";

        var metaColor = inputPolygonFeature.metaData.Color ? inputPolygonFeature.metaData.Color : inputPolygonFeature.color;
        var metaFillColor = inputPolygonFeature.metaData.FillColor ? inputPolygonFeature.metaData.FillColor : inputPolygonFeature.fillColor;
        var metaPenSize = inputPolygonFeature.metaData.LineThickness ? inputPolygonFeature.metaData.LineThickness : inputPolygonFeature.penSize;
        var metaIcon = inputPolygonFeature.icon;

        var annotationId1;
        var annotationId2;

        var inputFeaturesArray = [];
        var featuresArrayToDifferentiate = [];
        var outputFeaturesArray = [];

        if (inputPolygonFeature.getGeometry().getType() !== "Polygon") {
            console.error("Annotation contains non-polygon geometry");
            throw "Annotation contains non-polygon geometry";
        }
        var inputPolygonFeatureJson = format.writeFeatureObject(inputPolygonFeature, formatOptions);
        annotationId1 = inputPolygonFeatureJson.id;

        /*
        try {
            inputPolygonFeatureJson = unkinkPolygon(cleanCoords(inputPolygonFeatureJson));
        } catch {
            console.error("Annotation contains non-polygon geometry");
            return;
        }
        inputPolygonFeatureJson.features.forEach(feat => {
            inputFeaturesArray.push(feat);
        });
        */

        inputFeaturesArray.push(inputPolygonFeatureJson);

        if (polygonFeatureToDifferentiate.getGeometry().getType() !== "Polygon") {
            console.error("Annotation contains non-polygon geometry");
            throw "Annotation contains non-polygon geometry";
        }
        var polygonFeatureToDifferentiateJson = format.writeFeatureObject(polygonFeatureToDifferentiate, formatOptions);
        annotationId2 = polygonFeatureToDifferentiateJson.id;

        /*
        try {
            polygonFeatureToDifferentiateJson = unkinkPolygon(cleanCoords(polygonFeatureToDifferentiateJson));
        } catch {
            console.error("Annotation contains non-polygon geometry");
            return;
        }
        polygonFeatureToDifferentiateJson.features.forEach(feat => {
            featuresArrayToDifferentiate.push(feat);
        });
        */

        featuresArrayToDifferentiate.push(polygonFeatureToDifferentiateJson);

        inputFeaturesArray.forEach(input => {
            featuresArrayToDifferentiate.forEach(diff => {
                var intersection = booleanIntersects(input, diff);
                if (!intersection) {
                    console.error("Non-intersecting geometry");
                    throw "Non-intersecting geometry";
                }
                var differentiatedFeatureJson = difference(input, diff);
                if (!differentiatedFeatureJson) {
                    console.error("Nullified geometry");
                    throw "Nullified geometry";
                }
                if (differentiatedFeatureJson.type === "FeatureCollection") {
                    throw "Feature collection detected";
                }
                outputFeaturesArray.push(differentiatedFeatureJson);
            });
        });

        if (outputFeaturesArray.length > 0) {
            outputFeaturesArray.forEach(output => {
                if (output.geometry.type === "MultiPolygon") {
                    output.geometry.coordinates.forEach((coords) => {
                        var feat = { 'type': 'Polygon', 'coordinates': coords };

                        var feature = format.readFeature(feat, formatOptions);

                        feature.color = metaColor;
                        feature.fillColor = metaFillColor;
                        feature.penSize = metaPenSize;
                        feature.icon = metaIcon;
                        feature.notes = metaNotes ? metaNotes : "";

                        addFeature.call(this, feature, false);

                        this.viewport.annotationsLayer.getSource().addFeatures([feature]);
                    });
                } else {
                    var feature = format.readFeature(output, formatOptions);

                    feature.color = metaColor;
                    feature.fillColor = metaFillColor;
                    feature.penSize = metaPenSize;
                    feature.icon = metaIcon;
                    feature.notes = metaNotes ? metaNotes : "";

                    addFeature.call(this, feature, false);

                    this.viewport.annotationsLayer.getSource().addFeatures([feature]);

                    return feature;
                }
            });

            this.deleteAnnotation(annotationId1);
            this.deleteAnnotation(annotationId2);
        }
    }

    saveTransformedAnnotations(editedFeature, existing = true) {
        if (existing) {
            editedFeature.color = editedFeature.metaData.Color;
            editedFeature.penSize = editedFeature.metaData.LineThickness;
            editedFeature.fillColor = editedFeature.metaData.FillColor;
            addFeature.call(this, editedFeature, true);
        }
        else {
            addFeature.call(this, editedFeature, false);
        }
    }

    enterTransformMode(annotation) {
        var feature = annotation;
        var transformInteraction = new ol_interaction_Transform({
            layers: [this.viewport.annotationsLayer],
            enableRotatedTransform: false,
            translateFeature: true,
            scale: true,
            rotate: true,
            keepAspectRatio: false,
            translate: true,
            stretch: true,
        });

        this.edit = transformInteraction;
        this.edit.options = { feature: annotation };

        this.viewport.map.addInteraction(transformInteraction);
        transformInteraction.on('select', function (args) {
            if (args.feature && args.feature.metaData.State !== AnnotationState.NotTracked) {
                args.feature.metaData.State = args.feature.metaData.State === AnnotationState.Added ? AnnotationState.Added : AnnotationState.Modified;
            }
        });

        if (feature) {
            transformInteraction.select(feature, true);
        }
    }

    enterModifyMode(annotation) {
        var feature = annotation;
        var modifyInteraction = new olInteraction.Modify({
            source: this.viewport.annotationsLayer.getSource(),
            features: feature ? new Collection([feature]) : undefined,
        });

        this.edit = modifyInteraction;
        this.edit.options = { feature: annotation };
        if (feature && feature.metaData.State !== AnnotationState.NotTracked) {
            feature.metaData.State = feature.metaData.State === AnnotationState.Added ? AnnotationState.Added : AnnotationState.Modified;
        }

        this.viewport.map.addInteraction(modifyInteraction);
    }

    enterMagicWandMode(options) {
        if (!options) {
            console.error("No style options provided.");
            return;
        }

        var magicWandInteraction = new MagicWandInteraction({
            layers: [this.viewport.mainLayer],
            hatchLength: 4,
            hatchTimeout: 300,
            waitClass: 'magic-wand-loading',
            addClass: 'magic-wand-add',
            backgroundColor: this.viewport.imageInfo.backgroundColor,
        });

        this.edit = magicWandInteraction;
        this.edit.options = options;

        //this.viewport.annotationsLayer.setVisible(false);
        this.viewport.map.addInteraction(magicWandInteraction);
    }

    saveMagicWandAnnotation() {
        var contours = this.edit.getContours(0.5, 100);

        if (!contours) return;

        // var format = new olFormat.GeoJSON();
        // var formatOptions = {
        //     featureProjection: this.viewport.map.getView().getProjection(),
        // };

        var rings = contours.map(c => c.points.map(p => this.viewport.map.getCoordinateFromPixel([p.x, p.y])));

        var feature = new Feature({
            geometry: new Polygon(rings)
        });


        feature.color = this.edit.options.color;
        feature.fillColor = this.edit.options.fillColor;
        feature.penSize = this.edit.options.lineThickness;
        feature.icon = this.edit.options.icon;
        feature.notes = this.edit.options.notes;
        feature.drawingType = Annotation.ClosedFreehand;
        this.saveTransformedAnnotations(feature, false);
        this.viewport.annotationsLayer.getSource().addFeatures([feature]);
        //this.viewport.annotationsLayer.setVisible(true);
    }

    enterBrushMode(eraser, annotation, options) {
        if (!options) {
            options = {
                brushSize: 500,
                brushType: 'circle',
                drawMode: false
            };
        }

        var brush = new ol_interaction_Brush({
            map: this.viewport.map,
            feature: annotation,
            layer: this.viewport.annotationsLayer,
            brushSize: options.brushSize,
            brushType: options.brushType,
            brushMode: !eraser ? 'brush' : 'eraser',
        });

        brush.once("drawingstart", (evt) => {
            if (!options.drawMode) return;
            if (evt.target.getFeature() === null && evt.target.sel_ === null) {
                var newFeature = evt.target.pointer_.clone();
                this.viewport.annotationsLayer.getSource().addFeature(newFeature);
            }
        });

        brush.once('brushend', (evt) => {
            var feature = evt.target.getFeature();
            if (feature !== null) {
                if (!feature.metaData) {
                    feature.color = this.edit.options.color;
                    feature.fillColor = this.edit.options.fillColor;
                    feature.penSize = this.edit.options.lineThickness;
                    feature.icon = this.edit.options.icon;
                    feature.notes = this.edit.options.notes;

                    addFeature.call(this, feature, false);
                } else {
                    addFeature.call(this, feature, true);
                }
            }
        });

        this.edit = brush;
        this.edit.options = { ...options, feature: annotation };

        this.viewport.map.addInteraction(brush);
    }

    enterSubtractMode(annotation) {
        addInteraction.call(this, {
            type: Annotation.ClosedFreehand,
            color: "#000",
            fillColor: "#fff6",
            lineThickness: 1,
            notes: "",
            hideControls: true,
        });

        if (annotation) {
            annotation.originalStyle.getFill().setColor("rgba(0,0,0,0)");
            annotation.originalStyle.getStroke().setColor("#0098fc");
            annotation.originalStyle.getStroke().setWidth(annotation.originalStyle.getStroke().getWidth() + 3);
        }

        var self = this;

        this.draw.once('drawstart', () => {
            self.snapInteraction.setActive(false);
        });

        this.draw.once('drawend', (evt) => {
            setTimeout(() => {
                try {
                    var shouldDeleteAnnotation = false;
                    if (!annotation) {
                        var featExtent = evt.feature.getGeometry().getExtent();
                        var featuresIntersecting = self.viewport.annotationsLayer.getSource().getFeaturesInExtent(featExtent);
                        if (featuresIntersecting.filter(f => f.getId() !== evt.feature.getId()).length === 0) {
                            console.error("No intersecting annotation found!");
                            throw "No intersecting annotation found!";
                        }
                        var annotations = featuresIntersecting.filter(f => f.getId() !== evt.feature.getId());
                        var format = new olFormat.GeoJSON();
                        var formatOptions = {
                            featureProjection: this.viewport.map.getView().getProjection(),
                        };
                        var maskFeatureJson = format.writeFeatureObject(evt.feature, formatOptions);
                        for (var i = 0; i < annotations.length; i++) {
                            var detectedFeatureJson = format.writeFeatureObject(annotations[i], formatOptions);
                            if (booleanContains(maskFeatureJson, detectedFeatureJson)) {
                                annotation = annotations[i];
                                shouldDeleteAnnotation = true;
                                break;
                            }
                            if (booleanIntersects(maskFeatureJson, detectedFeatureJson)) {
                                annotation = annotations[i];
                                shouldDeleteAnnotation = false;
                                break;
                            }
                        }
                        if (!annotation) {
                            console.error("No intersecting annotation found!");
                            throw "No intersecting annotation found!";
                        }
                    }
                    if (shouldDeleteAnnotation) {
                        self.deleteAnnotation(annotation.getId());
                        self.deleteAnnotation(evt.feature.getId());
                    } else {
                        self.booleanDifference(annotation, evt.feature);
                    }
                } catch {
                    self.deleteAnnotation(evt.feature.getId());
                    if (annotation) {
                        self.setMetadata(annotation, annotation.metaData);
                    }
                } finally {
                    showEditingControls.call(self, false, AnnotationTools.Subtract);
                    self.edit = { options: { feature: annotation } };
                    self.stopTool();
                    self.snapInteraction.setActive(true);
                }
            }, 100)
        });

        this.edit = { options: { feature: annotation } };
    }

    enterAdditionMode(annotation) {
        addInteraction.call(this, {
            type: Annotation.ClosedFreehand,
            color: "#000",
            fillColor: "#fff6",
            lineThickness: 1,
            notes: "",
            hideControls: true,
        });

        if (annotation) {
            annotation.originalStyle.getFill().setColor("rgba(0,0,0,0)");
            annotation.originalStyle.getStroke().setColor("#0098fc");
            annotation.originalStyle.getStroke().setWidth(annotation.originalStyle.getStroke().getWidth() + 3);
        }

        var self = this;

        this.draw.once('drawstart', () => {
            self.snapInteraction.setActive(false);
        });

        this.draw.once('drawend', (evt) => {
            setTimeout(() => {
                try {
                    if (!annotation) {
                        var featExtent = evt.feature.getGeometry().getExtent();
                        var featuresIntersecting = self.viewport.annotationsLayer.getSource().getFeaturesInExtent(featExtent);
                        if (featuresIntersecting.filter(f => f.getId() !== evt.feature.getId()).length === 0) {
                            console.error("No intersecting annotation found!");
                            throw "No intersecting annotation found!";
                        }
                        var annotations = featuresIntersecting.filter(f => f.getId() !== evt.feature.getId());
                        var format = new olFormat.GeoJSON();
                        var formatOptions = {
                            featureProjection: this.viewport.map.getView().getProjection(),
                        };
                        var maskFeatureJson = format.writeFeatureObject(evt.feature, formatOptions);
                        for (var i = 0; i < annotations.length; i++) {
                            var detectedFeatureJson = format.writeFeatureObject(annotations[i], formatOptions);
                            if (booleanIntersects(maskFeatureJson, detectedFeatureJson)) {
                                annotation = annotations[i];
                                break;
                            }
                        }
                        if (!annotation) {
                            console.error("No intersecting annotation found!");
                            throw "No intersecting annotation found!";
                        }
                    }
                    self.booleanUnion([annotation, evt.feature]);
                } catch {
                    self.deleteAnnotation(evt.feature.getId());
                    if (annotation) {
                        self.setMetadata(annotation, annotation.metaData);
                    }
                } finally {
                    showEditingControls.call(self, false, AnnotationTools.Addition);
                    self.edit = { options: { feature: annotation } };
                    self.stopTool();
                    self.snapInteraction.setActive(true);
                }
            }, 100)
        });

        this.edit = { options: { feature: annotation } };
    }

    /**
    * Starts an annotation drawing/editing tool
    * @param {Object} options - Options parameter required to start a tool
    * @param {PMA.UI.Types.AnnotationTools} options.type - The type of annotation tool to start
    * @param {string} [options.brushType] - The type of brush tip to use, 'circle' or 'square'. Used by brush and eraser tool. Default: 'circle'
    * @param {Number} [options.brushSize] - The size of brush tip. Used by brush and eraser tool. Default: 500
    * @param {boolean} [options.drawMode] - Enables drawing of new features in brush mode. Default: false
    * @param {string} [options.color] - Annotation outline color as HTML hex string. Required if type is Magic Wand or drawMode is enabled
    * @param {string} [options.fillColor] - Annotation fill color as HTML hex string. Required if type is Magic Wand or drawMode is enabled
    * @param {Number} [options.lineThickness] - The line thickness. Required if type is Magic Wand or drawMode is enabled
    * @param {string} [options.notes] - Text to add to the annotation
    * @param {string} [options.iconRelativePath] - Relative path to an image that will be used when drawing a point. The base URL is defined by the imageBaseUrl property of the {@link Viewport~annotationOptions|annotations} initialization option supplied in the {@link Viewport|PMA.UI.View.Viewport} constructor
    * @param {ol.Feature} [options.feature] - An existing {@link http://openlayers.org/en/master/apidoc/ol.Feature.html | feature} to edit. If this argument has a value, the viewport goes into edit mode, instead of drawing a new annotation
    * @fires PMA.UI.Components.Events.AnnotationEditingStarted
    */
    startTool(options) {
        if (!this.getEnabled()) {
            return;
        }

        if (this.drawing) {
            console.error("Drawing already in progress. Finish drawing before starting editing.");
            return;
        }

        if (this.editing) {
            this.stopTool();
        }

        this.editing = true;

        var evtArgs = {
            feature: options.feature ? options.feature : null,
            edit: this.edit,
        };
        this.fireEvent(Events.AnnotationEditingStarted, evtArgs);

        if (options.feature && options.feature.metaData && options.feature.metaData.State === AnnotationState.Pristine) {
            options.feature.metaData.State = AnnotationState.Modified;
        }

        switch (options.type) {
            case AnnotationTools.Modify:
                this.enterModifyMode(options.feature);
                break;
            case AnnotationTools.Transform:
                this.enterTransformMode(options.feature);
                break;
            case AnnotationTools.Wand:
                if (typeof options.color === "undefined" || typeof options.fillColor === "undefined" || typeof options.lineThickness === "undefined") {
                    console.error("Required parameters missing!");
                    this.editing = false;
                    showEditingControls.call(this, false, null, null);
                    return;
                }
                this.enterMagicWandMode({
                    color: options.color,
                    fillColor: options.fillColor,
                    lineThickness: options.lineThickness,
                    iconRelativePath: options.iconRelativePath,
                    notes: options.notes
                });
                break;
            case AnnotationTools.Brush:
                if (options.drawMode && (typeof options.color === "undefined" || typeof options.fillColor === "undefined" || typeof options.lineThickness === "undefined")) {
                    console.error("Required parameters missing!");
                    this.editing = false;
                    showEditingControls.call(this, false, null, null);
                    return;
                }
                this.enterBrushMode(false, options.feature, { brushType: options.brushType, brushSize: options.brushSize, drawMode: options.drawMode || false, color: options.color, fillColor: options.fillColor, lineThickness: options.lineThickness, iconRelativePath: options.iconRelativePath, notes: options.notes });
                break;
            case AnnotationTools.Eraser:
                this.enterBrushMode(true, options.feature, { brushType: options.brushType, brushSize: options.brushSize });
                break;
            case AnnotationTools.Subtract:
                // if (!options.feature || Array.isArray(options.feature)) {
                //     console.error("No feature to edit!");
                //     this.editing = false;
                //     showEditingControls.call(this, false, null, null);
                //     return;
                // }
                this.enterSubtractMode(options.feature, options);
                break;
            case AnnotationTools.Addition:
                this.enterAdditionMode(options.feature, options);
                break;
            default:
                this.editing = false;
                showEditingControls.call(this, false, null, null);
                return;
        }
        showEditingControls.call(this, true, options.type, options.feature);
    }

    /**
     * @ignore
     */
    enterEditMode(options) {
        return this.startTool(options);
    }

    /**
    * Stops the annotation tool
    * @fires PMA.UI.Components.Events.AnnotationEditingEnded
    */
    stopTool() {
        var feature;
        if (this.editing) {
            this.clearSelection();
            if (this.edit != null) {
                if (this.edit && this.edit.options && this.edit.options.feature) {
                    feature = this.edit.options.feature;
                    this.setMetadata(this.edit.options.feature, this.edit.options.feature.metaData);
                }

                var features = this.viewport.getAnnotations();
                for (var i = 0; i < features.length; i++) {
                    if (!features[i].metaData || !features[i].metaData.State) {
                        continue;
                    }

                    switch (features[i].metaData.State) {
                        case AnnotationState.Modified:
                            updateWktFromFeature.call(this, features[i]);
                            updateAreaLength.call(this, features[i]);
                            break;
                    }
                }

                showEditingControls.call(this, false, null, null);
                if (typeof this.edit.stopEditing === "function") {
                    this.edit.stopEditing();
                }

                this.editing = false;
                if (this.viewport.getAnnotationLabelsVisible()) {
                    this.viewport.showAnnotationsLabels(this.viewport.getAnnotationLabelsVisible());
                }
            }
        }

        var evtArgs = {
            feature: feature ? feature : null,
            edit: this.edit,
        };
        this.fireEvent(Events.AnnotationEditingEnded, evtArgs);
        removeInteraction.call(this);
    }

    /**
     * @ignore
     * @deprecated
     */
    stopEditing() {
        return this.stopTool();
    }

    /**
     * Sets the metadata for an existing annotation
     * @param {ol.Feature} feature An annotation instance
     * @param {Annotations~Metadata} metadata The metadata to set
     */
    setMetadata(feature, metadata) {
        feature.metaData = metadata;
        if (feature.metaData.State === AnnotationState.Pristine) {
            feature.metaData.State = AnnotationState.Modified;
        }

        if (!feature.metaData.AnnotationID) {
            feature.metaData.State = AnnotationState.Added;
        }

        this.updateRenderedAnnotation(feature);
        this.updateStyleText(feature, this.viewport.getAnnotationFormattedLabel(feature));
        feature.changed();
    }

    updateStyleText(feature, text) {
        if (!feature || !text) {
            return;
        }

        let current = feature.getStyle();
        if (current && current.getText) {
            let textStyle = current.getText();
            if (textStyle) {
                textStyle.setText(text);
            }
        }

        let original = feature.originalStyle;
        if (original && original.getText) {
            let textStyle = original.getText();
            if (textStyle) {
                textStyle.setText(text);
            }
        }
    }

    updateRenderedAnnotation(feature) {
        var fillColor = feature.metaData.FillColor ? feature.metaData.FillColor : DefaultFillColor;
        var strokeColor = feature.metaData.Color ? feature.metaData.Color : "#3399CC";
        var strokeWidth = feature.metaData.LineThickness ? feature.metaData.LineThickness : 2;

        feature.originalStyle.getStroke().setColor(strokeColor);
        feature.originalStyle.getStroke().setWidth(strokeWidth);
        feature.originalStyle.getFill().setColor(fillColor);

        if ((feature.metaData.Geometry.indexOf("POINT") !== -1) && (strokeColor.startsWith("#") || strokeColor.startsWith("rgb"))) {
            feature.originalStyle.getImage().getStroke().setColor(strokeColor);
            feature.originalStyle.getImage().getStroke().setWidth(strokeWidth);
            feature.originalStyle.getImage().setRadius(5);
            feature.originalStyle.getImage().getFill().setColor(fillColor);
        }
    }
}

/**
 * An object describing the metadata associated with an annotation
 * @typedef Annotations~Metadata
 * @property {number} AnnotationID - The annotation id
 * @property {number} LayerID - The layer id for this annotations
 * @property {string} Classification - The classification of the annotaion
 * @property {string} Notes - The notes for this annotation if any
 * @property {string} Color - The color of the annotation
 * @property {Date} CreatedOn - The date this annotation was created
 * @property {string} CreatedBy - The user that created this annotation
 * @property {string} Image - The slide path this annotation belongs to
 * @property {string} Geometry - The WKT describing this annotation's geometry
 * @property {number} LineThickness - The line thickness this annotation is drawn
 * @property {number} Dimensions - The dimensionality of this annotation, i.e. 0 for point, 1 for lines, 2 for polygons
 * @property {string} FillColor - The color to fill this annotation if any
 * @property {string} Area - The area for this annotation in mm^2, if applicable
 * @property {string} FormattedArea - The area for this annotation in human friendly format
 * @property {string} Length - The length of this annotation in mm
 * @property {string} FormattedLength - The length of this annotation in human friendly format
 * @property {string} Context - The context value
 */