diff --git a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
index 3d09eb0bfb3..ada4f3655ee 100644
--- a/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
+++ b/Kitodo/src/main/java/org/kitodo/production/forms/dataeditor/GalleryPanel.java
@@ -859,7 +859,7 @@ private void selectMedia(String physicalDivisionOrder, String stripeIndex, Strin
String scrollScripts = "scrollToSelectedTreeNode();scrollToSelectedPaginationRow();";
if (GalleryViewMode.PREVIEW.equals(galleryViewMode)) {
PrimeFaces.current().executeScript(
- "checkScrollPosition();initializeImage();metadataEditor.gallery.mediaView.update();" + scrollScripts);
+ "checkScrollPosition();metadataEditor.detailMap.update();metadataEditor.gallery.mediaView.update();" + scrollScripts);
} else {
PrimeFaces.current().executeScript(scrollScripts);
}
diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
index 91daeb15469..165f745e6f0 100644
--- a/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
+++ b/Kitodo/src/main/webapp/WEB-INF/resources/css/kitodo.css
@@ -3365,6 +3365,9 @@ Column content
left: 16px;
position: absolute;
top: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: start;
}
#map .ol-overlaycontainer-stopevent > .ol-zoom.ol-unselectable.ol-control {
@@ -3386,6 +3389,7 @@ Column content
}
#map .ol-control {
+ display: inline-block;
background: transparent;
padding: 0;
position: static;
diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js
index ccd10ae33c0..4cd4f57e078 100644
--- a/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js
+++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/metadata_editor.js
@@ -14,7 +14,7 @@
/*eslint new-cap: ["error", { "capIsNewExceptionPattern": "^PF" }]*/
/*eslint complexity: ["error", 10]*/
-var metadataEditor = {};
+var metadataEditor = metadataEditor || {};
metadataEditor.metadataTree = {
@@ -1147,7 +1147,7 @@ metadataEditor.shortcuts = {
case "PREVIEW":
initialize();
scrollToSelectedThumbnail();
- changeToMapView();
+ metadataEditor.detailMap.update();
break;
}
}
diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js
index 049d0f7da75..69e9a63eb73 100644
--- a/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js
+++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/ol_custom.js
@@ -11,244 +11,475 @@
/* globals ol */
// jshint unused:false
-// Kitodo namespace
-var kitodo = {};
-kitodo.map = null;
-
/**
- * @param {Object=} options Custom control options for Kitodo in OpenLayers
- * @extends {ol.control.Rotate}
- * @constructor
+ * Abstract class describing a custom control button for the OpenLayers map.
*/
-kitodo.RotateLeftControl = function(options = {}) {
- var buttonLeft = document.createElement('button');
- buttonLeft.innerHTML = "";
- buttonLeft.setAttribute("type", "button");
- buttonLeft.setAttribute("title", "Rotate left");
+class CustomControl extends ol.control.Control {
+
+ /**
+ * Initializes a custom control button with various options.
+ *
+ * @param {object} options the custom control options (className, icon, title, other OpenLayer options)
+ */
+ constructor(options) {
+ const className = options.className;
+ const icon = options.icon;
+ const title = options.title;
+
+ const button = document.createElement('button');
+ button.innerHTML = "";
+ button.setAttribute("type", "button");
+ button.setAttribute("title", title);
+
+ const element = document.createElement('div');
+ element.className = className + ' ol-unselectable ol-control ol-rotate';
+ element.appendChild(button);
+
+ super({
+ element,
+ target: options.target
+ });
+
+ button.addEventListener('click', this.handleClick.bind(this), false);
+ }
- var this_ = this;
+ /**
+ * Abstract method that handles a click event on the button.
+ *
+ * @param {MouseEvent} event the click event
+ */
+ handleClick(event) {
+ // not implemented
+ }
+}
- var handleRotateLeft = function() {
- var view = this_.getMap().getView();
+/**
+ * Custom control that rotates the image 90 degrees to the left.
+ */
+class RotateLeftControl extends CustomControl {
+
+ constructor(options) {
+ super(Object.assign(options || {}, {
+ className: "rotate-left",
+ icon: "fa-undo",
+ title: "Rotate left",
+ }));
+ }
+
+ handleClick() {
+ const view = this.getMap().getView();
view.animate({
rotation: view.getRotation() - (90 * (Math.PI / 180)),
duration: 100
});
- };
-
- buttonLeft.addEventListener('click', handleRotateLeft, false);
-
- var elementLeft = document.createElement('div');
- elementLeft.className = 'rotate-left ol-unselectable ol-control ol-rotate';
- elementLeft.appendChild(buttonLeft);
-
- ol.control.Control.call(this, {
- element: elementLeft,
- target: options.target
- });
+ }
};
/**
- * @param {Object=} options Custom control options for Kitodo in OpenLayers
- * @extends {ol.control.Rotate}
- * @constructor
+ * Custom control that rotates the image 90 degrees to the right.
*/
-kitodo.RotateRightControl = function(options = {}) {
- var buttonRight = document.createElement('button');
- buttonRight.innerHTML = "";
- buttonRight.setAttribute("type", "button");
- buttonRight.setAttribute("title", "Rotate right");
-
- var this_ = this;
+class RotateRightControl extends CustomControl {
+
+ constructor(options) {
+ super(Object.assign(options || {}, {
+ className: "rotate-right",
+ icon: "fa-repeat",
+ title: "Rotate right",
+ }));
+ }
- var handleRotateRight = function() {
- var view = this_.getMap().getView();
+ handleClick() {
+ const view = this.getMap().getView();
view.animate({
- rotation: view.getRotation() + (90 * (Math.PI / 180)),
+ rotation: view.getRotation() + (90 * (Math.PI / 180)),
duration: 100
});
- };
-
- buttonRight.addEventListener('click', handleRotateRight, false);
-
- var elementRight = document.createElement('div');
- elementRight.className = 'rotate-right ol-unselectable ol-control ol-rotate';
- elementRight.appendChild(buttonRight);
-
- ol.control.Control.call(this, {
- element: elementRight,
- target: options.target,
- duration: 250
- });
+ }
};
-function resetNorth() {
- if (kitodo.map) {
- let view = kitodo.map.getView();
- view.animate({
+/**
+ * Custom control that rotates the image back to default.
+ */
+class RotateNorthControl extends CustomControl {
+
+ constructor(options) {
+ super(Object.assign(options || {}, {
+ className: "rotate-north",
+ icon: "fa-compass",
+ title: "Reset orientation",
+ }));
+ }
+
+ handleClick() {
+ this.getMap().getView().animate({
rotation: 0,
- duration: 0
+ duration: 100
});
}
-}
+};
/**
- * @param {Object=} options Custom control options for Kitodo in OpenLayers
- * @extends {ol.control.Rotate}
- * @constructor
+ * Custom control that scales the image back to default.
*/
-kitodo.ResetNorthControl = function(options = {}) {
- let buttonResetNorth = document.createElement("button");
- buttonResetNorth.innerHTML = "";
- buttonResetNorth.setAttribute("type", "button");
- buttonResetNorth.setAttribute("title", "Reset orientation");
-
- buttonResetNorth.addEventListener("click", resetNorth, false);
-
- let elementResetNorth = document.createElement("div");
- elementResetNorth.className = "ol-rotate ol-unselectable ol-control"; /*ol-rotate-reset*/
- elementResetNorth.appendChild(buttonResetNorth);
-
- ol.control.Control.call(this, {
- element: elementResetNorth,
- target: options.target,
- duration: 250
- });
+class ResetZoomControl extends CustomControl {
+
+ /** The image dimensions as OpenLayers extent */
+ #extent;
+
+ /**
+ * Initialize a custom control button that scales the image back to its default position.
+ *
+ * @param {object} options containing the extent (Array) describing the image dimensions
+ */
+ constructor(options) {
+ super(Object.assign(options, {
+ className: "reset-zoom",
+ icon: "fa-expand",
+ title: "Reset zoom",
+ }));
+ this.#extent = options.extent;
+ }
+
+ handleClick() {
+ this.getMap().getView().fit(this.#extent, {});
+ }
};
-ol.inherits(kitodo.RotateLeftControl, ol.control.Rotate);
-ol.inherits(kitodo.RotateRightControl, ol.control.Rotate);
-ol.inherits(kitodo.ResetNorthControl, ol.control.Rotate);
+/**
+ * Class managing OpenLayers detail map showing images.
+ */
+class KitodoDetailMap {
+
+ /**
+ * Remember position, rotation and zoom level such that a new OpenLayers map can be
+ * initialized with the same position, rotation and zoom level when new images are selected.
+ */
+ #view = {
+ center: null,
+ zoom: null,
+ rotation: null,
+ };
-function random(length) {
- var text = "";
- var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ /**
+ * Image properties of the image that is currently shown in OpenLayers. Object with properties
+ * dimensions (width, height) and path (url).
+ */
+ #image = {
+ dimensions: null,
+ path: null,
+ };
+
+ /**
+ * The OpenLayers maps instance
+ */
+ #map = null;
- for (var i = 0; i < length; i++) {
- text += possible.charAt(Math.floor(Math.random() * possible.length));
+ /**
+ * Initialize a new Kitodo detail map
+ */
+ constructor() {
+ this.registerResizeEvent();
}
- return text;
-}
+ /**
+ * Debounces various event handlers to improve performance, e.g. when resizing.
+ *
+ * @param {function} func the function to be debounced
+ * @param {number} timeout the timeout in milliseconds
+ *
+ * @returns {function} the debounced function
+ */
+ static makeDebounced(func, timeout = 100) {
+ let timer = null;
+ return function () {
+ clearTimeout(timer);
+ timer = setTimeout(func, timeout);
+ };
+ }
-function createProjection(extent) {
- return new ol.proj.Projection({
- code: 'kitodo-image',
- units: 'pixels',
- extent: extent
- });
-}
+ /**
+ * Generate a random string of [a-zA-Z0-9].
+ *
+ * @param {number} length the length of the string to be generated
+ * @returns the string of random characters
+ */
+ static randomUUID(length) {
+ const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+ let text = "";
+ for (let i = 0; i < length; i++) {
+ text += possible.charAt(Math.floor(Math.random() * possible.length));
+ }
+
+ return text;
+ }
-function createSource(extent, imagePath, projection) {
- return new ol.source.ImageStatic({
- url: imagePath,
- projection: projection,
- imageExtent: extent
- });
-}
+ /**
+ * Create an OpenLayers projection given the image extent.
+ *
+ * @param {Array} extent the extent describing the image dimensions
+ * @returns {ol.proj.Projection} the OpenLayers projection
+ */
+ createProjection(extent) {
+ return new ol.proj.Projection({
+ code: 'kitodo-image',
+ units: 'pixels',
+ extent: extent
+ });
+ }
-function hideCanvas() {
- let map = document.querySelector("#map canvas");
- let loadingIcon = document.querySelector("#map > .fa-spinner");
- if (map) {
- map.style.opacity = 0;
- loadingIcon.style.opacity = 1;
+ /**
+ * Create an OpenLayers source object for the image.
+ *
+ * @param {Array} extent the extent describing the image dimensions
+ * @param {string} path the path (url) of the image
+ * @param {ol.proj.Projection} projection the OpenLayers projection used to map the image to the canvas
+ * @returns {ol.source.ImageStatic} the OpenLayers image source object
+ */
+ createSource(extent, path, projection) {
+ return new ol.source.ImageStatic({
+ url: path,
+ projection: projection,
+ imageExtent: extent,
+ interpolate: true
+ });
}
-}
-function showCanvas() {
- let map = document.querySelector("#map canvas");
- let loadingIcon = document.querySelector("#map > .fa-spinner");
- if (map) {
- map.style.opacity = 1;
- loadingIcon.style.opacity = 0;
+ /**
+ * Hide the map canvas (while the image is loading)
+ */
+ hideCanvas() {
+ let canvas = document.querySelector("#map canvas");
+ let loadingIcon = document.querySelector("#map > .fa-spinner");
+ if (canvas) {
+ canvas.style.opacity = 0;
+ loadingIcon.style.opacity = 1;
+ }
+ }
+
+ /**
+ * Show the map canvas (as soon as the image has finished loading)
+ */
+ showCanvas() {
+ let canvas = document.querySelector("#map canvas");
+ let loadingIcon = document.querySelector("#map > .fa-spinner");
+ if (canvas) {
+ canvas.style.opacity = 1;
+ loadingIcon.style.opacity = 0;
+ }
+ }
+
+ /**
+ * Handler that is called as soon as the image was completely loaded
+ * @param {*} image the jQuery image dom element
+ */
+ onImageLoad(image) {
+ this.#image = {
+ dimensions: [image.width(), image.height()],
+ path: image[0].src,
+ };
+ this.initializeOpenLayersMap();
}
-}
-function initializeMap(imageDimensions, imagePath) {
- // Map image coordinates to map coordinates to be able to use image extent in pixels.
- let extent = [0, 0, imageDimensions[0], imageDimensions[1]];
- let projection = createProjection(extent);
-
- kitodo.map = new ol.Map({
- controls: ol.control.defaults({
- attributionOptions: {
- collapsible: false
- },
- zoomOptions: {
- delta: 3
- },
- rotate: false
- }).extend([
- new kitodo.RotateRightControl(),
- new kitodo.RotateLeftControl(),
- new kitodo.ResetNorthControl()
- ]),
- layers: [
- new ol.layer.Image({
- source: createSource(extent, imagePath, projection)
+ /**
+ * Register the load event for the current image.
+ */
+ registerImageLoadEvent() {
+ this.hideCanvas();
+ let image = $("#imagePreviewForm\\:mediaPreviewGraphicImage");
+ if (image.length > 0) {
+ image.on("load", this.onImageLoad.bind(this, image));
+ image[0].src = image[0].src.replace(/&uuid=[a-z0-9]+/i, "") + "&uuid=" + KitodoDetailMap.randomUUID(8);
+ }
+ }
+
+ /**
+ * Return extent array containg image dimensions.
+ *
+ * @param {Array} dimensions dimensions in pixel as [width, height]
+ * @returns {Array} the extent array
+ */
+ createImageExtent(dimensions) {
+ return [0, 0, dimensions[0], dimensions[1]];
+ }
+
+ /**
+ * Creates the OpenLayers map object as soon as the image as been loaded.
+ */
+ initializeOpenLayersMap() {
+ // Map image coordinates to map coordinates to be able to use image extent in pixels.
+ const extent = this.createImageExtent(this.#image.dimensions);
+ const projection = this.createProjection(extent);
+
+ if (this.#map) {
+ // make last OpenLayers map forget canvas target
+ // (triggers OpenLayers cleanup code and allows garbage collection)
+ this.#map.setTarget(null);
+ }
+
+ // initialize new OpenLayers map
+ this.#map = new ol.Map({
+ controls: ol.control.defaults({
+ attributionOptions: {
+ collapsible: false
+ },
+ zoomOptions: {
+ delta: 3 // zoom delta when clicking zoom buttons
+ },
+ rotate: false
+ }).extend([
+ new RotateLeftControl(),
+ new RotateRightControl(),
+ new RotateNorthControl(),
+ new ResetZoomControl({ extent })
+ ]),
+ interactions: ol.interaction.defaults({
+ zoomDelta: 5, // zoom delta when using mouse wheel
+ zoomDuration: 100,
+ }),
+ layers: [
+ new ol.layer.Image({
+ source: this.createSource(extent, this.#image.path, projection)
+ })
+ ],
+ target: 'map',
+ view: new ol.View({
+ projection: projection,
+ center: this.unnormalizeCenter(this.#view.center, extent),
+ zoom: this.#view.zoom,
+ rotation: this.#view.rotation,
+ zoomFactor: 1.1,
+ extent,
+ constrainOnlyCenter: true,
+ smoothExtentConstraint: true,
+ showFullExtent: true,
+ padding: [20, 20, 20, 20]
})
- ],
- target: 'map',
- view: new ol.View({
- projection: projection,
- center: ol.extent.getCenter(extent),
- zoomFactor: 1.1
- })
- });
- kitodo.map.getView().fit(extent, {});
- kitodo.map.on("rendercomplete", function () {
- showCanvas();
- });
-}
+ });
+ if (this.#view.center == null) {
+ // fit image to current viewport unless previous zoom and center position is known
+ this.#map.getView().fit(extent, {});
+ }
+ // register various events to make sure that previous view is remembered
+ this.#map.on("rendercomplete", KitodoDetailMap.makeDebounced(this.onRenderComplete.bind(this)));
+ this.#map.on("change", KitodoDetailMap.makeDebounced(this.saveCurrentView.bind(this)));
+ this.#map.on("postrender", KitodoDetailMap.makeDebounced(this.saveCurrentView.bind(this)));
+ }
-function updateMap(imageDimensions, imagePath) {
- // Map image coordinates to map coordinates to be able to use image extent in pixels.
- let extent = [0, 0, imageDimensions[0], imageDimensions[1]];
- let projection = createProjection(extent);
+ /**
+ * Return unnormalized center coordinates in case previous center is known (not null). Otherwise
+ * center is calculated from the image extent containing image dimensions.
+ *
+ * @param {Array} center the normalized center coordinates [0..1, 0..1]
+ * @returns {Array} unnormalized center
+ */
+ unnormalizeCenter(center) {
+ if (center !== null) {
+ return [
+ center[0] * this.#image.dimensions[0],
+ center[1] * this.#image.dimensions[1],
+ ];
+ }
+ return ol.extent.getCenter(this.createImageExtent(this.#image.dimensions));
+ }
- kitodo.map.getLayers().getArray()[0].setSource(createSource(extent, imagePath, projection));
- kitodo.map.getView().setCenter(ol.extent.getCenter(extent));
- kitodo.map.getView().getProjection().setExtent(extent);
- kitodo.map.getView().fit(extent, {});
-}
+ /**
+ * Normalizes the center coordinates from [0..width, 0..height] to [0..1, 0..1] such
+ * that images with different dimensions are visualized at the same relative position
+ * in the viewport.
+ *
+ * @param {Array} center the current center coordinates as reported by OpenLayers
+ * @returns {Array} the normalized center coordinates
+ */
+ normalizeCenter(center) {
+ return [
+ center[0] / this.#image.dimensions[0],
+ center[1] / this.#image.dimensions[1],
+ ];
+ }
+
+ /**
+ * Remembers current view properties (center, zoom rotation) such that the OpenLayers
+ * map can be initialized with the same parameters when selecting another image.
+ */
+ saveCurrentView() {
+ this.#view = {
+ center: this.normalizeCenter(this.#map.getView().getCenter()),
+ zoom: this.#map.getView().getZoom(),
+ rotation: this.#map.getView().getRotation(),
+ };
+ }
-function addListener(element) {
- element.on("load", function () {
- if (kitodo.map && $("#map .ol-viewport").length) {
- updateMap([element.width(), element.height()], element[0].src);
- } else {
- initializeMap([element.width(), element.height()], element[0].src);
+ /**
+ * Is called by OpenLayers whenever a canvas rendering has finished. Unless debounced, this
+ * event is triggered potentially at 60fps.
+ */
+ onRenderComplete() {
+ this.showCanvas();
+ this.saveCurrentView();
+ }
+
+ /**
+ * Registers the resize event for the meta data editor column, such that the image can be
+ * repositioned appropriately.
+ */
+ registerResizeEvent() {
+ // reload map if container was resized
+ $('#thirdColumnWrapper').on('resize', KitodoDetailMap.makeDebounced(this.onResize.bind(this)));
+ }
+
+ /**
+ * Return current zoom level. Is used by integration tests to verify zoom buttons work.
+ * @returns {number} current zoom level
+ */
+ getZoom() {
+ if (this.#map) {
+ return this.#map.getView().getZoom();
}
- });
-}
+ return -1;
+ }
-function initializeImage() {
- resetNorth();
- hideCanvas();
- let image = $("#imagePreviewForm\\:mediaPreviewGraphicImage");
- if (image.length > 0) {
- addListener(image);
- image[0].src = image[0].src.replace(/&uuid=[a-z0-9]+/i, "") + "&uuid=" + random(8);
+ /**
+ * Returns true if the canvas is currently animated. Is used by integration tests to wait for animation end.
+ * @returns {boolean} whether animation is still ongoing
+ */
+ getAnimating() {
+ if (this.#map) {
+ return this.#map.getView().getAnimating();
+ }
+ return false;
}
-}
-function changeToMapView() {
- initializeImage();
- showCanvas();
- if (kitodo.map) {
- kitodo.map.handleTargetChanged_();
+ /**
+ * Return current rotation. Is used by integration tests to verify rotation buttons work.
+ * @returns {number} current rotation
+ */
+ getRotation() {
+ if (this.#map) {
+ return this.#map.getView().getRotation();
+ }
+ return 0;
}
-}
-// reload map if container was resized
-$('#thirdColumnWrapper').on('resize', function () {
- if (kitodo.map) {
- // FIXME: This causes lags. It should only be executed *once* after resize.
- kitodo.map.updateSize();
+ /**
+ * Is called when a resize event has happened. Unless debounced, this event is triggered potentially
+ * at 60fps.
+ */
+ onResize() {
+ if (this.#map) {
+ this.#map.updateSize();
+ }
+ }
+
+ /**
+ * Reloads the image. Is called when the detail view is activated, or a new image was selected.
+ */
+ update() {
+ this.registerImageLoadEvent();
}
-});
+}
+
+// register detail map class with the metadataEditor namespace
+var metadataEditor = metadataEditor || {};
+metadataEditor.detailMap = new KitodoDetailMap();
-$(document).ready(function () {
- initializeImage();
-});
diff --git a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js
index 208bb40dcc1..747d6129d20 100644
--- a/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js
+++ b/Kitodo/src/main/webapp/WEB-INF/resources/js/resize.js
@@ -397,6 +397,7 @@ function toggleThirdColumn() {
secondColumn.animate({width: wrapper.width() - firstColumn.width() - COLLAPSED_COL_WIDTH - 2 * SEPARATOR_WIDTH});
}
} else {
+ metadataEditor.detailMap.update();
var neededWidth = thirdColumnWidth - COLLAPSED_COL_WIDTH - (secondColumn.width() - secondColumn.data('min-width'));
if (secondColumn.hasClass(COLLAPSED)) {
firstColumn.animate({width: wrapper.width() - COLLAPSED_COL_WIDTH - thirdColumnWidth - 2 * SEPARATOR_WIDTH});
@@ -522,16 +523,14 @@ function updateMetadataEditorView(showMetadataColumn) {
}
expandThirdColumn();
scrollToSelectedThumbnail();
- initializeImage();
+ metadataEditor.detailMap.update();
metadataEditor.gallery.mediaView.update();
scrollToSelectedTreeNode();
scrollToSelectedPaginationRow();
}
function resizeMap() {
- if (kitodo.map) {
- kitodo.map.updateSize();
- }
+ metadataEditor.detailMap.onResize();
}
function saveLayout() {
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml
index 3b45179b4ac..aca83ac8064 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/dialogs/pagination.xhtml
@@ -47,7 +47,7 @@
showCheckbox="true">
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml
index 1cd71f79da8..7f43821e79a 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/gallery.xhtml
@@ -66,7 +66,7 @@
@@ -291,8 +291,8 @@
galleryWrapperPanel"/>
-
-
+
+
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
index fbe13c8c286..f4dc1e5fb9e 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/logicalStructure.xhtml
@@ -73,7 +73,7 @@
listener="#{DataEditorForm.structurePanel.treeLogicalSelect}"
oncomplete="scrollToSelectedThumbnail();
scrollToSelectedPaginationRow();
- changeToMapView();
+ metadataEditor.detailMap.update();
metadataEditor.gallery.mediaView.update();
expandMetadata('logical-metadata-tab');"
update="galleryHeadingWrapper
@@ -89,7 +89,7 @@
onstart="$('#contextMenuLogicalTree .ui-menuitem').addClass('ui-state-disabled')"
oncomplete="scrollToSelectedThumbnail();
scrollToSelectedPaginationRow();
- changeToMapView();
+ metadataEditor.detailMap.update();
metadataEditor.gallery.mediaView.update();
PF('contextMenuLogicalTree').show(currentEvent)"
update="@(.stripe)
diff --git a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml
index 5cae295802a..805262f113c 100644
--- a/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml
+++ b/Kitodo/src/main/webapp/WEB-INF/templates/includes/metadataEditor/physicalStructure.xhtml
@@ -34,7 +34,7 @@
listener="#{DataEditorForm.structurePanel.treePhysicalSelect}"
oncomplete="scrollToSelectedThumbnail();
scrollToSelectedPaginationRow();
- initializeImage();
+ metadataEditor.detailMap.update();
metadataEditor.gallery.mediaView.update();
expandMetadata('physical-metadata-tab');"
update="galleryHeadingWrapper
@@ -44,7 +44,7 @@
div:nth-child(2) .thumbnail-container";
+ private static final Double EPSILON = 0.001;
+
+ private static int processId = -1;
+
+ /**
+ * Prepare tests by inserting dummy processes into database and index for sub-folders of test metadata resources.
+ * @throws DAOException when saving of dummy or test processes fails.
+ * @throws DataException when retrieving test project for test processes fails.
+ * @throws IOException when copying test metadata or image files fails.
+ */
+ @BeforeAll
+ public static void prepare() throws DAOException, DataException, IOException {
+ MockDatabase.insertFoldersForSecondProject();
+ processId = MockDatabase.insertTestProcessIntoSecondProject(PROCESS_TITLE);
+ ProcessTestUtils.copyTestFiles(processId, TEST_RENAME_MEDIA_FILE);
+ }
+
+ /**
+ * Tests whether the image preview is shown when a user clicks on the image preview button.
+ * @throws Exception when something fails
+ */
+ @Test
+ public void imageVisibleTest() throws Exception {
+ login("kowal");
+ Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE);
+
+ // check detail view is not yet visible
+ assertEquals(0, findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).size());
+
+ // open detail view
+ Pages.getMetadataEditorPage().openDetailView();
+
+ // check it is visible now
+ pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed());
+ }
+
+ /**
+ * Tests whether the zoom buttons of the image preview (zoom in, out, reset) work as intended.
+ * @throws Exception when something fails
+ */
+ @Test
+ public void zoomLevelTest() throws Exception {
+ login("kowal");
+
+ // open detail view and wait for openlayers canvas
+ Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE);
+ Pages.getMetadataEditorPage().openDetailView();
+ pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed());
+
+ // remember initial zoom
+ Double initialZoom = getOpenLayersZoom();
+ assertTrue(initialZoom > 0);
+
+ // zoom in, and check zoom increases
+ findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> getOpenLayersZoom() > initialZoom);
+
+ // zoom out, and check zoom returns to initial zoom level
+ findElementsByCSS(OPEN_LAYERS_ZOOM_OUT_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> Math.abs(getOpenLayersZoom() - initialZoom) < EPSILON);
+
+ // zoom in, and reset zoom, check zoom returns to initial zoom level
+ findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ findElementsByCSS(OPEN_LAYERS_ZOOM_RESET_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> Math.abs(getOpenLayersZoom() - initialZoom) < EPSILON);
+ }
+
+ /**
+ * Tests whether the rotation buttons of the image preview (rotation left, right, north) work as intended.
+ * @throws Exception when something fails
+ */
+ @Test
+ public void rotationTest() throws Exception {
+ login("kowal");
+
+ // open detail view and wait for openlayers canvas
+ Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE);
+ Pages.getMetadataEditorPage().openDetailView();
+ pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed());
+
+ // check initial rotation is zero
+ assertTrue(Math.abs(getOpenLayersRotation()) < EPSILON);
+
+ // rotate left and check rotation is decreasing
+ findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> getOpenLayersRotation() < 0.0);
+
+ // rotate back, and check rotation returns to zero
+ findElementsByCSS(OPEN_LAYERS_ROTATE_RIGHT_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> Math.abs(getOpenLayersRotation()) < EPSILON);
+
+ // rotate left and reset to north, check rotation returns to zero
+ findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ findElementsByCSS(OPEN_LAYERS_ROTATE_NORTH_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+ assertTrue(() -> Math.abs(getOpenLayersRotation()) < EPSILON);
+ }
+
+ /**
+ * Tests that both zoom level and rotation persists when a user clicks on another image
+ * (which causes OpenLayers to be loaded again).
+ * @throws Exception when something fails
+ */
+ @Test
+ public void viewPersistsImageChange() throws Exception {
+ login("kowal");
+
+ // open detail view and wait for openlayers canvas
+ Pages.getProcessesPage().goTo().editMetadata(PROCESS_TITLE);
+ Pages.getMetadataEditorPage().openDetailView();
+ pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed());
+
+ // remember initial zoom, rotation
+ Double initialZoom = getOpenLayersZoom();
+ Double initialRotation = getOpenLayersRotation();
+
+ // rotate left and zoom in
+ findElementsByCSS(OPEN_LAYERS_ROTATE_LEFT_SELECTOR).get(0).click();
+ findElementsByCSS(OPEN_LAYERS_ZOOM_IN_SELECTOR).get(0).click();
+ pollAssertTrue(() -> !isOpenLayersAnimating());
+
+ // remember changed zoom, rotation
+ Double changedZoom = getOpenLayersZoom();
+ Double changedRotation = getOpenLayersRotation();
+
+ // verify zoom and rotation was applied
+ assertTrue(Math.abs(initialZoom - changedZoom) > 0);
+ assertTrue(Math.abs(initialRotation - changedRotation) > 0);
+
+ // change to second image
+ findElementsByCSS(SECOND_THUMBNAIL_SELECTOR).get(0).click();
+
+ // wait until second image has been loaded
+ pollAssertTrue(
+ () -> "Bild 1, Seite -".equals(
+ findElementsByCSS(GALLERY_HEADING_WRAPPER_SELECTOR).get(0).getText().strip()
+ )
+ );
+
+ // wait until OpenLayers canvas is available
+ pollAssertTrue(() -> findElementsByCSS(OPEN_LAYERS_CANVAS_SELECTOR).get(0).isDisplayed());
+
+ // check that rotation and zoom was correctly applied to next image (and is not reset)
+ assertTrue(Math.abs(getOpenLayersZoom() - changedZoom) < EPSILON);
+ assertTrue(Math.abs(getOpenLayersRotation() - changedRotation) < EPSILON);
+ }
+
+ /**
+ * Close metadata editor and logout after every test.
+ * @throws Exception when page navigation fails
+ */
+ @AfterEach
+ public void closeEditorAndLogout() throws Exception {
+ Pages.getMetadataEditorPage().closeEditor();
+ Pages.getTopNavigation().logout();
+ }
+
+ /**
+ * Cleanup test environment by removing temporal dummy processes from database and index.
+ * @throws DAOException when dummy process cannot be removed from database
+ * @throws CustomResponseException when dummy process cannot be removed from index
+ * @throws DataException when dummy process cannot be removed from index
+ * @throws IOException when deleting test files fails.
+ */
+ @AfterAll
+ public static void cleanup() throws DAOException, CustomResponseException, DataException, IOException {
+ ProcessService.deleteProcess(processId);
+ }
+
+ private void login(String username) throws InstantiationException, IllegalAccessException, InterruptedException {
+ User metadataUser = ServiceManager.getUserService().getByLogin(username);
+ Pages.getLoginPage().goTo().performLogin(metadataUser);
+ }
+
+ private List findElementsByCSS(String css) {
+ return Browser.getDriver().findElements(By.cssSelector(css));
+ }
+
+ private void pollAssertTrue(Callable conditionEvaluator) throws Exception {
+ await().ignoreExceptions().pollInterval(100, TimeUnit.MILLISECONDS).atMost(3, TimeUnit.SECONDS)
+ .until(conditionEvaluator);
+ }
+
+ private Boolean isOpenLayersAnimating() {
+ return (Boolean)Browser.getDriver().executeScript("return metadataEditor.detailMap.getAnimating()");
+ }
+
+ private Double getOpenLayersZoom() {
+ Object result = Browser.getDriver().executeScript("return metadataEditor.detailMap.getZoom()");
+ return ((Number)result).doubleValue();
+ }
+
+ private Double getOpenLayersRotation() {
+ Object result = Browser.getDriver().executeScript("return metadataEditor.detailMap.getRotation()");
+ return ((Number)result).doubleValue();
+ }
+
+}
diff --git a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java
index e6284c17a9d..131830957e6 100644
--- a/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java
+++ b/Kitodo/src/test/java/org/kitodo/selenium/testframework/pages/MetadataEditorPage.java
@@ -78,6 +78,9 @@ public class MetadataEditorPage extends Page {
@FindBy(id = "renamingMediaResultForm:okSuccess")
private WebElement okButtonRenameMediaFiles;
+ @FindBy(id = "imagePreviewForm:previewButton")
+ private WebElement imagePreviewButton;
+
public MetadataEditorPage() {
super("metadataEditor.jsf");
}
@@ -222,4 +225,12 @@ public long getNumberOfDisplayedStructureElements() {
return Browser.getDriver().findElements(By.cssSelector(".ui-treenode")).stream().filter(WebElement::isDisplayed)
.count();
}
+
+ /**
+ * Open detail view by clicking on image preview button.
+ */
+ public void openDetailView() {
+ imagePreviewButton.click();
+ }
+
}
diff --git a/pom.xml b/pom.xml
index 9d0be668f3d..baa5fa7c7fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -788,7 +788,7 @@ from system library in Java 11+ -->
org.webjars
openlayers
- 5.2.0
+ 6.14.1
org.webjars