From 4a913c5b839ef830520c83872ec0eae08505602e Mon Sep 17 00:00:00 2001 From: naglepuff Date: Wed, 22 Dec 2021 17:11:47 -0500 Subject: [PATCH 01/14] (WIP) Display image overlay annotations --- .../web_client/annotations/convertFeatures.js | 28 ++++++++++++++++++ .../web_client/models/AnnotationModel.js | 10 +++++++ .../views/imageViewerWidget/geojs.js | 29 ++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js index 2c9f69d1e..4c41bf1b0 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js +++ b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js @@ -1,3 +1,5 @@ +// import { restRequest } from '@girder/core/rest'; + /** * Create a color table that can be used for a heatmap. * @@ -92,6 +94,32 @@ function convertHeatmap(record, properties, layer) { return [heatmap]; } +/** + * Convert an image overlay annotation to a geojs feature. + * + * @param record: the image overlay annotation element. + * @param properties: a property map of additional data, such as the original + * annotation id. + * @param layer: the layer where this may be added. + */ +// function convertImageOverlay(record, properties, layer) { + // /* Image overlays need to be in their own layer */ + // const overlayItemId = record.girderId; + + // // Get overlay overlay metadata to inform layer params + // restRequest({ + // url: `item/${overlayItemId}/tiles` + // }).done((response) => { + // const map = layer.map(); + // const geo = window.geo; + // let params = geo.util.pixelCoordinateParams( + // map.node(), response.sizeX, response.sizeY, response.tileHeight, response.tileWidth + // ); + // return params; + // }).fail((response) => { + // }); +// } + /** * Convert a griddata heatmap annotation to a geojs feature. * diff --git a/girder_annotation/girder_large_image_annotation/web_client/models/AnnotationModel.js b/girder_annotation/girder_large_image_annotation/web_client/models/AnnotationModel.js index 6d1049c41..4990ba8ac 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/models/AnnotationModel.js +++ b/girder_annotation/girder_large_image_annotation/web_client/models/AnnotationModel.js @@ -361,6 +361,16 @@ export default AccessControlledModel.extend({ return convertFeatures(elements, {annotation: this.id}, layer); }, + /** + * Return annotation elements that cannot be represented as geojs + * features, such as image overlays. + */ + overlays() { + const json = this.get('annotation') || {}; + const elements = json.elements || []; + return elements.filter((element) => element.type === 'imageoverlay'); + }, + /** * Set the view. If we are paging elements, possibly refetch the elements. * Callers should listen for the g:fetched event to know when new elements diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index e42c309c6..830fd0363 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -4,6 +4,7 @@ import Backbone from 'backbone'; import events from '@girder/core/events'; import { wrap } from '@girder/core/utilities/PluginUtils'; +import { restRequest } from '@girder/core/rest'; import convertAnnotation from '../../annotations/geojs/convert'; @@ -93,11 +94,18 @@ var GeojsImageViewerWidgetExtension = function (viewer) { centroidFeature = feature; } }); + let overlayLayerIds = this._annotations[annotation.id].overlays.map((overlay) => overlay.id); + let overlayLayers = this.viewer.layers().filter((layer) => overlayLayerIds.contains(layer.id())); + for (const layer of overlayLayers) { + this.viewer.deleteLayer(layer); + } } + let overlays = annotation.overlays(); this._annotations[annotation.id] = { features: centroidFeature ? [centroidFeature] : [], options: options, - annotation: annotation + annotation: annotation, + overlays: overlays }; if (options.fetch && (!present || annotation.refresh() || annotation._inFetch === 'centroids')) { annotation.off('g:fetched', null, this).on('g:fetched', () => { @@ -190,6 +198,25 @@ var GeojsImageViewerWidgetExtension = function (viewer) { } }); } + // draw overlays + _.each(this._annotations[annotation.id].overlays, (overlay) => { + const overlayItemId = overlay.girderId; + restRequest({ + url: `item/${overlayItemId}/tiles` + }).done((response) => { + let params = geo.util.pixelCoordinateParams( + this.viewer.node(), response.sizeX, response.sizeY, response.tileHeight, response.tileWidth + ); + params.layer.useCredentials = true; + params.layer.url = `api/v1/item/${overlayItemId}/tiles/zxy/{z}/{x}/{y}`; + params.layer.autoshareRenderer = false; + params.layer.opacity = overlay.opacity || 1; + let overlayLayer = this.viewer.createLayer('osm', params.layer); + console.log({ overlayLayer }); + }).fail((response) => { + console.log({ response }); + }); + }); this._featureOpacity[annotation.id] = {}; geo.createFileReader('jsonReader', {layer: this.featureLayer}) .read(geojson, (features) => { From c9fdce4942d25a5ebbfd8c69ac91828beb82148d Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 23 Dec 2021 12:02:51 -0500 Subject: [PATCH 02/14] Include overlays in removeAnnotation function --- .../web_client/views/imageViewerWidget/geojs.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 830fd0363..3e90c90bf 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -212,9 +212,10 @@ var GeojsImageViewerWidgetExtension = function (viewer) { params.layer.autoshareRenderer = false; params.layer.opacity = overlay.opacity || 1; let overlayLayer = this.viewer.createLayer('osm', params.layer); - console.log({ overlayLayer }); + overlayLayer.id(overlay.id); + this.viewer.scheduleAnimationFrame(this.viewer.draw, true); }).fail((response) => { - console.log({ response }); + console.error(`There was an error overlaying image with ID ${overlayItemId}`); }); }); this._featureOpacity[annotation.id] = {}; @@ -429,6 +430,11 @@ var GeojsImageViewerWidgetExtension = function (viewer) { this.featureLayer.deleteFeature(feature); } }); + _.each(this._annotations[annotation.id].overlays, (overlay) => { + const overlayLayer = this.viewer.layers().find( + (layer) => layer.id() === overlay.id); + this.viewer.deleteLayer(overlayLayer); + }); delete this._annotations[annotation.id]; delete this._featureOpacity[annotation.id]; this.viewer.scheduleAnimationFrame(this.viewer.draw); From 8409bfe2b4ad17334fe4d98f6088c3e574831477 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 23 Dec 2021 16:52:13 -0500 Subject: [PATCH 03/14] Apply image overlay transform using proj strings Also, get rid of some code that will not be used in convertFeatures.js. --- .../web_client/annotations/convertFeatures.js | 26 ------------------- .../views/imageViewerWidget/geojs.js | 19 +++++++++++++- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js index 4c41bf1b0..a6374c1bc 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js +++ b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js @@ -94,32 +94,6 @@ function convertHeatmap(record, properties, layer) { return [heatmap]; } -/** - * Convert an image overlay annotation to a geojs feature. - * - * @param record: the image overlay annotation element. - * @param properties: a property map of additional data, such as the original - * annotation id. - * @param layer: the layer where this may be added. - */ -// function convertImageOverlay(record, properties, layer) { - // /* Image overlays need to be in their own layer */ - // const overlayItemId = record.girderId; - - // // Get overlay overlay metadata to inform layer params - // restRequest({ - // url: `item/${overlayItemId}/tiles` - // }).done((response) => { - // const map = layer.map(); - // const geo = window.geo; - // let params = geo.util.pixelCoordinateParams( - // map.node(), response.sizeX, response.sizeY, response.tileHeight, response.tileWidth - // ); - // return params; - // }).fail((response) => { - // }); -// } - /** * Convert a griddata heatmap annotation to a geojs feature. * diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 3e90c90bf..caf1e2e9b 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -56,6 +56,21 @@ var GeojsImageViewerWidgetExtension = function (viewer) { annotationAPI: _.constant(true), + /** + * Given an image overlay annotation element, compute and return + * a proj-string representation of its transform specification. + * @param {object} overlay An imageoverlay annotation element. + * @returns a proj-string representing how to overlay should be tranformed. + */ + _getOverlayTransformProjString: function (overlay) { + const transformInfo = overlay.transform || {}; + const xOffset = transformInfo.xoffset || 0; + const yOffset = transformInfo.yoffset || 0; + const matrix = transformInfo.matrix || [[1, 0], [0, 1]]; + return `+proj=longlat +axis=enu +s11=${1 / matrix[0][0]} +s12=${matrix[0][1]}` + + ` +s21=${matrix[1][0]} +s22=${ 1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; + }, + /** * Render an annotation model on the image. Currently, this is limited * to annotation types that can be (1) directly converted into geojson @@ -211,8 +226,10 @@ var GeojsImageViewerWidgetExtension = function (viewer) { params.layer.url = `api/v1/item/${overlayItemId}/tiles/zxy/{z}/{x}/{y}`; params.layer.autoshareRenderer = false; params.layer.opacity = overlay.opacity || 1; - let overlayLayer = this.viewer.createLayer('osm', params.layer); + const overlayLayer = this.viewer.createLayer('osm', params.layer); overlayLayer.id(overlay.id); + const proj = this._getOverlayTransformProjString(overlay); + overlayLayer.gcs(proj); this.viewer.scheduleAnimationFrame(this.viewer.draw, true); }).fail((response) => { console.error(`There was an error overlaying image with ID ${overlayItemId}`); From 084f76eaee00f31ade51bb46c96e7bca6854ba73 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Tue, 28 Dec 2021 11:05:07 -0500 Subject: [PATCH 04/14] Fix some lint/testing issues --- .../web_client/views/imageViewerWidget/geojs.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index caf1e2e9b..a26ff27c2 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -68,7 +68,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { const yOffset = transformInfo.yoffset || 0; const matrix = transformInfo.matrix || [[1, 0], [0, 1]]; return `+proj=longlat +axis=enu +s11=${1 / matrix[0][0]} +s12=${matrix[0][1]}` + - ` +s21=${matrix[1][0]} +s22=${ 1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; + ` +s21=${matrix[1][0]} +s22=${1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; }, /** @@ -109,11 +109,11 @@ var GeojsImageViewerWidgetExtension = function (viewer) { centroidFeature = feature; } }); - let overlayLayerIds = this._annotations[annotation.id].overlays.map((overlay) => overlay.id); - let overlayLayers = this.viewer.layers().filter((layer) => overlayLayerIds.contains(layer.id())); - for (const layer of overlayLayers) { - this.viewer.deleteLayer(layer); - } + _.each(this._annotations[annotation.id].overlays, (overlay) => { + const overlayLayer = this.viewer.layers().find( + (layer) => layer.id() === overlay.id); + this.viewer.deleteLayer(overlayLayer); + }); } let overlays = annotation.overlays(); this._annotations[annotation.id] = { From 1cd28810743a4fc14ca330a0c468a8c9d9c8c795 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Wed, 29 Dec 2021 12:57:46 -0500 Subject: [PATCH 05/14] Unclamp map bounds if image overlays are present --- .../web_client/views/imageViewerWidget/geojs.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index a26ff27c2..704a32fa6 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -115,7 +115,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { this.viewer.deleteLayer(overlayLayer); }); } - let overlays = annotation.overlays(); + const overlays = annotation.overlays() || []; this._annotations[annotation.id] = { features: centroidFeature ? [centroidFeature] : [], options: options, @@ -214,6 +214,10 @@ var GeojsImageViewerWidgetExtension = function (viewer) { }); } // draw overlays + if (this._annotations[annotation.id].overlays.length > 0) { + this.viewer.clampBoundsY(false); + this.viewer.clampBoundsX(false); + } _.each(this._annotations[annotation.id].overlays, (overlay) => { const overlayItemId = overlay.girderId; restRequest({ @@ -454,6 +458,8 @@ var GeojsImageViewerWidgetExtension = function (viewer) { }); delete this._annotations[annotation.id]; delete this._featureOpacity[annotation.id]; + this.viewer.clampBoundsY(true); + this.viewer.clampBoundsX(true); this.viewer.scheduleAnimationFrame(this.viewer.draw); } }, From a20f3ceff1326e40bfe2b0235caf1db4004a8dec Mon Sep 17 00:00:00 2001 From: naglepuff Date: Mon, 3 Jan 2022 12:57:45 -0500 Subject: [PATCH 06/14] Create test for overlay annotations (WIP) Fix test overlay element JSON (WIP) Fix creating test overlay annovation (WIP) Move test overlay to setup section Fix test for drawing overlay annotations --- .../web_client_specs/geojsSpec.js | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/girder_annotation/test_annotation/web_client_specs/geojsSpec.js b/girder_annotation/test_annotation/web_client_specs/geojsSpec.js index 6e2658ad2..aa0b99486 100644 --- a/girder_annotation/test_annotation/web_client_specs/geojsSpec.js +++ b/girder_annotation/test_annotation/web_client_specs/geojsSpec.js @@ -9,7 +9,7 @@ girderTest.addScripts([ girderTest.startApp(); $(function () { - var itemId, annotationId, interactor; + var itemId, annotationId, overlayAnnotationId, interactor; function closeTo(a, b, tol) { var i; @@ -83,10 +83,42 @@ $(function () { expect(annotationId).toBeDefined(); }); }); + it('upload test overlay annotation', function () { + runs(function () { + girder.rest.restRequest({ + url: 'annotation?itemId=' + itemId, + contentType: 'application/json', + processData: false, + type: 'POST', + data: JSON.stringify({ + name: 'test overlay annotation', + elements: [{ + type: 'imageoverlay', + girderId: itemId, + opacity: 0.5, + transform: { + 'xoffset': 10, + 'yoffset': 15 + } + }] + }) + }).then(function (resp) { + overlayAnnotationId = resp._id; + return null; + }); + }); + waitsFor(function () { + return overlayAnnotationId !== undefined; + }); + girderTest.waitForLoad(); + runs(function () { + expect(overlayAnnotationId).toBeDefined(); + }); + }); }); describe('Geojs viewer', function () { - var girder, large_image, $el, GeojsViewer, viewer, annotation, featureSpy, largeImageAnnotation; + var girder, large_image, $el, GeojsViewer, viewer, annotation, overlayAnnotation, featureSpy, largeImageAnnotation; beforeEach(function () { girder = window.girder; @@ -155,6 +187,26 @@ $(function () { }); }); + it('draw overlay', function() { + runs(function() { + overlayAnnotation = new largeImageAnnotation.models.AnnotationModel({ + _id: overlayAnnotationId + }); + overlayAnnotation.fetch(); + }) + girderTest.waitForLoad(); + runs(function () { + projStringSpy = sinon.spy(viewer, '_getOverlayTransformProjString'); + viewer.drawAnnotation(overlayAnnotation); + }); + girderTest.waitForLoad(); + runs(function () { + const annotationRecord = viewer._annotations[overlayAnnotationId] || undefined; + expect(annotationRecord).toBeDefined(); + expect(projStringSpy.callCount).toBe(1); + }); + }); + it('mouse over events', function () { var mouseon, mouseover, context = {}; runs(function () { @@ -262,6 +314,7 @@ $(function () { it('removeAnnotation', function () { viewer.removeAnnotation(annotation); + viewer.removeAnnotation(overlayAnnotation); expect(viewer._annotations).toEqual({}); sinon.assert.calledOnce(featureSpy); }); From 5b3f52b07123d3ef2a04327006a48e973b985469 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 6 Jan 2022 10:26:50 -0500 Subject: [PATCH 07/14] Don't specify full transform if not specified Also remove commented out import. --- .../web_client/annotations/convertFeatures.js | 2 -- .../web_client/views/imageViewerWidget/geojs.js | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js index a6374c1bc..2c9f69d1e 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js +++ b/girder_annotation/girder_large_image_annotation/web_client/annotations/convertFeatures.js @@ -1,5 +1,3 @@ -// import { restRequest } from '@girder/core/rest'; - /** * Create a color table that can be used for a heatmap. * diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 704a32fa6..96e7b350b 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -67,6 +67,9 @@ var GeojsImageViewerWidgetExtension = function (viewer) { const xOffset = transformInfo.xoffset || 0; const yOffset = transformInfo.yoffset || 0; const matrix = transformInfo.matrix || [[1, 0], [0, 1]]; + if (xOffset === 0 && yOffset === 0 && matrix === [[1, 0], [0, 1]]) { + return '+proj=longlat +axis=enu'; + } return `+proj=longlat +axis=enu +s11=${1 / matrix[0][0]} +s12=${matrix[0][1]}` + ` +s21=${matrix[1][0]} +s22=${1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; }, From dc210e3916757f29846e07f4a967fc68af70cdd6 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 6 Jan 2022 12:24:41 -0500 Subject: [PATCH 08/14] Add property to control clamping bounds New property `_unclampBoundsForOverlay` with getter/setter added to control whether or not the X/Y bounds should be clamped when rendering image overlays. Unclamping the bounds is useful because image overlays may be cut off by the viewer if they are larger than the base image layer. However, unclamped bounds eliminates the scroll out to center functionality. The default behavior at the time of this commit is to unclamp the bounds. --- .../views/imageViewerWidget/geojs.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 96e7b350b..4ca1920f9 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -26,6 +26,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { this._annotations = {}; this._featureOpacity = {}; + this._unclampBoundsForOverlay = true; this._globalAnnotationOpacity = settings.globalAnnotationOpacity || 1.0; this._globalAnnotationFillOpacity = settings.globalAnnotationFillOpacity || 1.0; this._highlightFeatureSizeLimit = settings.highlightFeatureSizeLimit || 10000; @@ -56,6 +57,23 @@ var GeojsImageViewerWidgetExtension = function (viewer) { annotationAPI: _.constant(true), + /** + * @returns whether to clamp viewer bounds when image overlays are + * rendered + */ + getUnclampBoundsForOverlay() { + return this._unclampBoundsForOverlay; + }, + + /** + * + * @param {bool} newValue Set whether to clamp viewer bounds when image + * overlays are rendered. + */ + setUnclampBoundsForOverlay(newValue) { + this._unclampBoundsForOverlay = newValue; + }, + /** * Given an image overlay annotation element, compute and return * a proj-string representation of its transform specification. @@ -217,7 +235,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { }); } // draw overlays - if (this._annotations[annotation.id].overlays.length > 0) { + if (this.getUnclampBoundsForOverlay() && this._annotations[annotation.id].overlays.length > 0) { this.viewer.clampBoundsY(false); this.viewer.clampBoundsX(false); } From 0bbbe0835248505fdfaec5283a5fe1342a8d73cb Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 6 Jan 2022 14:44:06 -0500 Subject: [PATCH 09/14] Use canvas to render overlays if many present --- .../views/imageViewerWidget/geojs.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 4ca1920f9..b203ea5d4 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -61,7 +61,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { * @returns whether to clamp viewer bounds when image overlays are * rendered */ - getUnclampBoundsForOverlay() { + getUnclampBoundsForOverlay: function () { return this._unclampBoundsForOverlay; }, @@ -70,7 +70,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { * @param {bool} newValue Set whether to clamp viewer bounds when image * overlays are rendered. */ - setUnclampBoundsForOverlay(newValue) { + setUnclampBoundsForOverlay: function (newValue) { this._unclampBoundsForOverlay = newValue; }, @@ -92,6 +92,19 @@ var GeojsImageViewerWidgetExtension = function (viewer) { ` +s21=${matrix[1][0]} +s22=${1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; }, + /** + * @returns The number of currently drawn overlay elements across + * all annotations. + */ + _countDrawnImageOverlays: function () { + let numOverlays = 0; + _.each(this._annotations, (value, key, obj) => { + let annotationOverlays = value.overlays || []; + numOverlays += annotationOverlays.length; + }); + return numOverlays; + }, + /** * Render an annotation model on the image. Currently, this is limited * to annotation types that can be (1) directly converted into geojson @@ -249,7 +262,11 @@ var GeojsImageViewerWidgetExtension = function (viewer) { ); params.layer.useCredentials = true; params.layer.url = `api/v1/item/${overlayItemId}/tiles/zxy/{z}/{x}/{y}`; - params.layer.autoshareRenderer = false; + if (this._countDrawnImageOverlays() <= 6) { + params.layer.autoshareRenderer = false; + } else { + params.layer.renderer = 'canvas'; + } params.layer.opacity = overlay.opacity || 1; const overlayLayer = this.viewer.createLayer('osm', params.layer); overlayLayer.id(overlay.id); From 2cf5c3f26be0337e294114aba95efc6594af5933 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 6 Jan 2022 16:37:41 -0500 Subject: [PATCH 10/14] Don't unconditionally re-clamp bounds --- .../web_client/views/imageViewerWidget/geojs.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index b203ea5d4..62fe383d5 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -496,8 +496,13 @@ var GeojsImageViewerWidgetExtension = function (viewer) { }); delete this._annotations[annotation.id]; delete this._featureOpacity[annotation.id]; - this.viewer.clampBoundsY(true); - this.viewer.clampBoundsX(true); + + // If removing an overlay annotation results in no more overlays drawn, and we've + // previously un-clamped bounds for overlays, re-clamp bounds + if (this._countDrawnImageOverlays() === 0 && this.getUnclampBoundsForOverlay()) { + this.viewer.clampBoundsY(true); + this.viewer.clampBoundsX(true); + } this.viewer.scheduleAnimationFrame(this.viewer.draw); } }, From 546ddf0718e6172574d03ccb6c0a79b3a83db4e7 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Tue, 11 Jan 2022 15:21:00 -0500 Subject: [PATCH 11/14] Fix proj string generation Remove console log statement --- .../views/imageViewerWidget/geojs.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 62fe383d5..5000a29a1 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -85,11 +85,22 @@ var GeojsImageViewerWidgetExtension = function (viewer) { const xOffset = transformInfo.xoffset || 0; const yOffset = transformInfo.yoffset || 0; const matrix = transformInfo.matrix || [[1, 0], [0, 1]]; - if (xOffset === 0 && yOffset === 0 && matrix === [[1, 0], [0, 1]]) { - return '+proj=longlat +axis=enu'; + const s11 = matrix[0][0]; + const s12 = matrix[0][1]; + const s21 = matrix[1][0]; + const s22 = matrix[1][1]; + let projString = '+proj=longlat +axis=enu'; + if (xOffset > 0) { + projString = projString + ` +xoff=-${xOffset}`; } - return `+proj=longlat +axis=enu +s11=${1 / matrix[0][0]} +s12=${matrix[0][1]}` + - ` +s21=${matrix[1][0]} +s22=${1 / matrix[1][1]} +xoff=-${xOffset} +yoff=${yOffset}`; + if (yOffset > 0) { + projString = projString + ` +yoff=${yOffset}`; + } + if (s11 !== 1 || s12 !== 0 || s21 !== 0 || s22 !== 1) { + // add affine matrix vals to projection string if not identity matrix + projString = projString + ` +s11=${1 / s11} +s12=${s12} +s21=${s21} +s22=${1 / s22}`; + } + return projString; }, /** From 3a5bb26ddc735a40a1dc7045b1e93b530aba0112 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Wed, 12 Jan 2022 17:04:38 -0500 Subject: [PATCH 12/14] Update layer params to account for zoom level Refactor generating layer params for overlay Fix layer params with different zoom levels --- .../views/imageViewerWidget/geojs.js | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index 5000a29a1..bbcc2c645 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -89,6 +89,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { const s12 = matrix[0][1]; const s21 = matrix[1][0]; const s22 = matrix[1][1]; + let projString = '+proj=longlat +axis=enu'; if (xOffset > 0) { projString = projString + ` +xoff=-${xOffset}`; @@ -116,6 +117,51 @@ var GeojsImageViewerWidgetExtension = function (viewer) { return numOverlays; }, + /** + * Generate layer parameters for an image overlay layer + * @param {object} overlayImageMetadata metadata such as size, tile size, and levels for the overlay image + * @param {string} overlayImageId ID of a girder image item + * @param {object} overlay information about the overlay such as opacity + * @returns layer params for the image overlay layer + */ + _generateOverlayLayerParams(overlayImageMetadata, overlayImageId, overlay) { + const geo = window.geo; + let params = geo.util.pixelCoordinateParams( + this.viewer.node(), overlayImageMetadata.sizeX, overlayImageMetadata.sizeY, overlayImageMetadata.tileHeight, overlayImageMetadata.tileWidth + ); + params.layer.useCredentials = true; + params.layer.url = `api/v1/item/${overlayImageId}/tiles/zxy/{z}/{x}/{y}`; + if (this._countDrawnImageOverlays() <= 6) { + params.layer.autoshareRenderer = false; + } else { + params.layer.renderer = 'canvas'; + } + params.layer.opacity = overlay.opacity || 1; + + if (this.levels !== overlayImageMetadata.levels) { + const levelDifference = Math.abs(this.levels - overlayImageMetadata.levels); + params.layer.url = (x, y, z) => 'api/v1/item/' + overlayImageId + `/tiles/zxy/${z - levelDifference}/${x}/${y}`; + params.layer.minLevel = levelDifference; + params.layer.maxLevel += levelDifference; + + params.layer.tilesMaxBounds = (level) => { + var scale = Math.pow(2, params.layer.maxLevel - level); + return { + x: Math.floor(overlayImageMetadata.sizeX / scale), + y: Math.floor(overlayImageMetadata.sizeY / scale) + }; + }; + params.layer.tilesAtZoom = (level) => { + var scale = Math.pow(2, params.layer.maxLevel - level); + return { + x: Math.ceil(overlayImageMetadata.sizeX / overlayImageMetadata.tileWidth / scale), + y: Math.ceil(overlayImageMetadata.sizeY / overlayImageMetadata.tileHeight / scale) + }; + }; + } + return params.layer; + }, + /** * Render an annotation model on the image. Currently, this is limited * to annotation types that can be (1) directly converted into geojson @@ -268,18 +314,8 @@ var GeojsImageViewerWidgetExtension = function (viewer) { restRequest({ url: `item/${overlayItemId}/tiles` }).done((response) => { - let params = geo.util.pixelCoordinateParams( - this.viewer.node(), response.sizeX, response.sizeY, response.tileHeight, response.tileWidth - ); - params.layer.useCredentials = true; - params.layer.url = `api/v1/item/${overlayItemId}/tiles/zxy/{z}/{x}/{y}`; - if (this._countDrawnImageOverlays() <= 6) { - params.layer.autoshareRenderer = false; - } else { - params.layer.renderer = 'canvas'; - } - params.layer.opacity = overlay.opacity || 1; - const overlayLayer = this.viewer.createLayer('osm', params.layer); + let params = this._generateOverlayLayerParams(response, overlayItemId, overlay); + const overlayLayer = this.viewer.createLayer('osm', params); overlayLayer.id(overlay.id); const proj = this._getOverlayTransformProjString(overlay); overlayLayer.gcs(proj); From 86cc2935791525f2350c3446f51d841db65f4b20 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 13 Jan 2022 13:18:30 -0500 Subject: [PATCH 13/14] Allow negative x/y offset for image overlays --- .../models/annotation.py | 6 ++---- .../web_client/views/imageViewerWidget/geojs.js | 11 +++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/models/annotation.py b/girder_annotation/girder_large_image_annotation/models/annotation.py index 39079a862..7c8d90a09 100644 --- a/girder_annotation/girder_large_image_annotation/models/annotation.py +++ b/girder_annotation/girder_large_image_annotation/models/annotation.py @@ -491,12 +491,10 @@ class AnnotationSchema: 'an X offset and a Y offset.', 'properties': { 'xoffset': { - 'type': 'number', - 'minimum': 0 + 'type': 'number' }, 'yoffset': { - 'type': 'number', - 'minimum': 0 + 'type': 'number' }, 'matrix': transformArray }, diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index bbcc2c645..c8ffba2d6 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -82,7 +82,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { */ _getOverlayTransformProjString: function (overlay) { const transformInfo = overlay.transform || {}; - const xOffset = transformInfo.xoffset || 0; + let xOffset = transformInfo.xoffset || 0; const yOffset = transformInfo.yoffset || 0; const matrix = transformInfo.matrix || [[1, 0], [0, 1]]; const s11 = matrix[0][0]; @@ -91,10 +91,13 @@ var GeojsImageViewerWidgetExtension = function (viewer) { const s22 = matrix[1][1]; let projString = '+proj=longlat +axis=enu'; - if (xOffset > 0) { - projString = projString + ` +xoff=-${xOffset}`; + if (xOffset !== 0) { + // negate x offset so positive values specified in the annotation + // move overlays to the right + xOffset = -1 * xOffset; + projString = projString + ` +xoff=${xOffset}`; } - if (yOffset > 0) { + if (yOffset !== 0) { projString = projString + ` +yoff=${yOffset}`; } if (s11 !== 1 || s12 !== 0 || s21 !== 0 || s22 !== 1) { From a1743d5854f40bd7219673a3432317272a801fad Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 13 Jan 2022 14:17:57 -0500 Subject: [PATCH 14/14] Fix various issues with layer param generation Don't use absolute value when determining the difference in levels between the base image and overlay image. Swap tileWidth and tileHeight in call to `pixelCoordinateParams`, as they were out of order. --- .../web_client/views/imageViewerWidget/geojs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js index c8ffba2d6..66c0f49b0 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/imageViewerWidget/geojs.js @@ -130,7 +130,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { _generateOverlayLayerParams(overlayImageMetadata, overlayImageId, overlay) { const geo = window.geo; let params = geo.util.pixelCoordinateParams( - this.viewer.node(), overlayImageMetadata.sizeX, overlayImageMetadata.sizeY, overlayImageMetadata.tileHeight, overlayImageMetadata.tileWidth + this.viewer.node(), overlayImageMetadata.sizeX, overlayImageMetadata.sizeY, overlayImageMetadata.tileWidth, overlayImageMetadata.tileHeight ); params.layer.useCredentials = true; params.layer.url = `api/v1/item/${overlayImageId}/tiles/zxy/{z}/{x}/{y}`; @@ -142,7 +142,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { params.layer.opacity = overlay.opacity || 1; if (this.levels !== overlayImageMetadata.levels) { - const levelDifference = Math.abs(this.levels - overlayImageMetadata.levels); + const levelDifference = this.levels - overlayImageMetadata.levels; params.layer.url = (x, y, z) => 'api/v1/item/' + overlayImageId + `/tiles/zxy/${z - levelDifference}/${x}/${y}`; params.layer.minLevel = levelDifference; params.layer.maxLevel += levelDifference;