Skip to content

Commit

Permalink
Merge pull request #114 from RationAI/development
Browse files Browse the repository at this point in the history
feat: winter school features + bugfixes: 1
  • Loading branch information
Aiosa authored Jan 21, 2025
2 parents a201ed6 + a7cfe88 commit 9e81765
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 163 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ The changelog file describes changes made since v2.0.0, which made significant c
to the versions 1.x.x.

### Unreleased 2.1.1
**Features:** standalone wsi tile source module.
**Features:** standalone wsi tile source module. Edge navigation optional.

**Bugfixes:** OIDC module popup method - await login.
Use session storage to store xOpat sessions as well.
Fixed scalebar magnification estimates. Annotations IO bugfixes.
Extend await event support.

### 2.1.0
**Features:** new system for module/plugin building, improvements of annotation listing features,
Expand Down
7 changes: 4 additions & 3 deletions docs/web/xopat_deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ To deploy xOpat, we need to configure it statically,
"path": "/",
// The default image server used. Configures an OpenSeadragon protocol using here URL of the service
"image_group_server": "http://localhost:8080",
"image_group_protocol": "`${path}/v3/batch/info?slides=${data}`",
"image_group_preview": "`${path}/v3/batch/thumbnail/max_size/250/250?slides=${data}`",
"image_group_protocol": "`${path}/v3/slides/info?slide_id=${data}`",
"image_group_preview": "`${path}/v3/slides/thumbnail/max_size/250/250?slide_id=${data}`",
"data_group_server": "http://localhost:8080",
"data_group_protocol": "`${path}/v3/batch/info?slides=${data.join(\",\")}`",
// This endpoint needs to ask for array of data items (get me tile level 5 x3 y0 for this slide list)
"data_group_protocol": "`${path}/v3/files/info?paths=${data.join(\",\")}`",
"headers": {},
"js_cookie_expire": 365,
"js_cookie_path": "/",
Expand Down
110 changes: 54 additions & 56 deletions modules/annotations/annotations.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,13 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
object.zooming(this.canvas.computeGraphicZoom(zoom), zoom);
}

setCloseEdgeMouseNavigation(enable) {
window.removeEventListener("mousemove", this._edgesMouseNavigation);
if (enable) {
window.addEventListener("mousemove", this._edgesMouseNavigation);
}
}

/************************ Layers *******************************/

/**
Expand Down Expand Up @@ -891,19 +898,19 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
* @param {boolean} defaultIfUnknown if false, empty string is returned in case no property was found
* @return {string|*} annotation description
*/
getAnnotationDescription(annotation, desiredKey="category", defaultIfUnknown=true) {
getAnnotationDescription(annotation, desiredKey="category", defaultIfUnknown=true, withCoordinates=true) {
let preset = this.presets.get(annotation.presetID);
if (preset) {
for (let key in preset.meta) {
let objmeta = annotation.meta || {}, overridingValue = objmeta[key];
let metaElement = preset.meta[key];
if (key === desiredKey) {
return overridingValue || metaElement.value ||
(defaultIfUnknown ? this.getDefaultAnnotationName(annotation) : "");
(defaultIfUnknown ? this.getDefaultAnnotationName(annotation, withCoordinates) : "");
}
}
}
return defaultIfUnknown ? this.getDefaultAnnotationName(annotation) : "";
return defaultIfUnknown ? this.getDefaultAnnotationName(annotation, withCoordinates) : "";
}

/**
Expand Down Expand Up @@ -1047,6 +1054,19 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
return o.hasOwnProperty("incrementId") && o.hasOwnProperty("sessionID");
}

/**
* Find annotations by a predicate
* @param callback
* @return {*}
*/
find(callback) {
return this.canvas._objects.find(callback);
}

filter(callback) {
return this.canvas._objects.filter(callback);
}

/**
* Delete object without knowledge of its identity (fully-fledged annotation or helper one)
* @param {fabric.Object} o
Expand Down Expand Up @@ -1540,49 +1560,9 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
_this.mode.handleMouseMove(o.e, screenToPixelCoords(o.e.x, o.e.y));
} else {
_this.mode.handleMouseHover(o.e, screenToPixelCoords(o.e.x, o.e.y));

}
});

// Cached event that keeps moving the viewport is not useful since it keeps moving when user exist the window
// let _lastCalled = 0;
// let _cachedEvent = null;
const mouseNavigation = e => {
// const now = Date.now();
if (this.mode !== this.Modes.AUTO /*|| now - _lastCalled > 30*/) {
//_cachedEvent = e || _cachedEvent; // keep reference to the most recent event

const edgeThreshold = 20;
// const mouseX = _cachedEvent.clientX;
// const mouseY = _cachedEvent.clientY;
const mouseX = e.clientX;
const mouseY = e.clientY;

const nearLeftEdge = mouseX >= 0 && edgeThreshold - mouseX;
const nearTopEdge = mouseY >= 0 && edgeThreshold / 2 - mouseY; //top edge near
const nearRightEdge = mouseX - window.innerWidth + edgeThreshold;
const nearBottomEdge = mouseY - window.innerHeight + edgeThreshold;

if (
(nearTopEdge < edgeThreshold && nearTopEdge > 0) ||
(nearRightEdge < edgeThreshold && nearRightEdge > 0) ||
(nearBottomEdge < edgeThreshold && nearBottomEdge > 0) ||
(nearLeftEdge < edgeThreshold && nearLeftEdge > 0)
) {
const center = VIEWER.viewport.getCenter(true);
//const current = VIEWER.viewport.windowToViewportCoordinates(new OpenSeadragon.Point(_cachedEvent.x, _cachedEvent.y));
const current = VIEWER.viewport.windowToViewportCoordinates(new OpenSeadragon.Point(e.x, e.y));
let direction = current.minus(center);
direction = direction.divide(Math.sqrt(Math.pow(direction.x, 2) + Math.pow(direction.y, 2)));
VIEWER.viewport.panTo(direction.times(0.004 / VIEWER.scalebar.imagePixelSizeOnScreen()).plus(center));
//_lastCalled = now;
//setTimeout(mouseNavigation, 35);
}
}
};

