import { checkBrowserCompatibility, ajax } from "./helpers";
import {
Events,
ButtonLocations,
Themes,
ObjectiveResolutions,
DefaultFillColor,
AnnotationState,
Controls as ControlTypes,
Annotation,
} from "./definitions";
import {
initialize,
createProjection,
createMainView,
annotationTransform,
getControlVisibility,
createOverviewControl,
setControlVisibility,
setMapSizeClass,
login,
applyImageAdjustments,
printObjectivesInZoomBar,
setControlsConfiguration,
getControlsConfiguration,
parseJson,
getFingerprint,
updateProgress,
findClosestObjectiveValue,
drawScalebar,
drawTitle,
drawBarcode,
drawOverview,
getAnnotationsServer,
getRenderingOptions,
saveRenderingOptions,
} from "./viewportHelpers";
import { startMeasuring, createMeasureTooltip, stopMeasuring } from "./measure.js";
import { Overview } from "./controls/overview";
import { saveAs } from "file-saver";
import { ol } from "./definitionsOl";
import { fromCircle } from "ol/geom/Polygon";
import { default as olText } from "ol/style/Text";
import { Fill, Stroke } from "ol/style";
import { RotationControl } from "./controls/rotationControl";
import { unByKey } from "ol/Observable";
import { PmaMouseWheelZoom, MouseWheelZoomMode } from "./interactions/customMouseWheelZoom";
import { changeDpiBlob } from "./changeDpi";
import { Feature } from "ol";
/**
* Receives pixel data and applies an image transformation to it
* @callback PMA.UI.View.tileTransformer
* @param {ImageData} pixels - Represents the underlying pixel data of an area of a canvas element
*/
/**
* Viewport position
* @typedef {Object} Viewport~position
* @property {Array} center - The x and y coordinates of the center point
* @property {number} zoom - The zoom level. Can be omitted if resolution is specified
* @property {number} [resolution] - Can be omitted if zoom is specified
* @property {number} [rotation=0]
*/
/**
* Viewport Field of view
* @typedef {Object} Viewport~fov
* @property {Array} extent - The extent of the viewport [minx, miny, maxx, maxy]
* @property {number} rotation - The rotation of the viewport
* @property {bool} [constrainResolution] - Whether to contain the resolution to allowed values when fitting to extent
* @property {Array} [channels] - The selected channels
* @property {number} [layer=0] - The selected layer
* @property {number} [timeframe=0] - The selected timeframe
*/
/**
* Annotation display options
* @typedef {Object} Viewport~annotationOptions
* @property {boolean} [visible=true] - Whether or not to display the loaded annotations
* @property {boolean} [labels=false] - Whether or not to render the text label of each annotation in the viewer
* @property {boolean} [imageBaseUrl=""] - The base URL from which to load images
* @property {number} [imageScale=NaN] - Scale factor for images
* @property {boolean} [alwaysDisplayInMicrons=false] - Whether or not to automatically select the appropriate units for annotations (μm(^2) or mm(^2)) depending on the value
* @property {boolean} [showMeasurements=true] - Whether to show the length and area of an annotation
* @property {boolean} [loadAnnotationsByFingerprint=false] - Whether to load annotations based on image's fingerprint
* @property {boolean} [currentUserOnly=false] - Whether to load annotations for the current user only or for everyone
* @property {Array<string>} [contexts=[]] - Optional context filters
* @property {Viewport~annotationsFilterCallback} [filter] - A function that takes a PMA.core annotation and returns true if the annotation should be loaded, otherwise false
*/
/**
* Attribution display options
* @typedef {Object} Viewport~attributionOptions
* @property {string} html - The HTML contents to add inside the attribution container element
* @property {string} [className="ol-attr"] - The CSS class to assign to the attribution container element
*/
/**
* Image flip options
* @typedef {Object} Viewport~flipOptions
* @property {boolean} [horizontally=false] - Whether or not to flip the image horizontally
* @property {boolean} [vertically=false] - Whether or not to flip the image vertically
*/
/**
* Function that returns a file name to display in the viewer
* @callback Viewport~filenameCallback
* @param {Object} options
* @param {string} options.serverUrl - The URL of the current PMA.core server
* @param {string} options.fileName - The full path of the currently loaded image
* @returns {string}
*/
/**
* Function that takes a PMA.core annotation and returns true if the annotation should be loaded, otherwise false
* @callback Viewport~annotationsFilterCallback
* @param {Object} annotation - The annotation object to filter
* @returns {bool}
*/
/**
* A custom button to be added to the viewer
* @typedef {Object} Viewport~customButton
* @property {string} title - The title of the button
* @property {string} content - The inner html of the button
* @property {string} class - The class of the button
* @property {ButtonLocations} [location=ButtonLocations.S] - The location in the viewport of the custom button
* @property {function} callback - The callback to call when the button is clicked with this referring to the viewer
*/
/**
* An annotation as returned by pma.core
* @typedef Viewport~annotation
* @property {Number} AnnotationID - The annotation id
* @property {Number} LayerID - The layer id
* @property {string} Geometry - The annotation geometry in wkt format
* @property {string} [Notes] - Optional notes for the annotation
* @property {string} [Classification] - Optional classification string (Necrosis, tumor etc)
* @property {string} [Color] - Optional color
* @property {string} [CreatedBy] - Optional created by string
* @property {string} [UpdateInfo] - Optional update info
* @property {string} [Updatedby] - Optional updated by info
* @property {string} [FillColor] - Optional fill color
* @property {Number} [Dimensions] - Optional dimensionality of the annotation
* @property {string} [Context] - Optional context value of the annotation
*/
/**
* An object for configuring the visible and collapse state of a control
* @typedef Viewport~ControlConfiguration
* @property {Controls} control - The control to configure
* @property {boolean} visible - The visibility of the control
* @property {boolean} collapsed - Whether the control is collapsed or not
*/
/**
* An object for configuring the visible and collapse state of a control
* @typedef Viewport~SnapshotCoordinates
* @property {Integer} x - The x coordinate
* @property {Integer} y - The y coordinate
* @property {Integer} w - The width
* @property {Integer} h - The height
* @property {Number} scale - The scale of the current view in radians
* @property {Number} rotation - The rotation of the current view in radians
* @property {Viewport~flipOptions} flip - Whether the image is flipped
*/
/**
* Annotation display options
* @typedef {Object} Viewport~referenceImage
* @property {string} src="pathomation.png" - Source of reference image to load
* @property {Number} width=383 - Width of reference image
* @property {Number} height=183 - Height of reference image
* @property {string} [backgroundColor="#ffffff"] - Background color of viewport in css format
*/
/**
* Channel rendering options
* @typedef {Object} Viewport~ChannelRenderingOptions
* @property {Number} index - Index of the channel
* @property {string} name - Name of the channel
* @property {string} color - Color of the channel
* @property {string} defaultColor - Default color of the channel
* @property {Array} clipping - Clipping of the channel
* @property {Number} gamma - Gamma of the channel
*/
export
/**
* Creates a new viewer
* @param {Object} options - Initialization properties
* @param {string} options.caller - Downstream application identifier
* @param {string|HTMLElement} options.element - The element that hosts the viewer. It can be either a valid CSS selector or an HTMLElement instance
* @param {string} options.image - The path or UID of the image to load
* @param {Array} options.serverUrls - An array of one or more PMA.core URLs to attempt to connect to. The viewer will loop through all the provided URLs until an accessible one is found.
* @param {string} [options.username] - The PMA.core username to use to authenticate, in order to obtain a sessionID. If a username and password pair is provided, then a sessionID is not required.
* @param {string} [options.password] - The PMA.core password to use to authenticate, in order to obtain a sessionID. If a username and password pair is provided, then a sessionID is not required.
* @param {string} [options.sessionID] - The PMA.core sessionID to use to interact with PMA.core. If a sessionID is provided, then a username and password pair is not required.
* @param {Number} [options.keyboardPanFactor=0.5] - A factor to calculate pan delta when pressing a keyboard arrow. The actual pan in pixels is calculated as keyboardPanFactor * viewportWidth.
* @param {Themes} [options.theme="default"] - The theme to use
* @param {boolean|Object} [options.overview=true] - Whether or not to display an overview map
* @param {boolean} [options.overview.collapsed] - Whether or not to start the overview in collapsed state
* @param {boolean} [options.overview.tracking] - Whether or not to start viewport movement tracking
* @param {boolean|Object} [options.dimensions=true] - Whether or not to display the dimensions selector for images that have more than one channel, z-stack or timeframe
* @param {boolean} [options.dimensions.collapsed] - Whether or not to start the dimensions selector in collapsed state
* @param {Viewport~flipOptions} [options.flip] - Image flip options
* @param {boolean|Object} [options.barcode=false] - Whether or not to display the image's barcode if it exists
* @param {boolean} [options.barcode.collapsed=undefined] - Whether or not to start the barcode in collapsed state
* @param {Number} [options.barcode.rotation=undefined] - Rotation in steps of 90 degrees
* @param {boolean|Object} [options.loadingBar=true] - Whether or not to display a loading bar while the image is loading
* @param {Viewport~position} [options.position] - The initial position of the viewport within the image
* @param {boolean} [options.snapshot=false] - Whether or not to display a button that generates a snapshot image
* @param {boolean} [options.rotationControl=true] - Whether or not to display rotation controls
* @param {boolean} [options.rotationControl.collapsed] - Whether or not to start rotation controls in collapsed state
* @param {boolean} [options.fullscreenControl=true] - Whether or not to add a full screen toggle button
* @param {Viewport~annotationOptions} [options.annotations] - Annotation options
* @param {Viewport~referenceImage} [options.referenceImage] - Reference image options that will load if no server url, server credentials and image path are provided
* @param {Number} [options.digitalZoomLevels=0] - The number of digital zoom levels to add
* @param {boolean} [options.scaleLine=true] - Whether or not to display a scale line when resolution information is available
* @param {boolean} [options.colorAdjustments=false] - Whether or not to add a control that allows color adjustments
* @param {boolean|Object} [options.annotationsLayers=true] - Whether or not to show server side annotation layers
* @param {boolean} [options.annotationsLayers.collapsed=false] - Whether or not to show the annotation layers control collapsed
* @param {boolean} [options.annotationsLayers.loadLayers=false] - Whether or not to show all the annotation layers control on slide load
* @param {Object|boolean} [options.magnifier=false] - Whether or not to show the magnifier control
* @param {Object|boolean} [options.magnifier.collapsed=undefined] - Whether or not to show the magnifier control in collapsed state
* @param {Object} [options.grid] - Options for measurement grid
* @param {Array<number>} [options.grid.size] - Grid cell width and height in micrometers
* @param {string|Viewport~filenameCallback} [options.filename] - A string to display as the file name or a callback function. If no value is supplied, no file name is displayed.
* @param {boolean|Viewport~attributionOptions}[options.attributions=undefined] - Whether or not to display Pathomation attribution in the viewer
* @param {Array<Viewport~customButton>} [options.customButtons] - An array of one or more custom buttons to add to the viewer
* @param {Viewport~fov} [options.fov] - A specified field of view to load
* @param {boolean} [options.zoomSlider] - Whether or not to show the zoomSlider control
* @param {function} [readyCallback] - Called when the slide has finished loading
* @param {function} [failCallback] - Called when loading the slide failed
* @tutorial 01-viewer-minimal
* @tutorial 02-viewer-full
* @tutorial 05-annotations
* @tutorial 07-3rd-party-annotations
*/
class Viewport {
// options
// readyCallback
// failCallback
// element
// userInfo
// imageInfo
// image;
// sessionID;
// username;
// password;
// serverUrls
// overviewControl
// dimensionsControl
// barcodeControl
// colorAdjustmentsControl
// layerSwitcher
// zoomSliderControl
// scaleLineControl
// magnifierControl
// filenameControl
// snapShotControl
// rotationControl
// attributionControl
// serviceUrl
// imagesUrl
// channelsString
// imageAdjustments = { brightness: 0, contrast: 1, gamma: 1, tileTransformers: [] }
// listeners
// map
// hiddenAnnotations
// annotationsLayer
// selectedTimeFrame
// selectedLayer
// mainLayer
// measureLayer
// gridLayer
// measureDraw
constructor(options, readyCallback, failCallback) {
this.options = options;
if (!this.options.hasOwnProperty("magnifier")) {
this.options.magnifier = false;
}
if (!this.options.hasOwnProperty("colorAdjustments")) {
this.options.colorAdjustments = false;
}
if (!this.options.hasOwnProperty("annotations")) {
this.options.annotations = false;
}
if (!this.options.hasOwnProperty("fullscreenControl")) {
this.options.fullscreenControl = true;
}
this.readyCallback = readyCallback;
this.failCallback = failCallback;
if (!this.options.flip) {
this.options.flip = {
horizontally: false,
vertically: false,
};
}
if (!checkBrowserCompatibility(options.element)) {
return;
}
if (typeof options.caller !== "string" && this.image) {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "Caller parameter not supplied";
}
if (typeof options.theme !== "string") {
options.theme = "default";
}
if (options.element) {
if (options.element instanceof HTMLElement) {
this.element = options.element;
} else if (typeof options.element == "string") {
var el = document.querySelector(options.element);
if (!el) {
throw "Invalid selector for element";
} else {
this.element = el;
}
}
} else {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "No element provided";
}
this.userInfo = null;
this.imageInfo = options.imageInfo;
if (options.image) {
this.image = options.image;
} else if (!this.imageInfo) {
if (!options.referenceImage || (!options.referenceImage.src || !options.referenceImage.width || !options.referenceImage.height)) {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "No image path, UID or reference image provided";
}
} else {
this.image = this.imageInfo.Filename;
}
if (options.sessionID) {
this.sessionID = options.sessionID;
} else if (options.username || options.password) {
this.username = options.username;
this.password = options.password;
if (!options.username || !options.password) {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "Username and/or password not povided";
}
} else {
if (this.image) {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "SessionID or Username not provided. Cannot login.";
}
}
if (
options.serverUrls &&
options.serverUrls instanceof Array &&
options.serverUrls.length > 0
) {
this.serverUrls = options.serverUrls;
} else if (!this.imageInfo) {
if (this.image) {
if (typeof this.failCallback === "function") {
this.failCallback();
}
throw "Server URLs not provided or is not an array and no image info was given.";
}
}
/**
* The overview control
* @type {Overview}
* @public
*/
this.overviewControl = null;
/**
* The dimension selector control
* @type {DimensionSelector}
* @public
*/
this.dimensionsControl = null;
/**
* The barcode control
* @type {AssociatedImage}
* @public
*/
this.barcodeControl = null;
/**
* The color adjustment control
* @type {ColorAdjustment}
* @public
*/
this.colorAdjustmentsControl = null;
/**
* The rotation control
* @type {RotationControl}
* @public
*/
this.rotationControl = null;
/**
* The server side annotation layers control
* @type {LayerSwitch}
* @public
*/
this.layerSwitcher = null;
/**
* The mouse wheel interaction
* @type {PmaMouseWheelZoom}
* @public
*/
this.mouseWheelInteraction = null
/**
* The measure draw interaction
* @type {Draw}
* @public
*/
this.measureDraw = null;
this.zoomSliderControl = null;
this.scaleLineControl = null;
this.magnifierControl = null;
this.filenameControl = null;
this.snapShotControl = null;
this.attributionControl = null;
this.serviceUrl = null;
this.imagesUrl = null;
this.channelsString = null;
this.channelClippingString = null;
this.channelColorString = null;
this.channelGammaString = null;
this.imageAdjustments = {
brightness: 0,
contrast: 1,
gamma: 1,
rgb: [1, 1, 1],
tileTransformers: [],
};
if (this.options.loadingBar === undefined) {
this.options.loadingBar = true;
}
if (this.options.annotationsLayers === undefined) {
this.options.annotationsLayers = true;
}
// holds callbacks for events
this.listeners = {};
this.listeners[Events.DimensionsChanged] = [];
this.listeners[Events.ViewChanged] = [];
this.listeners[Events.TilesError] = [];
this.listeners[Events.SlideLoadError] = [];
this.listeners[Events.FilenameClick] = [];
this.listeners[Events.AnnotationLayerChanged] = [];
this.listeners[Events.MeasurementStop] = [];
this.listeners[Events.ViewLoaded] = [];
/**
* The underlying {@link https://openlayers.org/en/latest/apidoc/ol.Map.html|OpenLayers map}
* @public
*/
this.map = null;
this.debounceFn = function (func, delay) {
return function (...args) {
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => func.apply(this, args), delay);
}
}
this.debouncedSetRenderingOptions = this.debounceFn(saveRenderingOptions.bind(this), 1000);
initialize.call(this);
}
/**
* Calculates the area of an OpenLayers geometry
* @param {ol.geom} geom - The geometry to calculate the area of
* @returns {number} The area in squared microns or zero if resolution information is not available
*/
calculateArea(geom) {
if (this.imageInfo.MicrometresPerPixelX === 0) {
return 0;
}
var pixelCoords = null;
if (geom instanceof ol.geom.Polygon) {
pixelCoords = geom.getCoordinates()[0];
} else if (geom instanceof ol.geom.LineString) {
////pixelCoords = geom.getCoordinates();
return 0;
} else if (geom instanceof ol.geom.Circle) {
pixelCoords = fromCircle(geom).getCoordinates()[0];
} else if (geom instanceof ol.geom.GeometryCollection) {
var self = this;
var geometries = geom.getGeometries();
var area = 0;
for (var j = 0; j < geometries.length; j++) {
area += self.calculateArea.call(self, geometries[j]);
}
return area;
} else {
return 0;
}
var transformed = [];
for (var i = 0; i < pixelCoords.length; i++) {
transformed.push([
pixelCoords[i][0] * this.imageInfo.MicrometresPerPixelX,
pixelCoords[i][1] * this.imageInfo.MicrometresPerPixelY,
]);
}
var tmpGeom = new ol.geom.Polygon([transformed]);
var v = tmpGeom.getArea();
tmpGeom = null;
return v;
}
/**
* Calculates the length of an OpenLayers geometry
* @param {ol.geom} geom - The geometry to calculate the length of
* @returns {number} The length in microns or zero if resolution information is not available
*/
calculateLength(geom) {
if (this.imageInfo.MicrometresPerPixelX === 0) {
return 0;
}
var pixelCoords = null;
if (geom instanceof ol.geom.Polygon) {
pixelCoords = geom.getCoordinates()[0];
} else if (geom instanceof ol.geom.LineString) {
pixelCoords = geom.getCoordinates();
} else if (geom instanceof ol.geom.Circle) {
pixelCoords = fromCircle(geom).getCoordinates()[0];
} else if (geom instanceof ol.geom.GeometryCollection) {
var self = this;
var geometries = geom.getGeometries();
var length = 0;
for (var j = 0; j < geometries.length; j++) {
length += self.calculateLength.call(self, geometries[j]);
}
return length;
} else {
return 0;
}
var transformed = [];
for (var i = 0; i < pixelCoords.length; i++) {
transformed.push([
pixelCoords[i][0] * this.imageInfo.MicrometresPerPixelX,
pixelCoords[i][1] * this.imageInfo.MicrometresPerPixelY,
]);
}
var tmpGeom = new ol.geom.LineString(transformed);
var v = tmpGeom.getLength();
tmpGeom = null;
return v;
}
/**
* Formats the length of an OpenLayers geometry
* @param {number} length - The length of the geometry to format
* @returns {number} The string with the formatted length and unit
* @ignore
*/
formatLength(length) {
if (this.imageInfo.MicrometresPerPixelX === 0) {
return 0;
}
var alwaysDisplayInMicrons =
this.options.annotations &&
this.options.annotations.alwaysDisplayInMicrons === true;
var output;
if (!alwaysDisplayInMicrons && length >= 1000) {
output = Math.round((length * 10) / 1000) / 10 + " mm";
} else {
output = Math.round(length * 10) / 10 + " μm";
}
return output;
}
/**
* Formats the area of an OpenLayers geometry
* @param {number} area - The area of the geometry to format
* @returns {number} The string with the formatted area and unit
*/
formatArea(area) {
if (this.imageInfo.MicrometresPerPixelX === 0) {
return 0;
}
var supscript = String.fromCharCode("2".charCodeAt(0) + 128);
var alwaysDisplayInMicrons =
this.options.annotations &&
this.options.annotations.alwaysDisplayInMicrons === true;
var output;
if (!alwaysDisplayInMicrons && area >= 100000) {
output = Math.round((area * 10) / 1000000) / 10 + " mm" + supscript; //<sup>2</sup>';
} else {
output = Math.round(area * 10) / 10 + " μm" + supscript; // <sup>2</sup>';
}
return output;
}
/**
* Returns the text style to be used to render the text part (labels) of annotations. This method is not meant to be used by PMA.UI consumers.
* @param {ol.Feature} [feature] - The ol.Feature associated with the annotation
* @returns {ol.style.Text} A text style instance
*/
getAnnotationTextStyle(feature, strokeColor) {
var txt = this.getAnnotationFormattedLabel(feature);
return new olText({
text: txt,
textAlign: "center",
textBaseline: "bottom",
placement: "point",
overflow: true,
font: "normal 16px Tahoma",
fill: new Fill({
color: "#000000",
}),
stroke: new Stroke({
color: "#ffffff",
width: 2,
}),
});
}
/**
* Returns the text of an annotation. This method is not meant to be used by PMA.UI consumers.
* @param {ol.Feature} [feature] - The ol.Feature associated with the annotation
* @returns {String} The annotation text
*/
getAnnotationFormattedLabel(feature) {
var txt = "";
if (this.options.annotations && feature && feature.metaData) {
if (this.options.annotations.labels !== false && feature.metaData.Notes) {
txt = feature.metaData.Notes;
}
if (this.options.annotations.showMeasurements !== false) {
var geomType = feature.getGeometry().getType();
if (feature.metaData.hasOwnProperty("Dimensions") && !isNaN(feature.metaData.Dimensions)) {
if (feature.metaData.Dimensions > 1) {
txt += "\n" + feature.metaData.FormattedArea;
} else if (feature.metaData.Dimensions > 0) {
txt += "\n" + feature.metaData.FormattedLength;
}
} else {
if (geomType.indexOf("LineString") !== -1) {
txt += "\n" + feature.metaData.FormattedLength;
} else if (geomType.indexOf("Polygon") !== -1 || geomType.indexOf("Circle") !== -1) {
txt +=
// "\n" +
// feature.metaData.FormattedLength +
"\n" +
feature.metaData.FormattedArea;
}
}
}
}
return txt;
}
/**
* Returns a style that contatins the text of an annotation. This method is not meant to be used by PMA.UI consumers.
* @param {ol.style.Style} [style] - The ol.style.Style associated with the annotation
* @param {ol.Feature} [feature] - The ol.Feature associated with the annotation
* @returns {Function} The annotation style function
*/
getAnnotationStyle(style, feature) {
style.getText().setText(this.getAnnotationFormattedLabel(feature));
return style;
// var self = this;
// return function () {
// style.getText().setText(self.getAnnotationFormattedLabel(feature));
// return style;
// };
}
/**
* Toggles the visibility of an annotation
* @param {number} id - The id of the annotation to hide or show
* @param {boolean} show - True to show the annotation, otherwise hide it
*/
showAnnotation(id, show) {
if (id === null || isNaN(id)) {
return;
}
if (!this.hiddenAnnotations) {
this.hiddenAnnotations = [];
}
var f = null;
if (show) {
for (var k = 0; k < this.hiddenAnnotations.length; k++) {
if (this.hiddenAnnotations[k].getId() == id) {
f = this.hiddenAnnotations.splice(k, 1);
this.annotationsLayer.getSource().addFeatures(f);
return;
}
}
} else {
f = this.annotationsLayer.getSource().getFeatureById(id);
if (f) {
this.annotationsLayer.getSource().removeFeature(f);
this.hiddenAnnotations.push(f);
}
}
}
/**
* Toggles the visibility of annotations loaded by fingerprint and loads them if not already loaded
* @param {boolean} show - True to show the annotation, otherwise hide it
*/
showAnnotationsByFingerprint(show) {
if (show) {
if (!this.forceLoadedAnnotationsByFingerprint) {
if (!this.options.annotations.loadAnnotationsByFingerprint) {
getFingerprint.call(this, (fingerprint) => {
this.reloadAnnotations(null, fingerprint);
});
this.forceLoadedAnnotationsByFingerprint = true;
}
}
}
if (!this.hiddenAnnotationsByFingerprint) {
this.hiddenAnnotationsByFingerprint = [];
}
var f = null;
var annotations = this.annotationsLayer.getSource().getFeatures();
if (show) {
for (var k = this.hiddenAnnotationsByFingerprint.length; k > 0; k--) {
f = this.hiddenAnnotationsByFingerprint.pop();
this.annotationsLayer.getSource().addFeature(f);
}
} else {
for (var l = 0; l < annotations.length; l++) {
f = annotations[l];
if (f.metaData.Image !== this.imageInfo.Filename) {
this.annotationsLayer.getSource().removeFeature(f);
this.hiddenAnnotationsByFingerprint.push(f);
}
}
}
}
/**
* Toggles the visibility of the annotations layer in total
* @param {boolean} show - True to show the annotations, otherwise hide them
*/
showAnnotations(visible) {
if (!this.annotationsLayer) {
return;
}
this.annotationsLayer.setVisible(visible);
}
/**
* Gets a value indicating whether or not the text of the annotations is rendered in the viewer
*/
getAnnotationLabelsVisible() {
if (!this.annotationsLayer) {
return false;
}
return this.options.annotations.labels === true;
}
/**
* Toggles the text of the annotations visibility
* @param {boolean} visible - True to show the annotation labels, otherwise hide them
* @param {boolean} [showMeasurements] - True to show the annotation measurements, otherwise hide them
*/
showAnnotationsLabels(visible, showMeasurements) {
if (!this.annotationsLayer) {
return;
}
var _this = this;
if (showMeasurements !== undefined) {
if (this.options.annotations.labels === visible && this.options.annotations.showMeasurements === showMeasurements) {
return;
}
this.options.annotations.showMeasurements = showMeasurements;
} else {
showMeasurements = this.options.annotations.showMeasurements;
}
this.options.annotations.labels = visible;
this.annotationsLayer
.getSource()
.forEachFeature(function (feature, index) {
var styles = feature.getStyle();
var style = [];
if (styles instanceof Array) {
for (var k = 0; k < styles.length; k++) {
if (styles[k].getText()) {
style.push(styles[k]);
break;
}
}
} else {
style = [styles];
}
for (var k = 0; k < style.length; k++) {
var s = style[k];
if (!s) {
continue;
}
try {
if (!visible && !showMeasurements) {
s.getText().setText("");
} else {
s.getText().setText(_this.getAnnotationFormattedLabel(feature));
}
} catch (e) {
continue;
}
}
//feature.changed();
}, this);
this.annotationsLayer.changed();
}
/* zoomToAnnotation(annotationIndex) {
this.map.getLayers().forEach(function (layer, i) {
if (layer instanceof ol.layer.Vector) {
var selection = layer.getSource().getFeatures()[annotationIndex];
var extent = selection.getGeometry().getExtent();
var view2D = this.map.getView();
view2D.fit(extent, this.map.getSize());
}
}, this);
};
*/
/**
* Returns all the annotations
* @returns {Array} An array of ol.Feature instances
*/
getAnnotations() {
if (!this.annotationsLayer) {
return null;
}
if (this.hiddenAnnotations && this.hiddenAnnotations.length > 0) {
return this.annotationsLayer
.getSource()
.getFeatures()
.concat(this.hiddenAnnotations)
.filter(f => !!f.metaData);
} else {
return this.annotationsLayer.getSource().getFeatures().filter(f => !!f.metaData);
}
}
/**
* Gets an array that contains the indices of the currently active channels
* @returns {Array}
*/
getActiveChannels() {
var channels = [];
for (
var i = 0; i < this.imageInfo.TimeFrames[0].Layers[0].Channels.length; i++
) {
if (this.imageInfo.TimeFrames[0].Layers[0].Channels[i].Active) {
channels.push(i);
}
}
return channels;
}
/**
* Sets the active channels
* @param {Array} channels - An array that contains the indices of the channels to display
* @fires PMA.UI.View.Events#DimensionsChanged
*/
setActiveChannels(channels) {
for (var i = 0; i < this.imageInfo.TimeFrames[0].Layers[0].Channels.length; i++) {
this.imageInfo.TimeFrames[0].Layers[0].Channels[i].Active = false;
}
for (i = 0; i < channels.length; i++) {
this.imageInfo.TimeFrames[0].Layers[0].Channels[
channels[i]
].Active = true;
}
this.channelsString = this.getActiveChannels().join(",");
this.channelClippingString = this.getChannelClippingString();
this.channelGammaString = this.getChannelGammaString();
this.channelColorString = this.getChannelColorString();
this.debouncedSetRenderingOptions.call(this);
this.redraw();
this.fireEvent(Events.DimensionsChanged, this);
}
/**
* Gets the rendering options for all channels or specific channel
* @param {object} [options] - An object that determines the channel to get rendering options. If no options exist, all channels will be returned
* @param {number} [options.index] - The index of the channel to get rendering options
* @returns {Viewport~ChannelRenderingOptions[]} renderingOptions - The rendering options object
*/
getChannelRenderingOptions(options) {
if (options && Number.isInteger(options.index)) {
const channel = this.imageInfo.TimeFrames[0].Layers[0].Channels[options.index];
if (!channel) {
console.error("Channel not found!");
return;
}
return [{
index: options.index,
name: channel.Name,
color: (channel.UserOptions && channel.UserOptions.Color) ? channel.UserOptions.Color : channel.Color,
defaultColor: channel.Color,
clipping: channel.UserOptions ? channel.UserOptions.Clipping : null,
gamma: (channel.UserOptions && channel.UserOptions.Gamma) ? channel.UserOptions.Gamma : channel.Gamma
}];
} else {
const channels = this.imageInfo.TimeFrames[0].Layers[0].Channels;
let channelRenderingOptions = [];
for (let i = 0; i < channels.length; i++) {
const channel = channels[i];
channelRenderingOptions.push({
index: i,
name: channel.Name,
color: (channel.UserOptions && channel.UserOptions.Color) ? channel.UserOptions.Color : channel.Color,
defaultColor: channel.Color,
clipping: channel.UserOptions ? channel.UserOptions.Clipping : null,
gamma: (channel.UserOptions && channel.UserOptions.Gamma) ? channel.UserOptions.Gamma : channel.Gamma
});
}
return channelRenderingOptions;
}
}
/**
* Sets the rendering options for a channel
* @param {object} options - An object that determines the channel and the options to be set
* @param {number} options.index - An object that determines the channel and the options to be set
* @param {string} options.color - Channel's color in css format
* @param {Array<Number>} options.clipping - A pair of values [0..100] to clip and scale pixel values
* @param {Number} options.gamma - A value of tha gamma correction factor for this channel
*/
setChannelRenderingOptions(options) {
var ch = this.imageInfo.TimeFrames[0].Layers[0].Channels[options.index];
ch.UserOptions = {
Clipping: options.clipping,
Color: options.color ? options.color : ch.Color,
Gamma: options.gamma ? options.gamma : ch.Gamma
};
this.channelClippingString = this.getChannelClippingString();
this.channelGammaString = this.getChannelGammaString();
this.channelColorString = this.getChannelColorString();
this.debouncedSetRenderingOptions.call(this);
this.redraw();
this.fireEvent(Events.DimensionsChanged, this);
}
getChannelClippingString() {
const channels = this.imageInfo.TimeFrames[0].Layers[0].Channels;
this.channelClippingString = "";
for (let i = 0; i < channels.length; i++) {
if (!channels[i].Active) {
continue;
}
if (this.channelClippingString.length > 0) {
this.channelClippingString += ",";
}
if ((channels[i].UserOptions && channels[i].UserOptions.Clipping) || channels[i].Clipping) {
this.channelClippingString += (channels[i].UserOptions && channels[i].UserOptions.Clipping) ?
channels[i].UserOptions.Clipping[0] + "," + channels[i].UserOptions.Clipping[1] :
channels[i].Clipping[0] + "," + channels[i].Clipping[1];
} else {
this.channelClippingString += "0,100";
}
}
return this.channelClippingString;
}
getChannelColorString() {
const channels = this.imageInfo.TimeFrames[0].Layers[0].Channels;
this.channelColorString = "";
for (let i = 0; i < channels.length; i++) {
if (!channels[i].Active) {
continue;
}
if (this.channelColorString.length > 0) {
this.channelColorString += ",";
}
if (channels[i].Color) {
this.channelColorString += (channels[i].UserOptions && channels[i].UserOptions.Color) ? channels[i].UserOptions.Color : channels[i].Color;
} else {
this.channelColorString += "";
}
}
return this.channelColorString;
}
getChannelGammaString() {
const channels = this.imageInfo.TimeFrames[0].Layers[0].Channels;
this.channelGammaString = "";
for (let i = 0; i < channels.length; i++) {
if (!channels[i].Active) {
continue;
}
if (this.channelGammaString.length > 0) {
this.channelGammaString += ",";
}
if (channels[i].Gamma) {
this.channelGammaString += (channels[i].UserOptions && channels[i].UserOptions.Gamma) ? channels[i].UserOptions.Gamma : channels[i].Gamma;
} else {
this.channelGammaString += "1.0";
}
}
return this.channelGammaString;
}
/**
* Gets the index of the active time frame
* @returns {number}
*/
getActiveTimeFrame() {
return this.selectedTimeFrame;
}
/**
* Sets the active time frame
* @param {number} timeframe - The index of the time frame to activate
* @fires PMA.UI.View.Events#DimensionsChanged
*/
setActiveTimeFrame(timeframe) {
timeframe = parseInt(timeframe);
if (timeframe >= 0 && timeframe < this.imageInfo.TimeFrames.length) {
this.selectedTimeFrame = timeframe;
this.redraw();
this.fireEvent(Events.DimensionsChanged, this);
}
}
/**
* Gets the index of the active layer (z-stack)
* @returns {number}
*/
getActiveLayer() {
return this.selectedLayer;
}
/**
* Sets the active layer (z-stack)
* @param {number} layer - The index of the layer to activate
* @fires PMA.UI.View.Events#DimensionsChanged
*/
setActiveLayer(layer) {
layer = parseInt(layer);
if (
layer >= 0 &&
layer <
this.imageInfo.TimeFrames[this.selectedTimeFrame].Layers.length
) {
this.selectedLayer = layer;
this.redraw();
this.fireEvent(Events.DimensionsChanged, this);
}
}
/**
* Sets the image flip options
* @param {bool} horizontally - Whether or not to flip the image horizontally
* @param {bool} vertically - Whether or not to flip the image vertically
*/
setFlip(horizontally, vertically) {
if (!this.image) {
return;
}
var newH = horizontally == true;
var newV = vertically == true;
var hasHorizontalChange = this.options.flip.horizontally !== newH;
var hasVerticalChange = this.options.flip.vertically !== newV;
if (!hasHorizontalChange && !hasVerticalChange) {
return;
}
// reset all annotations, before we change the transformation
var i, geom;
var oldExtent = this.map.getView().getProjection().getExtent();
var oldPos = this.getPosition();
var oldView = this.map.getView();
var extent = oldView.getProjection().getExtent();
var extentObj = { extent: extent, flip: this.options.flip };
var annotations = this.getAnnotations();
if (annotations) {
for (i = 0; i < annotations.length; i++) {
geom = annotations[i].getGeometry();
geom.applyTransform(annotationTransform.bind(extentObj));
}
}
this.options.flip.horizontally = newH;
this.options.flip.vertically = newV;
var pixelProjection = createProjection.call(this);
this.map.setView(
createMainView.call(
this,
pixelProjection,
null,
null, -oldView.getRotation()
)
);
// now apply the new transformation
extent = this.map.getView().getProjection().getExtent();
extentObj = { extent: extent, flip: this.options.flip };
if (annotations) {
for (i = 0; i < annotations.length; i++) {
geom = annotations[i].getGeometry();
geom.applyTransform(annotationTransform.bind(extentObj));
}
}
// if (this.options.grid) {
// this.toggleGrid(this.options.grid.size);
// }
var pos = this.getPosition();
pos.zoom = oldPos.zoom;
if (hasHorizontalChange) {
pos.center[0] = extent[0] + (oldExtent[2] - oldPos.center[0]);
} else {
pos.center[0] = extent[0] + (oldPos.center[0] - oldExtent[0]);
}
if (hasVerticalChange) {
pos.center[1] = extent[1] + (oldExtent[3] - oldPos.center[1]);
} else {
pos.center[1] = extent[1] + (oldPos.center[1] - oldExtent[1]);
}
this.setPosition(pos, null);
this.redraw();
if (this.overviewControl) {
this.overviewControl.setMap(null);
var overviewVisible = getControlVisibility.call(
this,
this.element.querySelector(".ol-overview")
);
var overviewCollapsed = this.overviewControl && this.overviewControl.getCollapsed();
this.map.removeControl(this.overviewControl);
this.overviewControl = createOverviewControl.call(this);
this.map.addControl(this.overviewControl);
if (!overviewVisible) {
setControlVisibility.call(
this,
this.element.querySelector(".ol-overview"),
false
);
}
this.overviewControl.setCollapsed(overviewCollapsed);
setMapSizeClass.call(this);
}
this.fireEvent(Events.ViewChanged, this);
}
/**
* Gets the image flip options
* @returns {Viewport~flipOptions}
*/
getFlip() {
return {
horizontally: this.options.flip.horizontally,
vertically: this.options.flip.vertically,
};
}
/**
* Forces the viewer to refresh
*/
redraw() {
if (!this.image) {
return;
}
var array = this.map.getLayers().getArray();
var layer;
for (var i = 0; i < array.length; i++) {
if (array[i].getSource) {
if (array[i] === this.mainLayer) {
var maxZoom = this.imageInfo.MaxZoomLevel;
var tilesPerBoxSide = Math.pow(2, maxZoom);
layer = new ol.layer.Tile({
source: new ol.source.XYZ({
tileUrlFunction: this.mainLayer.getSource().getTileUrlFunction(),
tileLoadFunction: this.mainLayer.getSource().getTileLoadFunction(),
projection: this.mainLayer.getSource().getProjection(),
wrapX: false,
attributions: "",
crossOrigin: "PMA.UI",
tileGrid: ol.tilegrid.createXYZ({
tileSize: [this.imageInfo.TileSize, this.imageInfo.TileSize],
extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize],
maxZoom: maxZoom
}),
}),
className: "ol-layer main-layer",
extent: [0, 0, tilesPerBoxSide * this.imageInfo.TileSize, tilesPerBoxSide * this.imageInfo.TileSize],
});
layer.set("active", true, true);
this.mainLayer.set("active", false, true);
if (this.options.loadingBar) {
unByKey(this.listeners.tileloadstart);
unByKey(this.listeners.tileloadend);
unByKey(this.listeners.tileloaderror);
this.listeners.tileloadstart = [];
this.listeners.tileloadend = [];
this.listeners.tileloaderror = [];
this.loading = 0;
this.loaded = 0;
updateProgress.call(this);
var olViewer = this;
this.listeners.tileloadstart.push(layer.getSource().on('tileloadstart', function () {
if (olViewer.loading === 0) {
olViewer.progressEl.style.visibility = 'visible';
}
++olViewer.loading;
updateProgress.call(olViewer);
}));
this.listeners.tileloadend.push(layer.getSource().on('tileloadend', function () {
setTimeout(function () {
++olViewer.loaded;
updateProgress.call(olViewer);
}, 100);
}));
this.listeners.tileloaderror.push(layer.getSource().on('tileloaderror', function () {
setTimeout(function () {
++olViewer.loaded;
updateProgress.call(olViewer);
}, 100);
}));
}
setTimeout((map, mainLayer, overviewControl) => {
overviewControl.setMap(null);
map.removeLayer(mainLayer);
overviewControl.setMap(map);
}, 10000, this.map, this.mainLayer, this.overviewControl);
}
// else {
// if (!(array[i].getSource() instanceof ol.source.XYZ)) {
// array[i].getSource().refresh();
// }
// }
} else if (array[i].getLayers) {
var groupLayers = array[i].getLayers().getArray();
for (var j = 0; j < groupLayers.length; j++) {
groupLayers[j].getSource().refresh();
groupLayers[j].setZIndex(999);
}
}
}
if (layer) {
this.mainLayer = layer;
this.map.addLayer(this.mainLayer);
this.mainLayer.setZIndex(900);
if (this.annotationsLayer) {
this.annotationsLayer.setZIndex(1000);
}
if (this.measureLayer) {
this.measureLayer.setZIndex(10000);
}
}
}
/**
* Zooms the view to a specific objective
* @param {string} objective - 1:1, 1X, 2X, 5X, 10X etc.
*/
zoomToObjective(objective) {
if (!this.imageInfo || this.imageInfo.MicrometresPerPixelX === 0) {
return;
}
var mapResolution;
if ("1:1" === objective) {
mapResolution = 1;
} else {
objective = parseFloat(objective);
if (isNaN(objective) || objective === 0.0) {
return;
}
// mapResolution =
// (this.imageInfo.MicrometresPerPixelX - 0.75) /
// -0.0125 /
// objective;
mapResolution = 10 / this.imageInfo.MicrometresPerPixelX / objective;
}
this.map.getView().setResolution(mapResolution);
}
/**
* Gets the current viewport position
* @returns {Viewport~position} position
*/
getPosition() {
if (!this.map) {
return null;
}
var v = this.map.getView();
if (!v) {
return null;
}
return {
center: v.getCenter(),
zoom: v.getZoom(),
resolution: v.getResolution(),
rotation: v.getRotation(),
};
}
/**
* sets the current viewport position
* @param {Viewport~position} position
*/
setPosition(position, skipAnimation) {
if (!this.map) {
return;
}
var mainView = this.map.getView();
mainView.animate(Object.assign(position, { duration: 0 }));
}
/**
* Fits the view to supplied extent
* @param {ol.geom.SimpleGeometry|Array.<number>} extent - An array of numbers representing an extent: `[minx, miny, maxx, maxy]` or a simple geometry
* @param {bool} constrainResolution - Whether to fit at any resolution even outside the allowed range
*/
fitToExtent(extent, constrainResolution) {
if (!this.map) {
return;
}
var mainView = this.map.getView();
mainView.fit(extent, {
size: this.map.getSize(),
constrainResolution: constrainResolution ?
constrainResolution : false,
});
}
/**
* Gets the currently visible extent
* @param {bool} unrotate - Whether to unrotate the extend back to axis aligned
* @returns {Array.<number>} An array of numbers representing an extent: `[minx, miny, maxx, maxy]`
*/
getCurrentExtent(unrotate) {
if (!this.map) {
return;
}
var mainView = this.map.getView();
var extent = mainView.calculateExtent(this.map.getSize());
if (unrotate) {
return this.rotateExtent(extent, -mainView.getRotation());
}
return extent;
}
/**
* Overrides the current sessionID and forces the viewer to redraw
* @param {string} sessionID
*/
setSessionID(sessionID) {
this.sessionID = sessionID;
this.redraw();
}
/**
* Gets the currently used sessionID
* @returns {string}
*/
getSessionID() {
return this.sessionID;
}
/**
* Authenticates and forces the viewer to redraw
* @param {string} username
* @param {string} password
*/
login(username, password) {
if (username) {
this.username = username;
}
if (password) {
this.password = password;
}
login.call(this, this.redraw);
}
/**
* Generates a rectangle for the current view
* @returns {Array.<number[]>} An array of the x, y coordinates of the 4 corners of viewing rectangle
*/
getViewportCoordinates() {
var mapSize = this.map.getSize();
var coords = [
this.map.getCoordinateFromPixel([0, 0]),
this.map.getCoordinateFromPixel([mapSize[0], 0]),
this.map.getCoordinateFromPixel([mapSize[0], mapSize[1]]),
this.map.getCoordinateFromPixel([0, mapSize[1]]),
];
var flip = this.getFlip();
var maxZoom = this.imageInfo.MaxZoomLevel;
var pow = Math.pow(2, maxZoom);
var xPadding = 0,
yPadding = 0;
var boxSize = pow * this.imageInfo.TileSize;
if (flip.horizontally === true) {
xPadding = boxSize - this.imageInfo.Width;
}
if (flip.vertically !== true) {
yPadding = boxSize - this.imageInfo.Height;
}
for (var i = 0; i < 4; i++) {
coords[i][0] -= xPadding;
coords[i][1] -= yPadding;
if (flip.horizontally === true) {
coords[i][0] = this.imageInfo.Width - coords[i][0];
}
if (flip.vertically !== true) {
coords[i][1] = this.imageInfo.Height - coords[i][1];
}
}
return coords;
}
/**
* Generates the parameters required for generating a snapshot url using getSnapshotUrl
* @returns {PMA.UI.Components~snapshotParameters}
*/
getSnapshotParameters() {
var coords = this.getViewportCoordinates();
var size = this.map.getSize();
var scale = 1 / this.map.getView().getResolution();
var flip = this.getFlip();
var rotation =
((-this.map.getView().getRotation() * 180.0) / Math.PI) % 360;
if (flip.horizontally ^ flip.vertically) {
rotation *= -1;
}
var x = Math.round(coords[0][0]);
var y = Math.round(coords[0][1]);
var w = Math.round(size[0] / scale);
var h = Math.round(size[1] / scale);
return {
x: x,
y: y,
width: w,
height: h,
rotation: rotation,
channels: this.getActiveChannels(),
layer: this.selectedLayer,
timeframe: this.selectedTimeFrame,
flipHorizontally: flip.horizontally,
flipVertically: flip.vertically,
};
}
/**
* An object containing the url, width and height of the final image
* @typedef {Object} PMA.UI.View~SnapshotResult
* @property {url} url - The snapshot url
* @property {Number} width - The width of the snapshot
* @property {boolean} height - The height of the snapshot
*/
/**
* Generates a URL where a snapshot of the current viewport is rendered
* @param {boolean} download - True to prompt the user to save the snapshot instead of viewing it within the browser
* @param {PMA.UI.View.ObjectiveResolutions} [objectiveResolution] - The desired objective resolution. If not passed the viewport scale will be used
* @param {string} [format=jpg] - The snapshot image format
* @param {boolean} [drawScaleBar=false] - Whether or not to draw a scalebar
* @param {number} [dpi=null] - Dots per inch to set to the image
* @param {string} [filename=snapshot] - The filename of the image
* @param {ol.Feature} [feature=null] - Clip the snapshot to the bounding box of this annotation
* @param {number} [scale=null] - A factor to scale the image by. Default is the current viewport scale
* @returns {PMA.UI.View~SnapshotResult} - An object containing the url, width and height of the final image
*/
getSnapshotUrl(download, objectiveResolution, format, drawScaleBar, dpi = null, filename = "snapshot", feature = null, scale = null) {
if (!format) {
format = "jpg";
}
if (filename === null) {
filename = "snapshot";
}
drawScaleBar = !!drawScaleBar;
scale = scale ? scale : 1 / this.map.getView().getResolution();
var coords = this.getViewportCoordinates();
var size = this.map.getSize().map(s => s / scale);
var flip = this.getFlip();
if (feature) {
let projection = this.mainLayer.getSource().getProjection();
var projExtent = projection.getExtent();
var extentObj = { extent: projExtent, flip: flip };
var geom = feature.getGeometry().clone();
geom.applyTransform(annotationTransform.bind(extentObj));
let vp = this.map.getView().getProjection().getExtent();
let extent = geom.getExtent();
let x = flip.horizontally ? extent[2] + vp[0] : extent[0];
let y = extent[flip.vertically ? 3 : 1];
coords = [[x, y], [extent[2], extent[3]]];
size = [Math.abs(extent[2] - extent[0]), Math.abs(extent[3] - extent[1])];
}
if (objectiveResolution) {
if (objectiveResolution == ObjectiveResolutions.Min) {
scale = 1 / Math.pow(2, this.imageInfo.MaxZoomLevel);
} else if (objectiveResolution == ObjectiveResolutions.Max) {
if (this.options.digitalZoomLevels > 0) {
scale = 2;
} else {
scale = 1;
}
} else if (objectiveResolution == ObjectiveResolutions["1:1"]) {
scale = 1;
} else {
var obji = parseInt(objectiveResolution);
if (obji >= 1) {
var leveli = -Math.log2(10) + Math.log2(obji * this.imageInfo.MicrometresPerPixelX) + this.imageInfo.MaxZoomLevel;
scale = 1 / Math.pow(2, this.imageInfo.MaxZoomLevel - leveli);
}
}
}
// if no flip is set, or both horizontal and vertical flipping is set
// we need the negative rotation angle,
// otherwise, if one fo the two flips is set but not the other, we must not negate the angle
var rotation = ((-this.map.getView().getRotation() * 180.0) / Math.PI) % 360;
if (flip.horizontally ^ flip.vertically) {
rotation *= -1;
}
if (feature) {
rotation = 0;
}
var x = Math.round(coords[0][0]);
var y = Math.round(coords[0][1]);
var w = Math.round(size[0]);
var h = Math.round(size[1]);
var url =
this.imagesUrl +
"region?pathOrUid=" +
encodeURIComponent(this.image) +
"&format=" +
encodeURIComponent(format) +
"&timeframe=" +
this.selectedTimeFrame +
"&layer=" +
this.selectedLayer +
"&channels=" +
this.channelsString +
"&channelClipping=" +
this.channelClippingString +
"&channelColor=" +
this.channelColorString +
"&gamma=" +
this.channelGammaString +
"&sessionID=" +
encodeURIComponent(this.sessionID) +
"&drawScaleBar=" +
drawScaleBar +
"&x=" +
x +
"&y=" +
y +
"&width=" +
w +
"&height=" +
h +
"&scale=" +
scale +
"&rotation=" +
rotation +
"&dpi=" +
dpi +
"&filename=" +
filename +
"&flipHorizontal=" +
flip.horizontally +
"&flipVertical=" +
flip.vertically +
(this.imageAdjustments.gamma && this.imageAdjustments.gamma != 1.0 ? "&postGamma=" + this.imageAdjustments.gamma.toFixed(2) : "") +
(this.imageAdjustments.brightness && this.imageAdjustments.brightness != 0 ? "&brightness=" + this.imageAdjustments.brightness : "") +
(this.imageAdjustments.contrast && this.imageAdjustments.contrast != 1.0 ? "&contrast=" + this.imageAdjustments.contrast.toFixed(2) : "");
if (download) {
url += "&downloadInsteadOfDisplay=true";
}
return {
url: url,
width: Math.round(w * scale),
height: Math.round(h * scale),
scale: scale,
};
}
drawViewport() {
var coords = [];
var size = this.map.getSize();
coords.push(this.map.getCoordinateFromPixel([0, 0]));
coords.push(this.map.getCoordinateFromPixel([0, size[1]]));
coords.push(this.map.getCoordinateFromPixel([size[0], size[1]]));
coords.push(this.map.getCoordinateFromPixel([size[0], 0]));
var poly = new ol.geom.Polygon([coords]);
var feat = new ol.Feature(poly);
var s = this.measureLayer.getSource();
s.clear();
s.addFeature(feat);
}
/**
* Generates an image with a snapshot of the current viewport and saves it if format is "png" or "jpg"
* @param {string} [format=png] - The snapshot image format, one of "png", "jpg", "blob"
* @param {Object} [extraParams] - Extra parameters to draw the snapshot
* @param {string} [extraParams.title] - A title to draw on top middle of the snapshot
* @param {boolean} [extraParams.scalebar=true] - Whether to draw a scalebar of the slide
* @param {boolean} [extraParams.scalebarLocation="TopLeft"] - When the scalebar will be positioned, one of "TopLeft","TopRight", "BottomLeft", "BottomRight"
* @param {boolean} [extraParams.scalebarFont="24px serif"] - A valid css font property to draw e.g. "24px serif"
* @param {boolean} [extraParams.barcode=false] - Whether to draw the barcode of the slide
* @param {boolean} [extraParams.overview=false] - Whether to draw an overview of the slide
* @param {number} [extraParams.dpi=null] - Set the dpi of the output image
* @param {string} [extraParams.filename=snapshot] - The filename of the image
* @param {string} [extraParams.action="download"] - Action that will take place after snapshot created, one of "download","open-tab"
* @returns {Promise<Blob>} A promise to the blob object
*/
getSnapshot(format, extraParams) {
if (!format) {
format = "png";
}
var that = this;
return new Promise((resolve, reject) => {
that.map.once('rendercomplete', function (ev) {
try {
var mapCanvas = document.createElement('canvas');
var size = ev.target.getSize();
mapCanvas.width = size[0];
mapCanvas.height = size[1];
var mapContext = mapCanvas.getContext('2d');
var scale = 1 / that.map.getView().getResolution();
let canvasDraw = async function () {
let mainCanvases = Array.from(that.map.getTargetElement().querySelectorAll('.ol-layer.main-layer canvas'));
let mainCanvas = mainCanvases[mainCanvases.length - 1];
let canvasList = Array.from(that.map.getTargetElement().querySelectorAll('.ol-layer.annotations-layer canvas'));
canvasList = [mainCanvas, ...canvasList];
let firstCanvas = canvasList[0];
if (canvasList.length === 0) {
return;
}
if (firstCanvas.width > 0) {
var opacity = firstCanvas.parentNode.style.opacity;
mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
let bgColor = "#000000";
if (that.imageInfo && that.imageInfo.BackgroundColor) {
bgColor = "#" + that.imageInfo.BackgroundColor;
}
mapContext.fillStyle = bgColor;
mapContext.fillRect(0, 0, mapContext.canvas.width, mapContext.canvas.height);
for (let i = 0; i < canvasList.length; i++) {
if (canvasList[i].width > 0) {
var transform = canvasList[i].style.transform;
// Get the transform parameters from the style's transform matrix
var matrix = transform
.match(/^matrix\(([^\(]*)\)$/)[1]
.split(',')
.map(Number);
// Apply the transform to the export map context
CanvasRenderingContext2D.prototype.setTransform.apply(
mapContext,
matrix
);
mapContext.drawImage(canvasList[i], 0, 0);
mapContext.resetTransform();
}
}
if (!extraParams || extraParams.scalebar !== false) {
drawScalebar(mapContext, that.imageInfo, scale,
extraParams ? extraParams.scalebarLocation : null,
extraParams ? extraParams.scalebarFont : null);
}
if (extraParams && extraParams.title) {
drawTitle(mapContext, extraParams.title);
}
if (extraParams && extraParams.barcode) {
let rotation = 0;
if (that.barcodeControl && that.barcodeControl.rotation) {
rotation = that.barcodeControl.rotation;
}
await drawBarcode(mapContext, that, rotation);
}
if (extraParams && extraParams.overview) {
await drawOverview(mapContext, that);
}
}
}
canvasDraw().then(function () {
let filename = "snapshot";
if (extraParams && extraParams.filename) {
filename = extraParams.filename;
}
if (navigator.msSaveBlob) {
var blob = mapCanvas.msToBlob();
if (format == "jpg") {
console.warn("Cannot save jpg snapshot on internet explorer/edge. The resulting snapshot is in png format");
}
if (format == "blob") {
resolve(blob);
return;
}
let fn = function (blb) {
if (extraParams && extraParams.action === "open-tab") {
var a = new FileReader();
a.onload = function (e) {
var tab = window.open(e.target.result, '_blank');
if (tab) tab.focus();
}
a.readAsDataURL(blb);
return;
}
navigator.msSaveBlob(blb, filename + ".png");
};
if (extraParams && extraParams.dpi) {
changeDpiBlob(blob, extraParams.dpi).then(function (bl) {
fn(bl);
resolve(bl);
});
return;
}
fn(blob);
resolve(blob);
} else {
mapCanvas.toBlob(
function (blob) {
if (format == "blob") {
resolve(blob);
return;
}
let fn = function (blb) {
if (extraParams && extraParams.action === "open-tab") {
var fileURL = URL.createObjectURL(blb);
var tab = window.open(fileURL, '_blank');
if (tab) tab.focus();
return;
}
saveAs(blb, filename + "." + (format == "jpg" ? "jpg" : "png"));
};
if (extraParams && extraParams.dpi) {
changeDpiBlob(blob, extraParams.dpi).then(function (bl) {
fn(bl);
resolve(bl);
});
return;
}
fn(blob);
resolve(blob);
},
format == "jpg" ? "image/jpeg" : "image/png"
);
}
});
} catch (ex) {
reject(Error("Error creating snapshot"));
}
});
that.map.renderSync();
});
}
/**
* Attaches an event listener to the viewer
* @param {PMA.UI.View.Events} eventName - The name of the event to listen to
* @param {function} callback - The function to call when the event occurs
*/
listen(eventName, callback) {
if (!this.listeners.hasOwnProperty(eventName)) {
console.error(eventName + " is not a valid event");
}
this.listeners[eventName].push(callback);
}
/**
* Detaches an event listener from the viewer
* @param {PMA.UI.View.Events} eventName - The name of the event to remove the callback from
* @param {function} callback - The function to remove from the event listener list
* @returns {bool} Whether or not the callback was actually found and removed
*/
unlisten(eventName, callback) {
if (!this.listeners.hasOwnProperty(eventName)) {
console.error(eventName + " is not a valid event");
}
var index = this.listeners[eventName].indexOf(callback);
if (index >= 0 && index < this.listeners[eventName].length) {
this.listeners[eventName].splice(index, 1);
return true;
}
return false;
}
fireEvent(eventName, eventArgs) {
if (!this.listeners.hasOwnProperty(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);
}
}
/**
* Pans and zooms the viewer appropriately to focus around an annotation
* @param {number} annotationId - The id of the annotation to which the viewport should focus
*/
focusToAnnotation(annotationId) {
var feature = this.annotationsLayer
.getSource()
.getFeatureById(annotationId);
if (feature) {
var pan = {
duration: 500,
center: this.map.getView().getCenter(),
};
var sz = this.map.getSize();
var extent = feature.getGeometry().getExtent();
var area = Math.abs(
(extent[2] - extent[0]) * (extent[3] - extent[1])
);
if (area < 0.0001) {
var factor = 32;
var worldExt = this.map.getView().getProjection().getExtent();
extent[0] -= worldExt[2] / factor;
extent[1] -= worldExt[3] / factor;
extent[2] += worldExt[2] / factor;
extent[3] += worldExt[3] / factor;
}
this.map.getView().animate(pan);
this.map.getView().fit(extent, sz);
}
}
/**
* Sets the brightness of the viewer
* @param {number} brightness
*/
setRenderingOptions() {
saveRenderingOptions.call(this);
}
/**
* Gets the current brightness value of the viewer
* @returns {number}
*/
getRenderingOptions() {
return getRenderingOptions.call(this);
}
/**
* Sets the brightness of the viewer
* @param {number} brightness
*/
setBrightness(brightness) {
this.imageAdjustments.brightness = brightness;
applyImageAdjustments.call(this);
}
/**
* Sets the contrast of the viewer
* @param {number} contrast
*/
setContrast(contrast) {
this.imageAdjustments.contrast = contrast;
applyImageAdjustments.call(this);
}
/**
* Sets the brightness and contrast of the viewer
* @param {number} brightness
* @param {number} contrast
* @param {number} gamma
*/
setBrightnessContrastGamma(brightness, contrast, gamma) {
this.imageAdjustments.brightness = brightness;
this.imageAdjustments.contrast = contrast;
this.imageAdjustments.gamma = gamma;
applyImageAdjustments.call(this);
}
/**
* Sets the gamma of the viewer
* @param {number} gamma
*/
setGamma(gamma) {
this.imageAdjustments.gamma = gamma;
applyImageAdjustments.call(this);
}
/**
* Sets the color balance for each rgb channel
* @param {number} red A value between 0 and 2 indicating the red channel balance
* @param {number} green A value between 0 and 2 indicating the green channel balance
* @param {number} blue A value between 0 and 2 indicating the blue channel balance
*/
setColorBalance(red, green, blue) {
this.imageAdjustments.rgb = [red, green, blue];
applyImageAdjustments.call(this);
}
/**
* Gets the color balance values
* @returns An array containing the color balance values for [red, green, blue]
*/
getColorBalance() {
return this.imageAdjustments.rgb;
}
/**
* Adding tile transformer to the list of transformers
* @param {PMA.UI.View.tileTransformer} transformer function that gets pixels as an input parameter and returns nothing, it should adjust pixels values directly in array from parameters
*/
addTileTransformer(transformer) {
this.imageAdjustments.tileTransformers =
this.imageAdjustments.tileTransformers || [];
this.imageAdjustments.tileTransformers.push(transformer);
}
/**
* Setting list of tile transformers
* @param {PMA.UI.View.tileTransformer[]} transformers list of transformer function that gets pixels as an input parameter and returns nothing, it should adjust pixels values directly in array from parameters
*/
setTileTransformers(transformers) {
this.imageAdjustments.tileTransformers = transformers;
}
/**
* Gets the current brightness value of the viewer
* @returns {number}
*/
getBrightness() {
return this.imageAdjustments.brightness;
}
/**
* Gets the current gamma value of the viewer
* @returns {number}
*/
getGamma() {
return this.imageAdjustments.gamma;
}
/**
* Gets the current contrast value of the viewer
* @returns {number}
*/
getContrast() {
return this.imageAdjustments.contrast;
}
/**
* Gets the current list of tile transformers
* @returns {Array.<PMA.UI.View.tileTransformer>}
*/
getTileTransformers() {
return (this.imageAdjustments.tileTransformers || []).slice();
}
/**
* Gets the URL of the PMA.core server currently connected to
* @returns {string}
*/
getActiveServerUrl() {
return this.imagesUrl;
}
/**
* Forces the viewer to recalculate the viewport size
*/
updateSize() {
if (this.map) {
this.map.updateSize();
var controls = this.map.getControls();
for (var i = 0; i < controls.getLength(); i++) {
var cntrl = controls.item(i);
if (cntrl instanceof Overview && cntrl.overviewMap) {
cntrl.overviewMap.updateSize();
}
}
printObjectivesInZoomBar.call(this);
setMapSizeClass.call(this);
}
}
/**
* Converts an array of pma.core annotations to open layers features
* @param {Viewport~annotation[]} annotations - An annotation array as returned by pma.core
* @param {Object} - The projection used for diplay
*/
initializeFeatures(annotations, projection) {
var format = new ol.format.WKT();
var extent = projection.getExtent();
var extentObj = { extent: extent, flip: this.getFlip() };
var features = [];
for (var i = 0; i < annotations.length; i++) {
var annot = annotations[i];
let f = format.readFeature(annot.Geometry);
if (
annot.AnnotationID === null ||
annot.AnnotationID === undefined
) {
f.setId((Math.random() * 10000) | 0);
} else {
f.setId(annot.AnnotationID);
}
var geom = f.getGeometry();
geom.applyTransform(annotationTransform.bind(extentObj));
var dimensions = isNaN(annot.Dimensions) ? 2 : annot.Dimensions;
var area = this.calculateArea(geom);
var frmtArea = this.formatArea(area);
var length = this.calculateLength(geom);
var frmtLength = this.formatLength(length);
var updateDate = null;
if (annot.AuditLastModifiedOn) {
var updateMatches = /\/Date\(((\d*)([\+\-]?\d*))\)\//.exec(annot.AuditLastModifiedOn);
if (updateMatches && updateMatches.length > 2) {
updateDate = new Date(parseInt(updateMatches[2]));
}
}
var createDate = null;
if (annot.AuditCreatedOn) {
var createMatches = /\/Date\(((\d*)([\+\-]?\d*))\)\//.exec(annot.AuditCreatedOn);
if (createMatches && createMatches.length > 2) {
createDate = new Date(parseInt(createMatches[2]));
}
} else {
createDate = new Date();
}
var fillColor = annot.FillColor ?
annot.FillColor :
DefaultFillColor;
var strokeColor = annot.Color ? annot.Color : "#3399CC";
var imageStyle = null;
f.name = annot.Notes;
f.metaData = {
AnnotationID: annot.AnnotationID,
Classification: annot.Classification,
Color: strokeColor,
CreatedOn: createDate,
CreatedBy: annot.AuditCreatedBy,
Image: annot.Image,
LayerID: annot.LayerID,
Notes: annot.Notes,
UpdateInfo: annot.UpdateInfo,
UpdatedOn: updateDate,
UpdatedBy: annot.AuditLastModifiedBy,
Geometry: annot.Geometry,
Area: dimensions > 1 ? area : 0,
FormattedArea: dimensions > 1 ? frmtArea : "",
Length: dimensions > 0 ? length : 0,
FormattedLength: dimensions > 0 ? frmtLength : "",
FillColor: fillColor,
Dimensions: dimensions,
State: AnnotationState.Pristine,
LineThickness: annot.LineThickness ? annot.LineThickness : 2,
};
// geom.on('change', function (feature, e) {
// feature.metaData.State = AnnotationState.Modified;
// }.bind(this, f));
// f.on('change:geometry', function (e) {
// e.target.metaData.State = AnnotationState.Modified;
// });
if (geom.getType() === "MultiPoint") {
f.metaData.PointCount = geom.getCoordinates().length;
}
var fill = new ol.style.Fill({
color: fillColor,
});
var stroke = new ol.style.Stroke({
color: strokeColor,
width: f.metaData.LineThickness ? f.metaData.LineThickness : 2,
});
if (annot.Geometry.indexOf("POINT") !== -1) {
if (!strokeColor.startsWith("#") && !strokeColor.startsWith("rgb")) {
// it's a point and it's color doesn't start with #, so it's probably an icon - a hack convention in PMA.view 1.x
imageStyle = new ol.style.Icon({
anchor: [0.5, 0.5],
anchorXUnits: "fraction",
anchorYUnits: "fraction",
opacity: 1,
src: this.options.annotations.imageBaseUrl + strokeColor,
scale: isNaN(this.options.annotations.imageScale) ?
1 : this.options.annotations.imageScale,
});
}
else {
imageStyle = new ol.style.Circle({
fill: fill,
stroke: stroke,
radius: 5,
});
}
}
var currentAnnotationStyle = new ol.style.Style({
image: imageStyle,
fill: fill,
stroke: stroke,
text: this.getAnnotationTextStyle(f, strokeColor),
});
f.setStyle(this.getAnnotationStyle(currentAnnotationStyle, f));
f.originalStyle = currentAnnotationStyle;
features.push(f);
}
return features;
}
/**
* Returns the supported annotation layer names for the loaded slide
*/
getAnnotationsLayersNames() {
if (!this.imageInfo || !this.imageInfo.AnnotationsLayers) {
return [];
}
return this.imageInfo.AnnotationsLayers;
}
/**
* Sets a new configuration for the controls of the viewport
* @param {Viewport~ControlConfiguration[]} configuration - The control configuration object
*/
setControlsConfiguration(configuration) {
if (!Array.isArray(configuration)) {
configuration = [configuration];
}
setControlsConfiguration.call(this, configuration);
}
/**
* Gets the current configuration of the controls of the viewport
* @returns {Viewport~ControlConfiguration[]} configuration - The control configuration object
*/
getControlsConfiguration() {
return getControlsConfiguration.call(this);
}
/**
* Hides the measurement grid if visible or does nothing
*/
hideGrid() {
if (this.gridLayer) {
this.gridLayer.getSource().clear();
}
}
/**
* Shows a measurement grid
* @param {Array<number>} size - The size of the grid cells in micrometers
*/
showGrid(size) {
if (!size || size.length < 2 || size[0] <= 0 || size[1] <= 0) {
throw "Invalid size specified";
}
this.options.grid = { size: size };
this.gridLayer.getSource().clear();
var sizeX = size[0] / this.imageInfo.MicrometresPerPixelX;
var sizeY = size[1] / this.imageInfo.MicrometresPerPixelY;
var proj = this.map.getView().getProjection();
var extent = proj.getExtent();
var startX = Math.min(extent[0], extent[2]);
var endX = Math.max(extent[0], extent[2]);
var startY = Math.min(extent[1], extent[3]);
var endY = Math.max(extent[1], extent[3]);
var xSteps = Math.ceil((endX - startX) / sizeX);
var ySteps = Math.ceil((endY - startY) / sizeY);
endX = startX + xSteps * sizeX;
startY = endY - ySteps * sizeY;
for (var x = 0; x <= xSteps; x++) {
this.gridLayer.getSource().addFeature(
new ol.Feature({
geometry: new ol.geom.LineString([
[startX + x * sizeX, startY],
[startX + x * sizeX, endY],
]),
})
);
}
for (var y = 0; y <= ySteps; y++) {
this.gridLayer.getSource().addFeature(
new ol.Feature({
geometry: new ol.geom.LineString([
[startX, endY - y * sizeY],
[endX, endY - y * sizeY],
]),
})
);
}
}
/**
* Reloads annotations from the server
* @param {function} [readyCallback] - Called when the annotations have finished loading
* @param {string} [fingerprint] - If exists, will reload annotations by fingerprint
*/
reloadAnnotations(readyCallback, fingerprint) {
if (!this.annotationsLayer ||
!this.mainLayer ||
!this.sessionID ||
!this.image
) {
console.error("Reload annotations: No image loaded in viewer");
if (typeof readyCallback === "function") {
readyCallback.call(this);
}
return;
}
var vectorSource = this.annotationsLayer.getSource();
vectorSource.clear();
if (this.hiddenAnnotations) {
this.hiddenAnnotations = [];
}
if (this.hiddenAnnotationsByFingerprint) {
this.hiddenAnnotationsByFingerprint = [];
}
var pixelProjection = this.mainLayer.getSource().getProjection();
getAnnotationsServer.call(this, vectorSource, pixelProjection, fingerprint, this.options.annotations.filter, readyCallback);
}
rotateExtent(extent, rotation) {
var geom = ol.geom.Polygon.fromExtent(extent);
var center = ol.extent.getCenter(extent);
geom.rotate(rotation, center);
return geom.getExtent();
}
/**
* Returns the current objective magnification
* @returns {string} - A string representing the current objective magnification ie. "1X", "2X" etc
*/
getCurrentObjective() {
if (!this.imageInfo || this.imageInfo.MicrometresPerPixelX == 0) {
return "";
}
if (!this.map) {
console.warn("PMA.UI.Viewport map is not initialized");
return "";
}
var objective = 10 / this.imageInfo.MicrometresPerPixelX / this.map.getView().getResolution();
if (objective > 1) {
objective |= 0;
return objective + "X";
}
}
/**
* Returns the max objective magnification
* @returns {string} - A string representing the max objective magnification ie. "1X", "2X" etc
*/
getMaxObjective() {
var objective = 10 / this.imageInfo.MicrometresPerPixelX;
if (objective > 1) {
objective |= 0;
return findClosestObjectiveValue(objective) + "X";
}
}
/**
* Gets the visibility of the third party annotation layers
* @returns {Object[]} options - An array of options
* @returns {string} options.name - The name of the layer to change the visibility
* @returns {boolean} options.visible - Whether to show or hide the specified layer
*/
getAnnotationLayersVisibility() {
if (!this.layerSwitcher) {
return [];
}
return this.layerSwitcher.getLayersVisibility();
}
/**
* Sets the visibility of the third party annotation layers
* @param {Object[]} options - An array of options
* @param {string} options.name - The name of the layer to change the visibility
* @param {boolean} options.visible - Whether to show or hide the specified layer
* @param {number} [options.opacity] - A number between 0 and 1 that sets the opacity of the layer
*/
setAnnotationLayersVisibility(options) {
if (!this.layerSwitcher) {
return;
}
this.layerSwitcher.setLayersVisibility(options);
}
/**
* Gets the wheel zoom mode to one of "Normal", "Objectives". The normal mode zooms through all the available resolution,
* while the "Objectives" mode zoom only on the round objective resolutions 1X,2X,5X,10X, 20X, 40X, 80X, 160X
* @returns {MouseWheelZoomMode} mode
*/
getMouseWheelZoomMode() {
if (!this.mouseWheelInteraction) {
return MouseWheelMode.Normal;
}
return this.mouseWheelInteraction.mode;
}
/**
* Sets the wheel zoom mode to one of "Normal", "Objectives". The normal mode zooms through all the available resolutions,
* while the "Objectives" mode zooms only to the rounded objective resolutions 1X, 2X, 5X, 10X, 20X, 40X, 80X and 160X
* @param {MouseWheelZoomMode} mode
*/
setMouseWheelZoomMode(mode) {
if (!this.mouseWheelInteraction) {
return;
}
this.mouseWheelInteraction.setMode(mode);
}
/**
* Start the realtime measuring tool
* @param {"area" | "line"} type - Whether to start measuring length or area
*/
startMeasuring(type) {
startMeasuring.call(this, type);
}
/**
* Stops and clears all realtime measurements
*/
stopMeasuring() {
stopMeasuring.call(this, true);
}
}