/**
* PMA.UI.Components contains UI and utility components that interact with a viewport and PMA.core
* @namespace PMA.UI.Components
*/
import { Resources } from "../../resources/resources";
import { loginSupportsPost } from "../../view/version";
/**
* Events fired by components
* @memberof PMA.UI.Components
* @namespace PMA.UI.Components.Events
*/
/**
* Events fired by components
* @readonly
* @enum {string}
* @ignore
*/
export const Events = {
/**
* Fires when a directory has been selected by a PMA.UI.Components.Tree instance
* @event PMA.UI.Components.Events.DirectorySelected
* @param {Object} args
* @param {string} args.serverUrl
* @param {string} args.path
*/
DirectorySelected: "directorySelected",
/**
* Fires when a slide image has been selected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery or a PMA.UI.Components.MetadataTree instance
* @event PMA.UI.Components.Events.SlideSelected
* @param {Object} args
* @param {string} args.serverUrl - The server url this slide belongs to
* @param {string} args.path - The path to the slide
* @param {Number} [args.index] - The index of the slide selected in the gallery components (PMA.UI.Components.Gallery only)
* @param {boolean} [args.userInteraction] - Whether this event was fired by a user interaction or programmatically (PMA.UI.Components.Gallery only)
*/
SlideSelected: "slideSelected",
/**
* Fires when a server node has been selected by a PMA.UI.Components.Tree instance
* @event PMA.UI.Components.Events.ServerSelected
* @param {Object} args
* @param {string} args.serverUrl
*/
ServerSelected: "serverSelected",
/**
* Fires when a server node has been expanded by a PMA.UI.Components.Tree instance
* @event PMA.UI.Components.Events.ServerExpanded
* @param {Object} args
* @param {string} args.serverUrl
*/
ServerExpanded: "serverExpanded",
/**
* Fires when a folder node has been expanded by a PMA.UI.Components.Tree instance
* @event PMA.UI.Components.Events.DirectoryExpanded
* @param {Object} args
* @param {string} args.serverUrl
* @param {string} args.path
*/
DirectoryExpanded: "directoryExpanded",
/**
* Fires when slide images have been checked by a PMA.UI.Components.Tree
* @event PMA.UI.Components.Events.MultiSelectionChanged
* @param {Object[]} args
* @param {string} args.serverUrl
* @param {string} args.path
*/
MultiSelectionChanged: "multiSelectionChanged",
/**
* Fires when a slide image has been deselected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery instance
* @event PMA.UI.Components.Events.SlideDeSelected
*/
SlideDeSelected: "slideDeSelected",
/**
* Fires when a form has been saved to PMA.core
* @event PMA.UI.Components.Events.TreeNodeDoubleClicked
* @param {Object} args
* @param {string} args.serverUrl
* @param {string} args.path
* @param {boolean} isSlide
*/
TreeNodeDoubleClicked: "treeNodeDoubleClicked",
/**
* Fires when image info could not be loaded
* @event PMA.UI.Components.Events.SlideInfoError
*/
SlideInfoError: "SlideInfoError",
/**
* Fires before a viewport attempts to load an image
* @event PMA.UI.Components.Events.BeforeSlideLoad
*/
BeforeSlideLoad: "BeforeSlideLoad",
/**
* Fires after a viewport has finished loading an image
* @event PMA.UI.Components.Events.SlideLoaded
* @param {Object} args - The slide loaded arguments
* @param {string} args.serverUrl - The server the slide belongs to
* @param {string} args.path - The slide path
* @param {boolean} args.dropped - Whether the slide was loaded via a drag and drop operation
*/
SlideLoaded: "SlideLoaded",
/**
* Fires after when a new annotation has been added
* @event PMA.UI.Components.Events.AnnotationAdded
* @param {Object} args
* @param {Object} args.feature - The annotation object
*/
AnnotationAdded: "annotationAdded",
/**
* Fires when annotation drawing begins
* @event PMA.UI.Components.Events.AnnotationDrawing
*/
AnnotationDrawing: "annotationDrawing",
/**
* Fires when annotation drawing finishes
* @event PMA.UI.Components.Events.AnnotationDrawingFinished
* @param {Object} args
* @param {Object} args.cancelled - A boolean indicating if the drawing was cancelled or not
*/
AnnotationDrawingFinished: "annotationDrawingFinished",
/**
* Fires when an annotation has been deleted
* @event PMA.UI.Components.Events.AnnotationDeleted
* @param {Object} args
* @param {Number} args.annotationId
* @param {Object} args.feature
*/
AnnotationDeleted: "annotationDeleted",
/**
* Fires when an annotation has been modified
* @event PMA.UI.Components.Events.AnnotationModified
*/
AnnotationModified: "annotationModified",
/**
* Fires when the annotations have been saved to PMA.core
* @event PMA.UI.Components.Events.AnnotationsSaved
* @param {Object} args
* @param {boolean} args.success
*/
AnnotationsSaved: "annotationsSaved",
/**
* Fires when the currently selected annotation(s) have changed
* @event PMA.UI.Components.Events.AnnotationsSelectionChanged
* @param {Object[]} args - The selected annotation objects
*/
AnnotationsSelectionChanged: "annotationsSelectionChanged",
/**
* Fires when annotation editing begins
* @event PMA.UI.Components.Events.AnnotationEditingStarted
*/
AnnotationEditingStarted: "annotationEditingStarted",
/**
* Fires when annotation editing ends
* @event PMA.UI.Components.Events.AnnotationEditingEnded
*/
AnnotationEditingEnded: "annotationEditingEnded",
/**
* Fires when a form has been saved to PMA.core
* @event PMA.UI.Components.Events.FormSaved
* @param {Object} args - The result of the save operation
* @param {string} args.message - The error message if any
* @param {boolean} args.success - True if save was successful
*/
FormSaved: "formSaved",
/**
* Fires when a the edit button of a read only form is clicked
* @event PMA.UI.Components.Events.FormEditClick
*/
FormEditClick: "formEditClick",
/**
* Fires when the state of the synchronization of viewports has changed (enabled/disabled)
* @event PMA.UI.Components.Events.SyncChanged
* @param {boolean} enabled - Whether syncronization was enabled
*/
SyncChanged: "syncChanged",
/**
* Fires when a search has started
* @event PMA.UI.Components.Events.SearchStarted
*/
SearchStarted: "searchStarted",
/**
* Fires when a search has finished
* @event PMA.UI.Components.Events.SearchFinished
* @param {Object.<string, string[]>} results - The object containing the result for each server available
*/
SearchFinished: "searchFinished",
/**
* Fires when a search has failed
* @event PMA.UI.Components.Events.SearchFailed
* @param {Tree~server} server - The object containing the server failed to fetch search results for
*/
SearchFailed: "searchFailed",
/**
* Fires when a form value is expanded by a PMA.UI.Components.MetadataTree
* @event PMA.UI.Components.Events.ValueExpanded
* @param {Object} args - The arguments passed to this event
* @param {string} args.serverUrl - The server url that the form value belongs to
* @param {string[]} args.slides - An array of the paths for the slides belonging to the formvalue expanded
*/
ValueExpanded: "valueExpanded",
/**
* Fires when data are dropped on a PMA.UI.Components.Gallery
* @event PMA.UI.Components.Events.Dropped
* @param {Object} args - The arguments passed to this event
* @param {string} args.serverUrl - The server url
* @param {string} args.path - The path to a slide or directory dropped
* @param {boolean} args.isFolder - Whether a directory was dropped
* @param {boolean} args.append - Whether the slide/directory was appended or replaced(alt key was pressed)
*/
Dropped: "dropped",
/**
* Fires when a session id authentication fails
* @event PMA.UI.Components.Events.SessionIdLoginFailed
* @param {Object} args - The arguments passed to this event
* @param {string} args.serverUrl - The server url
*/
SessionIdLoginFailed: "sessionIdLoginFailed",
/**
* Fires when a slide is dropped in the {@link SlideLoader}
* @event PMA.UI.Components.Events.BeforeDrop
* @param {Object} args - The arguments passed to this event
* @param {string} args.serverUrl - The serverUrl
* @param {string} args.path - The path to the slide dropped
* @param {Object} args.node - The node metadata object
* @returns {boolean} - Whether to cancel the slide loading or not
*/
BeforeDrop: "beforeDrop",
/**
* Fires when there are changes in the {@link ImageUpload} component
* @event PMA.UI.Components.Events.OnProgress
* @param {Object} args - The arguments passed to this event
* @param {string} args.type - Type of OnProgress event
* @param {ImageUpload~Slide | null} args.slide - The slide this event is about or null
* @param {ImageUpload~Slide[]} args.slides - The list of processed slides
*/
OnProgress: "onProgress"
};
export
/**
* PMA.core API methods
* @readonly
* @enum {string}
* @memberof PMA.UI.Components
*/
const ApiMethods = {
Authenticate: "Authenticate",
GetFiles: "GetFiles",
GetDirectories: "GetDirectories",
GetImageInfo: "GetImageInfo",
GetImagesInfo: "GetImagesInfo",
DeAuthenticate: "DeAuthenticate",
GetForms: "GetForms",
GetFormDefinitions: "GetFormDefinitions",
GetFormSubmissions: "GetFormSubmissions",
GetForm: "GetForm",
SaveFormDefinition: "SaveFormDefinition",
DeleteFormDefinition: "DeleteFormDefinition",
SaveFormData: "SaveFormData",
GetFormData: "GetFormData",
GetAnnotations: "GetAnnotations",
AddAnnotation: "AddAnnotation",
UpdateAnnotation: "UpdateAnnotation",
DeleteAnnotation: "DeleteAnnotation",
GetVersionInfo: "GetVersionInfo",
QueryFilename: "Filename",
SaveAnnotations: "SaveAnnotations",
DistinctValues: "DistinctValues",
Metadata: "Metadata",
GetEvents: "GetEvents",
RunScripts: "Run",
CreateDirectory: "CreateDirectory",
DeleteDirectory: "DeleteDirectory",
RenameDirectory: "RenameDirectory",
DeleteSlide: "DeleteSlide",
Upload: "Upload"
};
export
/**
* PMA.core scopes for the GetSlides API method
* @readonly
* @memberof PMA.UI.Components
* @enum {string}
*/
const GetSlidesScope = {
Normal: 0,
OneLevel: 1,
Recursive: 2
};
export
/**
* Available components to render
* @readonly
* @enum {string}
* @memberof PMA.UI.Components
*/
const GalleryRenderOptions = {
/** Render both thumbnail and barcode*/
All: "all",
/** Render thumbnail only */
Thumbnail: "thumbnail",
/** Render barcode only */
Barcode: "barcode",
};
export
/**
* The mime type used for drag n drop data transfer
* @typedef {string} PMA.UI.Components~DragDropMimeType
*/
const DragDropMimeType = "application/x-pma-node";
/**
* An object expected for drag and drop features
* @typedef {Object} PMA.UI.Components~dragDropObject
* @property {string} serverUrl - The serverUrl
* @property {string} path - The path to a slide or directory
* @property {boolean} isFolder - Whether this is a path or a directory
*/
// static variables
//export let _sessionList = {};
export const _sessionList = {
_list: {},
set(key, value) { this._list[key] = value; },
get() { return this._list; },
clear() { this._list = {}; },
};
var alertedForIncompatibility = false;
function combinePath(parts) {
var rx = /^\/+|\/+$/g; // trim left and right trailing slash
var rxLeftOnly = /^\/+/g; // trim left trailing slash
for (var i = 0, max = parts.length; i < max; i++) {
if (i === max - 1) {
parts[i] = parts[i].replace(rxLeftOnly, "");
} else {
parts[i] = parts[i].replace(rx, "");
}
}
return parts.join("/");
}
export const parseJson = function (response) {
if (response === null || response === undefined || response === "") {
return null;
}
var obj = JSON.parse(response);
// if (obj.hasOwnProperty("d")) {
if (Object.prototype.hasOwnProperty.call(obj, "d")) {
return obj.d;
} else {
return obj;
}
};
export
/**
* Tries to parse the data passed from a drag and drop operation
* @param {Object} dataTransfer - The browser dataTransfer object
* @returns {PMA.UI.Components~dragDropObject} - The parsed drag and drop object or null
* @ignore
*/
const parseDragData = function (dataTransfer) {
try {
var d1 = dataTransfer.getData(PMA.UI.Components.DragDropMimeType); // eslint-disable-line no-undef
return JSON.parse(d1);
} catch (e) { } // eslint-disable-line no-empty
//Try the text for IE compatibility
try {
var d2 = dataTransfer.getData("text");
return JSON.parse(d2);
} catch (e) { } // eslint-disable-line no-empty
return null;
};
/**
* Encodes an object so that it can be passed as an ajax data object
* @param {object} data
* @returns a www-encoded serialized string of the data object
* @ignore
*/
export const formify = function (data) {
if (data && typeof data === "object") {
var sData = "";
var i = 0;
for (var property in data) {
// if (data.hasOwnProperty(property)) {
if (Object.prototype.hasOwnProperty.call(data, property)) {
if (i > 0) {
sData += "&";
}
sData += encodeURIComponent(property) + "=" + encodeURIComponent(data[property]);
i++;
}
}
return sData;
}
return data;
};
/**
* @param {string} url
* @param {string} method
* @param {string} contentType
* @param {object} data
* @param {function} success
* @param {function} failure
* @ignore
*/
export const ajax = function (url, method, contentType, data, success, failure) {
method = method.toUpperCase();
var sData = null;
if (contentType && contentType.toLowerCase && contentType.toLowerCase() === "application/json") {
sData = JSON.stringify(data);
} else {
sData = formify(data);
}
if (sData && method == "GET") {
url = url + "?" + sData;
sData = null;
}
var http = new XMLHttpRequest();
http.open(method, url, true);
if (method == "POST" && !contentType) {
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
} else if (contentType) {
http.setRequestHeader('Content-Type', contentType);
}
http.onreadystatechange = function () {
if (http.readyState === 4) {
if (http.status === 200) {
if (typeof success === "function") {
success(http);
}
} else if (typeof failure === "function") {
failure(http);
}
}
};
if (sData) {
http.send(sData);
} else {
http.send();
}
};
/**
* Invokes a PMA.core API method. This method will properly encode all provided parameters.
* @param {Object} options
* @param {string} options.serverUrl - The URL of the PMA.core instance
* @param {string} options.method - The API method to call
* @param {string} options.httpMethod - The HTTP verb
* @param {object} options.data - The parameters to pass to the API method
* @param {string} [options.apiPath="api"] - The API path to append to the server URL
* @param {string} [options.contentType=""]
* @param {boolean} [options.webapi=false] - Whether the api call is a webapi call
* @param {function} [options.success] - Function to call upon successful method invocation
* @param {function} [options.failure] - Function to call upon unsuccessful method invocation
* @ignore
*/
export const callApiMethod = function (options) {
var httpMethod = "GET";
if (options.httpMethod) {
httpMethod = options.httpMethod;
}
if (!options.apiPath) {
options.apiPath = "api";
}
let infix = options.webapi === true ? "/" : "/json/";
if (options.webapi && httpMethod == "POST" && options.data) {
options.method += "?sessionId=" + options.data.sessionID;
}
ajax(combinePath([options.serverUrl, options.apiPath + infix, options.method]), httpMethod, options.contentType, options.data, options.success, options.failure);
};
export const callApiMethodPromise = function (options) {
return new Promise(function (resolve, reject) {
const localOptions = {
...options,
success: resolve,
failure: reject
};
callApiMethod(localOptions)
});
}
/**
* Authenticates against a PMA.core server
* @param {string} serverUrl
* @param {string} username
* @param {string} password
* @param {string} caller
* @param {function} success
* @param {function} failure
* @ignore
*/
export const login = function (serverUrl, username, password, caller, success, failure) {
if (typeof caller !== "string") {
throw "Caller parameter not supplied";
}
function authFailure(http) {
if (typeof failure === "function") {
if (!http.responseText || http.responseText.length === 0 || http.responseType === "") {
failure({ Message: Resources.translate("Authentication failed") });
} else {
try {
const response = parseJson(http.responseText);
failure(response);
} catch {
failure({ Message: Resources.translate("Authentication failed") });
}
}
}
}
callApiMethodPromise({
serverUrl: serverUrl,
method: ApiMethods.GetVersionInfo
})
.then((versionHttp) => {
let usePost = loginSupportsPost(JSON.parse(versionHttp.responseText));
callApiMethod({
serverUrl: serverUrl,
method: ApiMethods.Authenticate,
httpMethod: usePost ? "POST" : "GET",
contentType: usePost ? "application/json" : null,
data: { username: username, password: password, caller: caller },
success: function (http) {
var response = parseJson(http.responseText);
if (response && response.Success === true) {
// store session ID for this server in global cache
_sessionList.set(serverUrl, response);
if (typeof success === "function") {
success(response.SessionId);
}
} else {
authFailure(http);
}
},
failure: authFailure
});
}, authFailure);
};
/**
* Returns a URL that points to the thumbnail of a slide image
* @name getThumbnailUrl
* @memberof PMA.UI.Components
* @function
* @param {string} serverUrl The PMA.core server URL
* @param {string} sessionId The session id acquired by authenticating to the server
* @param {string} pathOrUid The path or UID of the slide
* @param {Number} [orientation=0] The desired rotation of the thumbnail in degrees
* @param {Number} [width=0] The desired width of the thumbnail in pixels
* @param {Number} [height=0] The desired height of the thumbnail in pixels
*/
export const getThumbnailUrl = function (serverUrl, sessionId, pathOrUid, orientation, width, height) {
return combinePath([serverUrl, "thumbnail"]) +
"?sessionID=" + encodeURIComponent(sessionId) +
"&pathOrUid=" + encodeURIComponent(pathOrUid) +
"&orientation=" + (orientation ? orientation : 0) +
"&w=" + (width ? width : 0) +
"&h=" + (height ? height : 0);
};
/**
* snapshot parameters
* @memberof PMA.UI.Components
* @typedef {Object} PMA.UI.Components~snapshotParameters
* @property {number} x - The x coordinate of the top left point
* @property {number} y - The y coordinate of the top left point
* @property {number} width - The width of the viewport
* @property {number} height - The height of the viewport
* @property {number} rotation - The rotation of the viewport (in degrees)
* @property {boolean} flipHorizontally - Wheter the snapshot is flipped horizontally
* @property {boolean} flipVertically - Wheter the snapshot is flipped vertically
* @property {Array} [channels] - The selected channels
* @property {string} [channelClipping] - A pair of values [0..100] to clip and scale pixel values for each active channel (comma separated)
* @property {string} [channelColor] - Color in css format for each active channel (comma separated)
* @property {string} [gamma] - A value of tha gamma correction factor for each active channel (comma separated)
* @property {number} [layer=0] - The selected layer
* @property {number} [timeframe=0] - The selected timeframe
* @property {number} [scale=null] - A factor to scale the image by. Default is the current viewport scale
* @property {number} [postGamma=null] - A value of the gamma correction factor
* @property {number} [brightness=null] - A value of the brightness
* @property {number} [contrast=null] - A value of the contrast
* @property {number} [dpi=null] - Dots per inch to set to the image
*/
export
/**
* Returns a URL that points to the snapshot of a slide image
* @funtion
* @memberof PMA.UI.Components
* @param {string} serverUrl
* @param {string} sessionId
* @param {string} pathOrUid
* @param {PMA.UI.Components~snapshotParameters} snapshotParameters - The parameters required for the snapshot
* @param {Number} thumbWidth - The final snapshot width (this may be smaller due to aspect ratio)
* @param {Number} thumbHeight - The final snapshot height (this may be smaller due to aspect ratio)
* @param {string} [format=jpg] - The snapshot image format
* @param {boolean} [drawScaleBar=false] - Whether or not to draw a scalebar
* @param {string} [filename="snapshot"] - The filename of the image
* @param {boolean} [download] - True to prompt the user to save the snapshot instead of viewing it within the browser
*/
const getSnapshotUrl = function (serverUrl, sessionId, pathOrUid, snapshotParameters, thumbWidth, thumbHeight, drawScaleBar, format, filename = "snapshot", download) {
if (!format) {
format = "jpg";
}
thumbWidth = thumbWidth ? thumbWidth : 150;
thumbHeight = thumbHeight ? thumbHeight : 150;
var channelsString = "0";
if (snapshotParameters.channels) {
channelsString = snapshotParameters.channels.join(",");
}
var channelsClippingString = "";
if (snapshotParameters.channelClipping) {
channelsClippingString = snapshotParameters.channelClipping;
} else {
channelsClippingString = "0,100"
}
var channelsColorString = "";
if (snapshotParameters.channelColor) {
channelsColorString = snapshotParameters.channelColor;
} else {
channelsColorString = "ffffffff"
}
var channelsGammaString = "";
if (snapshotParameters.gamma) {
channelsGammaString = snapshotParameters.gamma;
} else {
channelsGammaString = "1"
}
var selectedTimeFrame = snapshotParameters.timeframe ? snapshotParameters.timeframe : 0;
var selectedLayer = snapshotParameters.layer ? snapshotParameters.layer : 0;
var scale = snapshotParameters.scale ? snapshotParameters.scale : Math.max(thumbWidth / snapshotParameters.width, thumbHeight / snapshotParameters.height);
if (snapshotParameters.width * scale < 1 || snapshotParameters.height < 1) {
scale = 1 / Math.max(snapshotParameters.width, snapshotParameters.height);
}
var rotation = snapshotParameters.rotation ? snapshotParameters.rotation : 0;
return combinePath([serverUrl, "region"]) +
"?sessionID=" + encodeURIComponent(sessionId) +
"&pathOrUid=" + encodeURIComponent(pathOrUid) +
"&format=" + encodeURIComponent(format) +
"&x=" + Math.floor(snapshotParameters.x) +
"&y=" + Math.floor(snapshotParameters.y) +
"&width=" + Math.floor(snapshotParameters.width) +
"&height=" + Math.floor(snapshotParameters.height) +
"&rotation=" + rotation +
"&flipHorizontal=" + snapshotParameters.flipHorizontally +
"&flipVertical=" + snapshotParameters.flipVertically +
"&timeframe=" + selectedTimeFrame +
"&layer=" + selectedLayer +
"&channels=" + channelsString +
"&channelClipping=" + channelsClippingString +
"&channelColor=" + channelsColorString +
"&gamma=" + channelsGammaString +
"&scale=" + scale +
(snapshotParameters.dpi ? "&dpi=" + snapshotParameters.dpi : "") +
(snapshotParameters.postGamma ? "&postGamma=" + snapshotParameters.postGamma : "") +
(snapshotParameters.brightness ? "&brightness=" + snapshotParameters.brightness : "") +
(snapshotParameters.contrast ? "&contrast=" + snapshotParameters.contrast : "") +
(drawScaleBar ? "&drawScaleBar=true" : "") +
(download ? "&downloadInsteadOfDisplay=true&filename=" + filename : "");
};
/**
* Returns a URL that points to the barcode of a slide image. This method doesn't guarantee that a barcode image actually exists.
* @name getBarcodeUrl
* @memberof PMA.UI.Components
* @function
* @param {string} serverUrl The PMA.core server URL
* @param {string} sessionId The session id acquired by authenticating to the server
* @param {string} pathOrUid The path or UID of the slide
* @param {Number} [rotation=0] The initial rotation for this associated image
*/
export const getBarcodeUrl = function (serverUrl, sessionId, pathOrUid, rotation) {
return combinePath([serverUrl, "barcode"]) +
"?sessionID=" + encodeURIComponent(sessionId) +
"&pathOrUid=" + encodeURIComponent(pathOrUid) +
"&rotation=" + (rotation ? rotation : 0);
};