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 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
});
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);
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 = " + ";
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 = " - ";
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 = " - ";
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 = " + ";
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 = " ○ ";
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 = " □ ";
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
};
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);
});
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);
});
}
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) {
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);
}
feature.setStyle(self.viewport.getAnnotationStyle(stl, feature));
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 {
feature.setStyle(self.viewport.getAnnotationStyle(stl, feature));
if (updateExisting) {
if (self.viewport.getAnnotationLabelsVisible()) {
self.viewport.showAnnotationsLabels(self.viewport.getAnnotationLabelsVisible());
}
self.fireEvent(Events.AnnotationModified, evtArgs);
} else {
if (!self.shouldRejectDrawing && feature && feature.drawingType !== Annotation.CompoundFreehand) {
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 ` Area: ${formatArea(area, isPixels)} `;
}
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.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.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;
// 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);
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] - An optional flag to enable snapping. Defaults to false
*/
/**
* 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
];
}
}
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);
}
}
}
}
/**
* 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;
addFeature.call(this, feature, false);
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(strokeWidth);
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
*/