window.addEventListener("mousemove", mouseNavigation);

this.canvas.on('mouse:wheel', function (o) {
if (_this.disabledInteraction) return;

Expand Down Expand Up @@ -1618,6 +1598,9 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {

// Wheel while viewer runs not enabled because this already performs zoom.
// VIEWER.addHandler("canvas-scroll", function (e) { ... });

// Rewrite with bind this arg to use in events
this._edgesMouseNavigation = this._edgesMouseNavigation.bind(this);
}

static _registerAnnotationFactory(FactoryClass, atRuntime) {
Expand Down Expand Up @@ -1748,15 +1731,6 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
}

_loadObjects(input, clear, reviver, inheritSession) {
const originalToObject = fabric.Object.prototype.toObject;
const inclusionProps = this._exportedPropertiesGlobal();

//we ignore incoming props as we later reset the override
fabric.Object.prototype.toObject = function (_) {
return originalToObject.call(this, inclusionProps);
}
const resetToObjectCall = () => fabric.Object.prototype.toObject = originalToObject;

//from loadFromJSON implementation in fabricJS
const _this = this.canvas, self = this;
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -1792,12 +1766,36 @@ window.OSDAnnotations = class extends XOpatModuleSingleton {
return resolve();
});
}, reviver);
}).then(resetToObjectCall).catch(e => {
resetToObjectCall();
throw e;
}); //todo rethrow? rewrite as async call with try finally
});
}

_edgesMouseNavigation(e) {
if (this.mode !== this.Modes.AUTO) {

const edgeThreshold = 20;
const mouseX = e.clientX;
const mouseY = e.clientY;

const nearLeftEdge = mouseX >= 0 && edgeThreshold - mouseX;
const nearTopEdge = mouseY >= 0 && edgeThreshold / 2 - mouseY; //top edge near
const nearRightEdge = mouseX - window.innerWidth + edgeThreshold;
const nearBottomEdge = mouseY - window.innerHeight + edgeThreshold;

if (
(nearTopEdge < edgeThreshold && nearTopEdge > 0) ||
(nearRightEdge < edgeThreshold && nearRightEdge > 0) ||
(nearBottomEdge < edgeThreshold && nearBottomEdge > 0) ||
(nearLeftEdge < edgeThreshold && nearLeftEdge > 0)
) {
const center = VIEWER.viewport.getCenter(true);
const current = VIEWER.viewport.windowToViewportCoordinates(new OpenSeadragon.Point(e.x, e.y));
let direction = current.minus(center);
direction = direction.divide(Math.sqrt(Math.pow(direction.x, 2) + Math.pow(direction.y, 2)));
VIEWER.viewport.panTo(direction.times(0.004 / VIEWER.scalebar.imagePixelSizeOnScreen()).plus(center));
}
}
};

