import { checkBrowserCompatibility } from '../../view/helpers';
import { Viewport } from '../../view/viewport';
import { Events, DragDropMimeType, parseDragData, _sessionList } from './components';
function clone(obj) {
if (null === obj || "object" != typeof obj) {
return obj;
}
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = obj[attr];
}
}
return copy;
}
function checkReloadImage(serverUrl, path, doneCb, dropped) {
if (!this.lastLoadImageRequest) {
return;
}
if (this.lastLoadImageRequest.serverUrl !== serverUrl || this.lastLoadImageRequest.path !== path || this.lastLoadImageRequest.doneCb !== doneCb) {
var ref = this.lastLoadImageRequest;
this.lastLoadImageRequest = null;
this.load(ref.serverUrl, ref.path, ref.doneCb, ref.dropped);
}
}
function onDragOver(ev) {
ev.preventDefault();
var types = ev.dataTransfer.types;
if (types) {
var hasData = false;
if (types.indexOf) {
hasData = types.indexOf("application/x-fancytree-node") > -1 || types.indexOf(DragDropMimeType) > -1;
}
else if (types.contains) {
// in IE and EDGE types is DOMStringList
hasData = types.contains("application/x-fancytree-node") || types.contains(DragDropMimeType);
}
if (hasData) {
ev.dataTransfer.dropEffect = "link";
return;
}
}
ev.dataTransfer.dropEffect = "none";
}
function onDrop(ev) {
ev.preventDefault();
var nodeData = parseDragData(ev.dataTransfer);
if (nodeData && !nodeData.isFolder && nodeData.path && nodeData.serverUrl) {
var cancels = this.fireEvent(Events.BeforeDrop, { serverUrl: nodeData.serverUrl, path: nodeData.path, node: nodeData });
if (cancels.filter(function (c) { return c == false; }).length == 0) {
this.load(nodeData.serverUrl, nodeData.path, null, true);
}
}
}
function initializeDropZone() {
if (this.element) {
this.element.addEventListener("drop", onDrop.bind(this), false);
this.element.addEventListener("dragover", onDragOver.bind(this), false);
}
}
/**
* Helper class that wraps around the {@link PMA.UI.View.Viewport} class. It's purpose is mainly to automatically handle slide reloading and authentication, via the provided {@link PMA.UI.Components.Context} instance.
* @param {PMA.UI.Components.Context} context
* @param {Object} slideLoaderOptions - Initialization options passed to each {@link PMA.UI.View.Viewport} that is created during a {@link PMA.UI.Components.SlideLoader#load} call. This is the same struct as the one accepted by the {@link PMA.UI.View.Viewport} constructor, omitting server URLs, credentials and specific slide paths. The omitted information is either available via the {@link PMA.UI.Components.Context} instance, or supplied during the {@link PMA.UI.Components.SlideLoader#load} call.
* @param {string|HTMLElement} slideLoaderOptions.element - The element that hosts the viewer. It can be either a valid CSS selector or an HTMLElement instance
* @param {string} slideLoaderOptions.image - The path or UID of the image to load
* @param {Number} [slideLoaderOptions.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 {PMA.UI.View.Themes} [slideLoaderOptions.theme="default"] - The theme to use
* @param {boolean|Object} [slideLoaderOptions.overview=true] - Whether or not to display an overview map
* @param {boolean} [slideLoaderOptions.overview.collapsed] - Whether or not to start the overview in collapsed state
* @param {boolean|Object} [slideLoaderOptions.dimensions=true] - Whether or not to display the dimensions selector for images that have more than one channel, z-stack or timeframe
* @param {boolean} [slideLoaderOptions.dimensions.collapsed] - Whether or not to start the dimensions selector in collapsed state
* @param {boolean|Object} [slideLoaderOptions.barcode=false] - Whether or not to display the image's barcode if it exists
* @param {boolean} [slideLoaderOptions.barcode.collapsed=undefined] - Whether or not to start the barcode in collapsed state
* @param {Number} [slideLoaderOptions.barcode.rotation=undefined] - Rotation in steps of 90 degrees
* @param {boolean|Object} [slideLoaderOptions.loadingBar=true] - Whether or not to display a loading bar while the image is loading
* @param {PMA.UI.View.Viewport~position} [slideLoaderOptions.position] - The initial position of the viewport within the image
* @param {boolean} [slideLoaderOptions.snapshot=false] - Whether or not to display a button that generates a snapshot image
* @param {PMA.UI.View.Viewport~annotationOptions} [slideLoaderOptions.annotations] - Annotation options
* @param {Number} [slideLoaderOptions.digitalZoomLevels=0] - The number of digital zoom levels to add
* @param {boolean} [slideLoaderOptions.scaleLine=true] - Whether or not to display a scale line when resolution information is available
* @param {boolean} [slideLoaderOptions.colorAdjustments=false] - Whether or not to add a control that allows color adjustments
* @param {string|PMA.UI.View.Viewport~filenameCallback} [slideLoaderOptions.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|PMA.UI.View.Viewport~attributionOptions}[slideLoaderOptions.attributions=undefined] - Whether or not to display Pathomation attribution in the viewer
* @param {Array<PMA.UI.View.Viewport~customButton>} [slideLoaderOptions.customButtons] - An array of one or more custom buttons to add to the viewer
* @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
* @fires PMA.UI.Components.Events#SlideInfoError
* @fires PMA.UI.Components.Events#BeforeSlideLoad
* @fires PMA.UI.Components.Events#SlideLoaded
* @fires PMA.UI.Components.Events#BeforeDrop
* @tutorial 03-gallery
* @tutorial 04-tree
* @tutorial 05-annotations
*/
export class SlideLoader {
constructor(context, slideLoaderOptions) {
if (!checkBrowserCompatibility()) {
return;
}
this.loadingImage = false;
this.lastLoadImageRequest = null;
this.context = context;
this.slideLoaderOptions = slideLoaderOptions || {};
this.listeners = {};
this.listeners[Events.SlideInfoError] = [];
this.listeners[Events.BeforeSlideLoad] = [];
this.listeners[Events.SlideLoaded] = [];
this.listeners[Events.BeforeDrop] = [];
/**
* The currently loaded {@link PMA.UI.View.Viewport} instance, or null
* @public
*/
this.mainViewport = null;
// try to get the element
if (this.slideLoaderOptions.element) {
if (this.slideLoaderOptions.element instanceof HTMLElement) {
this.element = this.slideLoaderOptions.element;
}
else if (typeof this.slideLoaderOptions.element == "string") {
var el = document.querySelector(this.slideLoaderOptions.element);
if (!el) {
throw "Invalid selector for element";
}
else {
this.element = el;
}
}
}
initializeDropZone.call(this);
}
/**
* Sets or overrides the value of an option. Useful when it is required to modify a viewer option before loading as slide.
* @param {string} option
* @param {any} value
*/
setOption(option, value) {
this.slideLoaderOptions[option] = value;
}
/**
* Gets the value of a viewer option.
* @param {string} option
* @param {any} value
* @return {any} The value of the option or undefined
*/
getOption(option) {
return this.slideLoaderOptions[option];
}
/**
* Creates a {@link PMA.UI.View.Viewport} instance that loads the requested slide
* @param {string} serverUrl - PMA.core server URL
* @param {string} path - Path or UID of the slide load
* @param {function} [doneCb] - Called when the slide has finished loading
* @param {boolean} [dropped] - Whether this slide was loaded by a drag and drop operation
* @fires PMA.UI.Components.Events#BeforeSlideLoad
* @fires PMA.UI.Components.Events#SlideLoaded
* @fires PMA.UI.Components.Events#SlideInfoError
*/
load(serverUrl, path, doneCb, dropped) {
if (this.loadingImage === true) {
//console.error("SlideLoader.loadImage: Last load image call hasn't finished yet");
this.lastLoadImageRequest = {
serverUrl: serverUrl,
path: path,
doneCb: doneCb,
dropped: dropped === true ? true : false
};
return;
}
if (dropped !== true) {
dropped = false;
}
this.loadingImage = true;
if (this.mainViewport && this.mainViewport.map) {
while (this.mainViewport.map.getInteractions().getLength() > 0) {
this.mainViewport.map.removeInteraction(this.mainViewport.map.getInteractions().item(0));
}
while (this.mainViewport.map.getLayers().getLength() > 0) {
this.mainViewport.map.removeLayer(this.mainViewport.map.getLayers().item(0));
}
while (this.mainViewport.map.getControls().getLength() > 0) {
this.mainViewport.map.removeControl(this.mainViewport.map.getControls().item(0));
}
}
var beforeLoadEa = { serverUrl: serverUrl, path: path, cancel: false };
this.fireEvent(Events.BeforeSlideLoad, beforeLoadEa);
if (beforeLoadEa.cancel) {
this.loadingImage = false;
return;
}
var _this = this;
if (!serverUrl || !path) {
if (this.mainViewport) {
this.mainViewport.element.innerHTML = "";
this.mainViewport = null;
}
this.loadingImage = false;
this.fireEvent(Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });
if (typeof doneCb === "function") {
doneCb();
}
checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
return;
}
this.context.getSession(serverUrl, function (sessionId) {
var opts = clone(_this.slideLoaderOptions);
opts.serverUrls = [serverUrl];
opts.image = path;
opts.sessionID = sessionId;
opts.caller = _this.context.getCaller();
_this.mainViewport = new Viewport(opts, function () {
_this.loadingImage = false;
_this.fireEvent(Events.SlideLoaded, { serverUrl: serverUrl, path: path, dropped: dropped });
if (typeof doneCb === "function") {
doneCb();
}
checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
}, function () {
_this.loadingImage = false;
console.error("Error loading slide");
});
var count = 0;
_this.mainViewport.listen("tileserror", function () {
if (count === 0) {
count++;
_sessionList.set(serverUrl, null);
_this.context.getSession(serverUrl, function (newSessionId) {
count = -1;
_this.mainViewport.sessionID = newSessionId;
_this.mainViewport.redraw();
}, function () {
_this.fireEvent(Events.SlideInfoError, {});
});
}
else if (count === -1) {
count = 0;
_this.fireEvent(Events.SlideInfoError, {});
}
});
_this.mainViewport.listen("SlideLoadError", function () {
_this.loadingImage = false;
_this.fireEvent(Events.SlideInfoError, {});
checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
});
},
function () {
_this.loadingImage = false;
_this.fireEvent(Events.SlideInfoError, {});
checkReloadImage.call(_this, serverUrl, path, doneCb, dropped);
});
}
/**
* Gets the image info of the currently loaded image
* @return {object|null}
*/
getLoadedImageInfo() {
if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
return this.mainViewport.imageInfo;
}
return null;
}
/**
* Reloads annotations from the server
* @param {function} [readyCallback] - Called when the annotations have finished loading
*/
reloadAnnotations(readyCallback) {
if (this.mainViewport && this.mainViewport.map && this.mainViewport.imageInfo) {
this.mainViewport.reloadAnnotations(readyCallback);
return;
}
if (typeof readyCallback === "function") {
readyCallback.call(this);
}
}
/**
* 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
*/
listen(eventName, callback) {
if (!this.listeners.hasOwnProperty(eventName)) {
console.error(eventName + " is not a valid event");
}
this.listeners[eventName].push(callback);
}
// fires an event
fireEvent(eventName, eventArgs) {
if (!this.listeners.hasOwnProperty(eventName)) {
console.error(eventName + " does not exist");
return;
}
var returns = [];
for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
returns.push(this.listeners[eventName][i].call(this, eventArgs));
}
return returns;
}
/**
* Draw annotations for each slide in collage mode
* @param {string} serverUrl - The server used to load the images
* @param {function} slidePathTransform - A function to transform each filename to a slide name
*/
drawCollageAnnotations(serverUrl, slidePathTransform) {
let collagePath = this.mainViewport.image;
if (!collagePath.startsWith("{")) {
return;
}
let collageObj = JSON.parse(collagePath);
let sources = collageObj["Sources"];
let collectionRows = collageObj["CollectionRows"];
let tileMargin = collageObj["CollectionTileMargin"];
let layout = collageObj["CollectionLayout"];
if (!sources || sources.length == 0) {
return;
}
this.context.getImagesInfo({
serverUrl: serverUrl,
images: sources,
success: (sessionId, images) => {
let annotations = new PMA.UI.Components.Annotations({
context: this.context,
viewport: this.mainViewport,
serverUrl: serverUrl,
path: "",
enabled: true
});
let minresX = Math.min.apply(Math, images.map(im => im.MicrometresPerPixelX));
let minresY = Math.min.apply(Math, images.map(im => im.MicrometresPerPixelY));
let c = Math.round(images.length / collectionRows);
c = Math.max(1, c);
let sx = 0;
let sy = 0;
let maxHeight = 0;
for (let i = 0; i < images.length; i++) {
let mX = images[i].MicrometresPerPixelX / minresX;
let mY = images[i].MicrometresPerPixelY / minresY;
let w = Math.round(images[i].Width * mX);
let h = Math.round(images[i].Height * mY);
annotations.addAnnotation({
LayerID: 0,
Geometry: `POLYGON ((${sx} ${sy}, ${sx} ${sy + h}, ${sx + w} ${sy + h},${sx + w} ${sy}, ${sx} ${sy}))`,
Notes: slidePathTransform(images[i].Filename)
});
if (layout == 0) {
sx += w + tileMargin;
maxHeight = Math.max(maxHeight, h);
}
else {
sy += h + tileMargin;
maxHeight = Math.max(maxHeight, w);
}
if (((i + 1) % c) == 0) {
if (layout == 0) {
sx = 0;
sy += maxHeight + tileMargin;
maxHeight = 0;
}
else {
sy = 0;
sx += maxHeight + tileMargin;
maxHeight = 0;
}
}
}
}
});
}
}