// Copied out of OpenSeadragon private code scope to allow manual scroll navigation
_fireMouseWheelNavigation(event) {
// Simulate a 'wheel' event
Expand Down
6 changes: 4 additions & 2 deletions modules/annotations/objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ OSDAnnotations.AnnotationObjectFactory = class {
"sessionID",
"presetID",
"layerID",
"id"
"id",
"author",
"created",
];

/**
* Properties copied with 'necessary' (+exports())
* Properties copied with 'necessary' (+exports()), subset of copiedProperties
* @type {string[]}
*/
static necessaryProperties = [
Expand Down
27 changes: 13 additions & 14 deletions modules/annotations/presets.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ OSDAnnotations.PresetManager = class {
*/
isUnusedPreset(p) {
return !p._used && p.objectFactory == this._context.polygonFactory
&& p.meta.category?.value === ""
&& !p.meta.category?.value
&& Object.keys(p.meta).length === 1;
}

Expand Down Expand Up @@ -416,13 +416,12 @@ OSDAnnotations.PresetManager = class {
async import(presets, clear=false) {
const _this = this;

if (clear) {
for (let pid in this._presets) {
const preset = this._presets[pid];
if (this.isUnusedPreset(preset)) {
this._context.raiseEvent('preset-delete', {preset});
delete this._presets[pid];
}
for (let pid in this._presets) {
const preset = this._presets[pid];
// TODO: clear might remove presets that are attached to existing annotations!
if (clear || this.isUnusedPreset(preset)) {
this._context.raiseEvent('preset-delete', {preset});
delete this._presets[pid];
}
}

Expand Down Expand Up @@ -530,12 +529,12 @@ OSDAnnotations.PresetManager = class {
let f = h * 6 - i;
let q = 1 - f;
switch(i % 6){
case 0: r = 1; g = f; b = 0; break;
case 1: r = q; g = 1; b = 0; break;
case 2: r = 0; g = 1; b = f; break;
case 3: r = 0; g = q; b = 1; break;
case 4: r = f; g = 0; b = 1; break;
case 5: r = 1; g = 0; b = q; break;
case 0: r = 1; g = f; b = 0; break;
case 1: r = q; g = 1; b = 0; break;
case 2: r = 0; g = 1; b = f; break;
case 3: r = 0; g = q; b = 1; break;
case 4: r = f; g = 0; b = 1; break;
case 5: r = 1; g = 0; b = q; break;
}
let c = "#" + ("00" + (~ ~(r * 255)).toString(16)).slice(-2)
+ ("00" + (~ ~(g * 255)).toString(16)).slice(-2)
Expand Down
12 changes: 7 additions & 5 deletions modules/empaia-wsi-tile-source/tile-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ OpenSeadragon.EmpaiaStandaloneV3TileSource = class extends OpenSeadragon.TileSou
*/
supports( data, url ) {
if (url && Array.isArray(data)) {
//multi-tile or single tile access
let match = url.match(/^(\/?[^\/].*\/v3\/files)\/info/i);
//multi-tile or single tile access, batch is old name on the api
let match = url.match(/^(\/?[^\/].*\/v3\/)(files|batch)\/info/i);
if (match) {
data = data || [{}];
data[0].tilesUrl = match[1];
data[0].tilesUrl = match[1] + match[2];
return true;
}
} else if (url && typeof data === "object") {
Expand Down Expand Up @@ -211,10 +211,12 @@ OpenSeadragon.EmpaiaStandaloneV3TileSource = class extends OpenSeadragon.TileSou

if (this.multifetch) {
//endpoint files/tile/level/[L]/tile/[X]/[Y]/?paths=path,list,separated,by,commas
return `${tiles}/tile/level/${level}/tile/${x}/${y}?slides=${this.fileId}`
const query_name = tiles.endsWith("batch") ? "slides" : "paths";
return `${tiles}/tile/level/${level}/tile/${x}/${y}?${query_name}=${this.fileId}`
}
//endpoint slides/[SLIDE]/tile/level/[L]/tile/[X]/[Y]/
return `${tiles}/tile/level/${level}/tile/${x}/${y}?slide=${this.fileId}`
const query_name = tiles.endsWith("batch") ? "slides" : "slide_id";
return `${tiles}/tile/level/${level}/tile/${x}/${y}?${query_name}=${this.fileId}`
}

_setDownloadHandler(isMultiplex) {
Expand Down
4 changes: 2 additions & 2 deletions modules/empation-api/empationapi.js

Large diffs are not rendered by default.

Loading

0 comments on commit 9e81765

Please sign in to comment.