diff --git a/girder_annotation/girder_large_image_annotation/models/annotationelement.py b/girder_annotation/girder_large_image_annotation/models/annotationelement.py index a3c9ceb80..77b867145 100644 --- a/girder_annotation/girder_large_image_annotation/models/annotationelement.py +++ b/girder_annotation/girder_large_image_annotation/models/annotationelement.py @@ -116,6 +116,8 @@ def getElements(self, annotation, region=None): The sum of the details values of the elements may exceed maxDetails slightly (the sum of all but the last element will be less than maxDetails, but the last element may exceed the value). + centroids: if specified and true, only return the id, center of the + bounding box, and bounding box size for each element. :param annotation: the annotation to get elements for. Modified. :param region: if present, a dictionary restricting which annotations @@ -128,8 +130,7 @@ def getElements(self, annotation, region=None): def yieldElements(self, annotation, region=None, info=None): """ - Given an annotation, fetch the elements from the database and add them - to it. + Given an annotation, fetch the elements from the database. When a region is used to request specific element, the following keys can be specified: left, right, top, bottom, low, high: the spatial area where @@ -149,10 +150,21 @@ def yieldElements(self, annotation, region=None, info=None): The sum of the details values of the elements may exceed maxDetails slightly (the sum of all but the last element will be less than maxDetails, but the last element may exceed the value). + centroids: if specified and true, only return the id, center of the + bounding box, and bounding box size for each element. :param annotation: the annotation to get elements for. Modified. :param region: if present, a dictionary restricting which annotations are returned. + :param info: an optional dictionary that will be modified with + additional query information, including count (total number of + available elements), returned (number of elements in response), + maxDetails (as specified by the region dictionary), details (sum of + details returned), limit (as specified by region), centroids (a + boolean based on the region specification). + :returns: a list of elements. If centroids were requested, each entry + is a list with str(id), x, y, size. Otherwise, each entry is the + element record. """ info = info if info is not None else {} region = region or {} @@ -176,9 +188,25 @@ def yieldElements(self, annotation, region=None, info=None): queryLimit = maxDetails if maxDetails and (not limit or maxDetails < limit) else limit offset = int(region['offset']) if region.get('offset') else 0 logger.debug('element query %r for %r', query, region) + fields = {'_id': True, 'element': True, 'bbox.details': True} + centroids = str(region.get('centroids')).lower() == 'true' + if centroids: + # fields = {'_id': True, 'element': True, 'bbox': True} + fields = { + '_id': True, + 'element.id': True, + 'bbox': True} + proplist = [] + propskeys = ['type', 'fillColor', 'lineColor', 'lineWidth', 'closed'] + for key in propskeys: + fields['element.%s' % key] = True + props = {} + info['centroids'] = True + info['props'] = proplist + info['propskeys'] = propskeys elementCursor = self.find( - query=query, sort=[(sortkey, sortdir)], limit=queryLimit, offset=offset, - fields={'_id': True, 'element': True, 'bbox.details': True}) + query=query, sort=[(sortkey, sortdir)], limit=queryLimit, + offset=offset, fields=fields) info.update({ 'count': elementCursor.count(), @@ -194,9 +222,26 @@ def yieldElements(self, annotation, region=None, info=None): for entry in elementCursor: element = entry['element'] element.setdefault('id', entry['_id']) - yield element + if centroids: + bbox = entry.get('bbox') + if not bbox or 'lowx' not in bbox or 'size' not in bbox: + continue + prop = tuple(element.get(key) for key in propskeys) + if prop not in props: + props[prop] = len(props) + proplist.append(list(prop)) + yield [ + str(element['id']), + (bbox['lowx'] + bbox['highx']) / 2, + (bbox['lowy'] + bbox['highy']) / 2, + bbox['size'] if entry.get('type') != 'point' else 0, + props[prop] + ] + details += 1 + else: + yield element + details += entry.get('bbox', {}).get('details', 1) count += 1 - details += entry.get('bbox', {}).get('details', 1) if maxDetails and details >= maxDetails: break info['returned'] = count @@ -299,6 +344,8 @@ def _boundingBox(self, element): bbox['size'] = ( (bbox['highy'] - bbox['lowy'])**2 + (bbox['highx'] - bbox['lowx'])**2) ** 0.5 + # we may want to store perimeter or area as that could help when we + # simplify to points return bbox def updateElements(self, annotation): diff --git a/girder_annotation/girder_large_image_annotation/rest/annotation.py b/girder_annotation/girder_large_image_annotation/rest/annotation.py index 8425334da..3b257e470 100644 --- a/girder_annotation/girder_large_image_annotation/rest/annotation.py +++ b/girder_annotation/girder_large_image_annotation/rest/annotation.py @@ -17,6 +17,7 @@ ############################################################################## import json +import struct import ujson import cherrypy @@ -138,6 +139,9 @@ def getAnnotationSchema(self, params): 'points are used to defined it. This is applied in addition ' 'to the limit. Using maxDetails helps ensure results will be ' 'able to be rendered.', required=False, dataType='int') + .param('centroids', 'If true, only return the centroids of each ' + 'element. The results are returned as a packed binary array ' + 'with a json wrapper.', dataType='boolean', required=False) .pagingParams(defaultSort='_id', defaultLimit=None, defaultSortDir=SortDir.ASCENDING) .errorResponse('ID was invalid.') @@ -174,6 +178,7 @@ def _getAnnotation(self, user, id, params): breakStr = b'"elements": [' base = json.dumps(annotation, sort_keys=True, allow_nan=False, cls=JsonEncoder).encode('utf8').split(breakStr) + centroids = str(params.get('centroids')).lower() == 'true' def generateResult(): info = {} @@ -181,12 +186,20 @@ def generateResult(): yield base[0] yield breakStr collect = [] + if centroids: + # Add a null byte to indicate the start of the binary data + yield b'\x00' for element in Annotationelement().yieldElements(annotation, params, info): # The json conversion is fastest if we use defaults as much as # possible. The only value in an annotation element that needs # special handling is the id, so cast that ourselves and then # use a json encoder in the most compact form. - element['id'] = str(element['id']) + if isinstance(element, dict): + element['id'] = str(element['id']) + else: + element = struct.pack( + '>QL', int(element[0][:16], 16), int(element[0][16:24], 16) + ) + struct.pack('= 100: - yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1] + if isinstance(collect[0], dict): + yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1] + else: + yield b''.join(collect) idx += 1 collect = [] if len(collect): - yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1] + if isinstance(collect[0], dict): + yield (b',' if idx else b'') + ujson.dumps(collect).encode('utf8')[1:-1] + else: + yield b''.join(collect) + if centroids: + # Add a final null byte to indicate the end of the binary data + yield b'\x00' yield base[1].rstrip().rstrip(b'}') yield b', "_elementQuery": ' yield json.dumps( info, sort_keys=True, allow_nan=False, cls=JsonEncoder).encode('utf8') yield b'}' - setResponseHeader('Content-Type', 'application/json') + if centroids: + setResponseHeader('Content-Type', 'application/octet-stream') + else: + setResponseHeader('Content-Type', 'application/json') return generateResult @describeRoute( 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 4817d7164..a25c9dfd4 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 @@ -5,6 +5,8 @@ import { restRequest } from '@girder/core/rest'; import ElementCollection from '../collections/ElementCollection'; import convert from '../annotations/convert'; +import style from '../annotations/style.js'; + /** * Define a backbone model representing an annotation. * An annotation contains zero or more "elements" or @@ -21,12 +23,14 @@ export default AccessControlledModel.extend({ resourceName: 'annotation', defaults: { - 'annotation': {} + annotation: {}, + maxDetails: 250000, + maxCentroids: 2000000 }, initialize() { this._region = { - maxDetails: 250000, + maxDetails: this.get('maxDetails'), sort: 'size', sortdir: -1 }; @@ -48,6 +52,105 @@ export default AccessControlledModel.extend({ }); }, + /** + * Fetch the centroids and unpack the bianry data. + */ + fetchCentroids: function () { + var url = (this.altUrl || this.resourceName) + '/' + this.get('_id'); + var restOpts = { + url: url, + data: {sort: 'size', sortdir: -1, centroids: true, limit: this.get('maxCentroids')}, + xhrFields: { + responseType: 'arraybuffer' + }, + error: null + }; + + return restRequest(restOpts).done((resp) => { + let dv = new DataView(resp); + let z0 = 0, z1 = dv.byteLength - 1; + for (; dv.getUint8(z0) && z0 < dv.byteLength; z0 += 1); + for (; dv.getUint8(z1) && z1 >= 0; z1 -= 1); + if (z0 >= z1) { + throw new Error('invalid centroid data'); + } + let json = new Uint8Array(z0 + dv.byteLength - z1 - 1); + json.set(new Uint8Array(resp.slice(0, z0)), 0); + json.set(new Uint8Array(resp.slice(z1 + 1)), z0); + let result = JSON.parse(decodeURIComponent(escape(String.fromCharCode.apply(null, json)))); + let defaults = { + default: { + fillColor: {r: 1, g: 120 / 255, b: 0}, + fillOpacity: 0.8, + strokeColor: {r: 0, g: 0, b: 0}, + strokeOpacity: 1, + strokeWidth: 1 + }, + rectangle: { + fillColor: {r: 176 / 255, g: 222 / 255, b: 92 / 255}, + strokeColor: {r: 153 / 255, g: 153 / 255, b: 153 / 255}, + strokeWidth: 2 + }, + polyline: { + strokeColor: {r: 1, g: 120 / 255, b: 0}, + strokeOpacity: 0.5, + strokeWidth: 4 + }, + polyline_closed: { + fillColor: {r: 176 / 255, g: 222 / 255, b: 92 / 255}, + strokeColor: {r: 153 / 255, g: 153 / 255, b: 153 / 255}, + strokeWidth: 2 + } + }; + result.props = result._elementQuery.props.map((props) => { + let propsdict = {}; + result._elementQuery.propskeys.forEach((key, i) => { + propsdict[key] = props[i]; + }); + Object.assign(propsdict, style(propsdict)); + let type = propsdict.type + (propsdict.closed ? '_closed' : ''); + ['fillColor', 'strokeColor', 'strokeWidth', 'fillOpacity', 'strokeOpacity'].forEach((key) => { + if (propsdict[key] === undefined) { + propsdict[key] = (defaults[type] || defaults.default)[key]; + } + if (propsdict[key] === undefined) { + propsdict[key] = defaults.default[key]; + } + }); + return propsdict; + }); + dv = new DataView(resp, z0 + 1, z1 - z0 - 1); + if (dv.byteLength !== result._elementQuery.returned * 28) { + throw new Error('invalid centroid data size'); + } + let centroids = { + id: new Array(result._elementQuery.returned), + x: new Float32Array(result._elementQuery.returned), + y: new Float32Array(result._elementQuery.returned), + r: new Float32Array(result._elementQuery.returned), + s: new Uint32Array(result._elementQuery.returned) + }; + let i, s; + for (i = s = 0; s < dv.byteLength; i += 1, s += 28) { + centroids.id[i] = + ('0000000' + dv.getUint32(s, false).toString(16)).substr(-8) + + ('0000000' + dv.getUint32(s + 4, false).toString(16)).substr(-8) + + ('0000000' + dv.getUint32(s + 8, false).toString(16)).substr(-8); + centroids.x[i] = dv.getFloat32(s + 12, true); + centroids.y[i] = dv.getFloat32(s + 16, true); + centroids.r[i] = dv.getFloat32(s + 20, true); + centroids.s[i] = dv.getUint32(s + 24, true); + } + result.centroids = centroids; + result.data = {length: result._elementQuery.returned}; + if (result._elementQuery.count > result._elementQuery.returned) { + result.partial = true; + } + this._centroids = result; + return result; + }); + }, + /** * Fetch a single resource from the server. Triggers g:fetched on success, * or g:error on error. @@ -64,7 +167,7 @@ export default AccessControlledModel.extend({ opts = opts || {}; var restOpts = { url: (this.altUrl || this.resourceName) + '/' + this.get('_id'), - /* Add out region request into the query */ + /* Add our region request into the query */ data: this._region }; if (opts.extraPath) { @@ -74,6 +177,11 @@ export default AccessControlledModel.extend({ restOpts.error = null; } this._inFetch = true; + if (this._refresh) { + delete this._pageElements; + delete this._centroids; + this._refresh = false; + } return restRequest(restOpts).done((resp) => { const annotation = resp.annotation || {}; const elements = annotation.elements || []; @@ -81,22 +189,45 @@ export default AccessControlledModel.extend({ this.set(resp); if (this._pageElements === undefined && resp._elementQuery) { this._pageElements = resp._elementQuery.count > resp._elementQuery.returned; + if (this._pageElements) { + this._inFetch = 'centroids'; + this.fetchCentroids().then(() => { + this._inFetch = true; + if (opts.extraPath) { + this.trigger('g:fetched.' + opts.extraPath); + } else { + this.trigger('g:fetched'); + } + return null; + }).always(() => { + this._inFetch = false; + if (this._nextFetch) { + var nextFetch = this._nextFetch; + this._nextFetch = null; + nextFetch(); + } + return null; + }); + } } - if (opts.extraPath) { - this.trigger('g:fetched.' + opts.extraPath); - } else { - this.trigger('g:fetched'); + if (this._inFetch !== 'centroids') { + if (opts.extraPath) { + this.trigger('g:fetched.' + opts.extraPath); + } else { + this.trigger('g:fetched'); + } } - this._elements.reset(elements, _.extend({sync: true}, opts)); }).fail((err) => { this.trigger('g:error', err); }).always(() => { - this._inFetch = false; - if (this._nextFetch) { - var nextFetch = this._nextFetch; - this._nextFetch = null; - nextFetch(); + if (this._inFetch !== 'centroids') { + this._inFetch = false; + if (this._nextFetch) { + var nextFetch = this._nextFetch; + this._nextFetch = null; + nextFetch(); + } } }); }, @@ -225,8 +356,10 @@ export default AccessControlledModel.extend({ * @param {number} maxZoom the maximum zoom factor. * @param {boolean} noFetch Truthy to not perform a fetch if the view * changes. + * @param {number} sizeX the maximum width to query. + * @param {number} sizeY the maximum height to query. */ - setView(bounds, zoom, maxZoom, noFetch) { + setView(bounds, zoom, maxZoom, noFetch, sizeX, sizeY) { if (this._pageElements === false || this.isNew()) { return; } @@ -245,16 +378,20 @@ export default AccessControlledModel.extend({ if (canskip && !this._inFetch) { return; } - this._region.left = bounds.left - xoverlap; - this._region.top = bounds.top - yoverlap; - this._region.right = bounds.right + xoverlap; - this._region.bottom = bounds.bottom + yoverlap; - /* ask for items that will be at least 0.5 pixels, minus a bit */ + var lastRegion = Object.assign({}, this._region); + this._region.left = Math.max(0, bounds.left - xoverlap); + this._region.top = Math.max(0, bounds.top - yoverlap); + this._region.right = Math.min(sizeX || 1e6, bounds.right + xoverlap); + this._region.bottom = Math.min(sizeY || 1e6, bounds.bottom + yoverlap); this._lastZoom = zoom; - this._region.minimumSize = Math.pow(2, maxZoom - zoom - 1) - 1; + /* Don't ask for a minimum size; we show centroids if the data is + * incomplete. */ if (noFetch) { return; } + if (['left', 'top', 'right', 'bottom', 'minumumSize'].every((key) => this._region[key] === lastRegion[key])) { + return; + } if (!this._nextFetch) { var nextFetch = () => { this.fetch(); 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 cda6032a0..c0ba0ff2b 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 @@ -78,17 +78,22 @@ var GeojsImageViewerWidgetExtension = function (viewer) { options = _.defaults(options || {}, {fetch: true}); var geojson = annotation.geojson(); var present = _.has(this._annotations, annotation.id); + var centroidFeature; if (present) { - _.each(this._annotations[annotation.id].features, (feature) => { - this.featureLayer.deleteFeature(feature); + _.each(this._annotations[annotation.id].features, (feature, idx) => { + if (idx || !annotation._centroids || feature.data().length !== annotation._centroids.data.length) { + this.featureLayer.deleteFeature(feature); + } else { + centroidFeature = feature; + } }); } this._annotations[annotation.id] = { - features: [], + features: centroidFeature ? [centroidFeature] : [], options: options, annotation: annotation }; - if (options.fetch && (!present || annotation.refresh())) { + if (options.fetch && (!present || annotation.refresh() || annotation._inFetch === 'centroids')) { annotation.off('g:fetched', null, this).on('g:fetched', () => { // Trigger an event indicating to the listener that // mouseover states should reset. @@ -99,9 +104,86 @@ var GeojsImageViewerWidgetExtension = function (viewer) { this.drawAnnotation(annotation); }, this); this.setBounds({[annotation.id]: this._annotations[annotation.id]}); + if (annotation._inFetch === 'centroids') { + return; + } } annotation.refresh(false); var featureList = this._annotations[annotation.id].features; + // draw centroids except for otherwise shown values + if (annotation._centroids && !centroidFeature) { + let feature = this.featureLayer.createFeature('point'); + featureList.push(feature); + feature.data(annotation._centroids.data).position((d, i) => ({ + x: annotation._centroids.centroids.x[i], + y: annotation._centroids.centroids.y[i] + })).style({ + radius: (d, i) => { + let r = annotation._centroids.centroids.r[i]; + if (!r) { + return 8; + } + // the given value is the diagonal of the bounding box, so + // to convert it to a circle radius means it must be + // divided by 2 or by 2 * 4/pi. + r /= 2.5 * this.viewer.unitsPerPixel(this.viewer.zoom()); + return r; + }, + stroke: (d, i) => { + return !annotation._shownIds || !annotation._shownIds.has(annotation._centroids.centroids.id[i]); + }, + strokeColor: (d, i) => { + let s = annotation._centroids.centroids.s[i]; + return annotation._centroids.props[s].strokeColor; + }, + strokeOpacity: (d, i) => { + let s = annotation._centroids.centroids.s[i]; + return annotation._centroids.props[s].strokeOpacity; + }, + strokeWidth: (d, i) => { + let s = annotation._centroids.centroids.s[i]; + return annotation._centroids.props[s].strokeWidth; + }, + fill: (d, i) => { + return !annotation._shownIds || !annotation._shownIds.has(annotation._centroids.centroids.id[i]); + }, + fillColor: (d, i) => { + let s = annotation._centroids.centroids.s[i]; + return annotation._centroids.props[s].fillColor; + }, + fillOpacity: (d, i) => { + let s = annotation._centroids.centroids.s[i]; + return annotation._centroids.props[s].fillOpacity; + } + }); + // bind an event so zoom updates radius + annotation._centroidLastZoom = undefined; + feature.geoOn(geo.event.pan, () => { + if (this.viewer.zoom() !== annotation._centroidLastZoom) { + annotation._centroidLastZoom = this.viewer.zoom(); + if (feature.verticesPerFeature) { + let scale = 2.5 * this.viewer.unitsPerPixel(this.viewer.zoom()); + let vpf = feature.verticesPerFeature(), + count = feature.data().length, + radius = new Float32Array(vpf * count); + for (var i = 0, j = 0; i < count; i += 1) { + let r = annotation._centroids.centroids.r[i]; + if (r) { + r /= scale; + } else { + r = 8; + } + for (var k = 0; k < vpf; k += 1, j += 1) { + radius[j] = r; + } + } + feature.updateStyleFromArray('radius', radius, true); + } else { + feature.modified().draw(); + } + } + }); + } this._featureOpacity[annotation.id] = {}; geo.createFileReader('jsonReader', {layer: this.featureLayer}) .read(geojson, (features) => { @@ -124,6 +206,9 @@ var GeojsImageViewerWidgetExtension = function (viewer) { // store the original opacities for the elements in each feature const data = feature.data(); + if (annotation._centroids) { + annotation._shownIds = new Set(feature.data().map((d) => d.id)); + } if (data.length <= this._highlightFeatureSizeLimit) { this._featureOpacity[annotation.id][feature.featureType] = feature.data() .map(({id, properties}) => { @@ -136,7 +221,28 @@ var GeojsImageViewerWidgetExtension = function (viewer) { } }); this._mutateFeaturePropertiesForHighlight(annotation.id, features); - this.viewer.scheduleAnimationFrame(this.viewer.draw); + if (annotation._centroids && featureList[0]) { + if (featureList[0].verticesPerFeature) { + this.viewer.scheduleAnimationFrame(() => { + let vpf = featureList[0].verticesPerFeature(), + count = featureList[0].data().length, + shown = new Float32Array(vpf * count); + for (let i = 0, j = 0; i < count; i += 1) { + let val = annotation._shownIds.has(annotation._centroids.centroids.id[i]) ? 0 : 1; + for (let k = 0; k < vpf; k += 1, j += 1) { + shown[j] = val; + } + } + featureList[0].updateStyleFromArray({ + stroke: shown, + fill: shown + }, undefined, true); + }); + } else { + featureList[0].modified(); + } + } + this.viewer.scheduleAnimationFrame(this.viewer.draw, true); }); }, @@ -231,7 +337,7 @@ var GeojsImageViewerWidgetExtension = function (viewer) { zoomRange = this.viewer.zoomRange(); _.each(annotations || this._annotations, (annotation) => { if (annotation.options.fetch && annotation.annotation.setView) { - annotation.annotation.setView(bounds, zoom, zoomRange.max); + annotation.annotation.setView(bounds, zoom, zoomRange.max, undefined, this.sizeX, this.sizeY); } }); }, diff --git a/girder_annotation/test_annotation/girder_utilities.py b/girder_annotation/test_annotation/girder_utilities.py index 5b9a0a6ae..a6e9e9881 100644 --- a/girder_annotation/test_annotation/girder_utilities.py +++ b/girder_annotation/test_annotation/girder_utilities.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import six from girder.models.folder import Folder from girder.models.upload import Upload @@ -38,3 +39,23 @@ def uploadTestFile(fileName, user, assetstore, folderName='Public', name=None): def respStatus(resp): return int(resp.output_status.split()[0]) + + +def getBody(response, text=True): + """ + Returns the response body as a text type or binary string. + + :param response: The response object from the server. + :param text: If true, treat the data as a text string, otherwise, treat + as binary. + """ + data = '' if text else b'' + + for chunk in response.body: + if text and isinstance(chunk, six.binary_type): + chunk = chunk.decode('utf8') + elif not text and not isinstance(chunk, six.binary_type): + chunk = chunk.encode('utf8') + data += chunk + + return data diff --git a/girder_annotation/test_annotation/test_annotations.py b/girder_annotation/test_annotation/test_annotations.py index 083e10e57..a1feefbb3 100644 --- a/girder_annotation/test_annotation/test_annotations.py +++ b/girder_annotation/test_annotation/test_annotations.py @@ -2,7 +2,6 @@ from bson import ObjectId import copy -import json import math import mock import pytest @@ -336,46 +335,6 @@ def testRevertVersion(self, admin): assert len(Annotation().revertVersion( annot['_id'], user=admin)['annotation']['elements']) == 1 - def testAnnotationsAfterCopyItem(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - item = Item().createItem('sample', admin, publicFolder) - Annotation().createAnnotation(item, admin, sampleAnnotation) - resp = server.request( - '/annotation', user=admin, params={'itemId': item['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 1 - resp = server.request( - '/item/%s/copy' % item['_id'], method='POST', user=admin) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation', user=admin, params={'itemId': resp.json['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 1 - resp = server.request( - '/item/%s/copy' % item['_id'], method='POST', user=admin, - params={'copyAnnotations': 'true'}) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation', user=admin, params={'itemId': resp.json['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 1 - resp = server.request( - '/item/%s/copy' % item['_id'], method='POST', user=admin, - params={'copyAnnotations': 'false'}) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation', user=admin, params={'itemId': resp.json['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 0 - resp = server.request( - '/item/%s/copy' % item['_id'], method='POST', user=admin, - params={'copyAnnotations': True}) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation', user=admin, params={'itemId': resp.json['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 1 - @pytest.mark.plugin('large_image_annotation') class TestLargeImageAnnotationElement(object): @@ -434,6 +393,7 @@ def testGetElements(self, admin): Annotationelement().getElements(annot) assert '_elementQuery' in annot assert len(annot['annotation']['elements']) == len(largeSample['elements']) # 7707 + assert 'centroids' not in annot['_elementQuery'] annot.pop('elements', None) annot.pop('_elementQuery', None) Annotationelement().getElements(annot, {'limit': 100}) @@ -468,10 +428,26 @@ def testGetElements(self, admin): Annotationelement().getElements(annot, { 'maxDetails': 300, 'sort': 'size', 'sortdir': 1}) elements = annot['annotation']['elements'] - elements = annot['annotation']['elements'] assert (elements[0]['width'] * elements[0]['height'] < elements[-1]['width'] * elements[-1]['height']) + def testGetElementsByCentroids(self, admin): + publicFolder = utilities.namedFolder(admin, 'Public') + item = Item().createItem('sample', admin, publicFolder) + largeSample = makeLargeSampleAnnotation() + # Use a copy of largeSample so we don't just have a referecne to it + annot = Annotation().createAnnotation(item, admin, largeSample.copy()) + # Clear existing element data, the get elements + annot.pop('elements', None) + annot.pop('_elementQuery', None) + Annotationelement().getElements(annot, {'centroids': True}) + assert '_elementQuery' in annot + assert len(annot['annotation']['elements']) == len(largeSample['elements']) # 7707 + assert annot['_elementQuery']['centroids'] is True + assert 'props' in annot['_elementQuery'] + elements = annot['annotation']['elements'] + assert isinstance(elements[0], list) + def testRemoveWithQuery(self, admin): publicFolder = utilities.namedFolder(admin, 'Public') item = Item().createItem('sample', admin, publicFolder) @@ -521,648 +497,6 @@ def testAnnotationGroup(self, admin): # updateElements -@pytest.mark.plugin('large_image_annotation') -class TestLargeImageAnnotationRest(object): - def testGetAnnotationSchema(self, server): - resp = server.request('/annotation/schema') - assert utilities.respStatus(resp) == 200 - assert '$schema' in resp.json - - def testGetAnnotation(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - item = Item().createItem('sample', admin, publicFolder) - annot = Annotation().createAnnotation(item, admin, sampleAnnotation) - annotId = str(annot['_id']) - resp = server.request(path='/annotation/%s' % annotId, user=admin) - assert utilities.respStatus(resp) == 200 - assert (resp.json['annotation']['elements'][0]['center'] == - annot['annotation']['elements'][0]['center']) - largeSample = makeLargeSampleAnnotation() - annot = Annotation().createAnnotation(item, admin, largeSample) - annotId = str(annot['_id']) - resp = server.request(path='/annotation/%s' % annotId, user=admin) - assert utilities.respStatus(resp) == 200 - assert (len(resp.json['annotation']['elements']) == - len(largeSample['elements'])) # 7707 - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'limit': 100, - }) - assert utilities.respStatus(resp) == 200 - assert len(resp.json['annotation']['elements']) == 100 - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'left': 3000, - 'right': 4000, - 'top': 4500, - 'bottom': 6500, - }) - assert utilities.respStatus(resp) == 200 - assert len(resp.json['annotation']['elements']) == 157 - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'left': 3000, - 'right': 4000, - 'top': 4500, - 'bottom': 6500, - 'minimumSize': 16, - }) - assert utilities.respStatus(resp) == 200 - assert len(resp.json['annotation']['elements']) == 39 - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'maxDetails': 300, - }) - assert utilities.respStatus(resp) == 200 - assert len(resp.json['annotation']['elements']) == 75 - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'maxDetails': 300, - 'sort': 'size', - 'sortdir': -1, - }) - assert utilities.respStatus(resp) == 200 - elements = resp.json['annotation']['elements'] - assert (elements[0]['width'] * elements[0]['height'] > - elements[-1]['width'] * elements[-1]['height']) - resp = server.request( - path='/annotation/%s' % annotId, user=admin, params={ - 'maxDetails': 300, - 'sort': 'size', - 'sortdir': 1, - }) - assert utilities.respStatus(resp) == 200 - elements = resp.json['annotation']['elements'] - assert (elements[0]['width'] * elements[0]['height'] < - elements[-1]['width'] * elements[-1]['height']) - - def testAnnotationCopy(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - # create annotation on an item - itemSrc = Item().createItem('sample', admin, publicFolder) - annot = Annotation().createAnnotation(itemSrc, admin, sampleAnnotation) - assert Annotation().load(annot['_id'], user=admin) is not None - - # Create a new item - itemDest = Item().createItem('sample', admin, publicFolder) - - # Copy the annotation from one item to an other - resp = server.request( - path='/annotation/{}/copy'.format(annot['_id']), - method='POST', - user=admin, - params={ - 'itemId': itemDest.get('_id') - } - ) - assert utilities.respStatus(resp) == 200 - itemDest = Item().load(itemDest.get('_id'), level=AccessType.READ) - - # Check if the annotation is in the destination item - resp = server.request( - path='/annotation', - method='GET', - user=admin, - params={ - 'itemId': itemDest.get('_id'), - 'name': 'sample' - } - ) - assert utilities.respStatus(resp) == 200 - assert resp.json is not None - - def testItemAnnotationEndpoints(self, server, user, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - # create two annotations on an item - itemSrc = Item().createItem('sample', admin, publicFolder) - annot = Annotation().createAnnotation(itemSrc, admin, sampleAnnotation) - annot = Annotation().setPublic(annot, False, True) - Annotation().createAnnotation(itemSrc, admin, sampleAnnotationEmpty) - # Get all annotations for that item as the user - resp = server.request( - path='/annotation/item/{}'.format(itemSrc['_id']), - user=user - ) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 1 - assert len(resp.json[0]['annotation']['elements']) == 0 - - # Get all annotations for that item as the admin - resp = server.request( - path='/annotation/item/{}'.format(itemSrc['_id']), - user=admin - ) - assert utilities.respStatus(resp) == 200 - annotList = resp.json - assert len(annotList) == 2 - assert (annotList[0]['annotation']['elements'][0]['center'] == - annot['annotation']['elements'][0]['center']) - assert len(annotList[1]['annotation']['elements']) == 0 - - # Create a new item - itemDest = Item().createItem('sample', admin, publicFolder) - - # Set the annotations on the new item - resp = server.request( - path='/annotation/item/{}'.format(itemDest['_id']), - method='POST', - user=admin, - type='application/json', - body=json.dumps(annotList) - ) - assert utilities.respStatus(resp) == 200 - assert resp.json == 2 - - # Check if the annotations are in the destination item - resp = server.request( - path='/annotation', - method='GET', - user=admin, - params={ - 'itemId': itemDest.get('_id'), - 'name': 'sample' - } - ) - assert utilities.respStatus(resp) == 200 - assert resp.json is not None - - # Check failure conditions - resp = server.request( - path='/annotation/item/{}'.format(itemDest['_id']), - method='POST', - user=admin, - type='application/json', - body=json.dumps(['not an object']) - ) - assert utilities.respStatus(resp) == 400 - resp = server.request( - path='/annotation/item/{}'.format(itemDest['_id']), - method='POST', - user=admin, - type='application/json', - body=json.dumps([{'key': 'not an annotation'}]) - ) - assert utilities.respStatus(resp) == 400 - - # Delete annotations - resp = server.request( - path='/annotation/item/{}'.format(itemDest['_id']), - method='DELETE', - user=None - ) - assert utilities.respStatus(resp) == 401 - - resp = server.request( - path='/annotation/item/{}'.format(itemDest['_id']), - method='DELETE', - user=admin - ) - assert utilities.respStatus(resp) == 200 - assert resp.json == 2 - - def testDeleteAnnotation(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - Setting().set(constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY, False) - item = Item().createItem('sample', admin, publicFolder) - annot = Annotation().createAnnotation(item, admin, sampleAnnotation) - annotId = str(annot['_id']) - assert Annotation().load(annot['_id'], user=admin) is not None - resp = server.request(path='/annotation/%s' % annotId, user=admin, method='DELETE') - assert utilities.respStatus(resp) == 200 - assert Annotation().load(annot['_id']) is None - - def testFindAnnotatedImages(self, server, user, admin, fsAssetstore): - - def create_annotation(item, user): - return str(Annotation().createAnnotation(item, user, sampleAnnotation)['_id']) - - def upload(name, user=user, private=False): - file = utilities.uploadExternalFile( - 'data/sample_image.ptif.sha512', admin, fsAssetstore, name=name) - item = Item().load(file['itemId'], level=AccessType.READ, user=admin) - - create_annotation(item, user) - create_annotation(item, user) - create_annotation(item, admin) - - return str(item['_id']) - - item1 = upload('image1-abcd.ptif', admin) - item2 = upload(u'Образец Картина.ptif') - item3 = upload('image3-ABCD.ptif') - item4 = upload('image3-ijkl.ptif', user, True) - - # test default search - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100 - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids, [item4, item3, item2 == item1] - - # test filtering by user - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100, - 'creatorId': user['_id'] - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids, [item4, item3 == item2] - - # test getting annotations without admin access - resp = server.request('/annotation/images', user=user, params={ - 'limit': 100 - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids, [item3, item2 == item1] - - # test sort direction - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100, - 'sortdir': 1 - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids, [item1, item2, item3 == item4] - - # test pagination - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 1 - }) - assert utilities.respStatus(resp) == 200 - assert resp.json[0]['_id'] == item4 - - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 1, - 'offset': 3 - }) - assert utilities.respStatus(resp) == 200 - assert resp.json[0]['_id'] == item1 - - # test filtering by image name - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100, - 'imageName': 'image3-aBcd.ptif' - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids == [item3] - - # test filtering by image name substring - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100, - 'imageName': 'aBc' - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids, [item3 == item1] - - # test filtering by image name with unicode - resp = server.request('/annotation/images', user=admin, params={ - 'limit': 100, - 'imageName': u'Картина' - }) - assert utilities.respStatus(resp) == 200 - ids = [image['_id'] for image in resp.json] - assert ids == [item2] - - def testCreateAnnotation(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - item = Item().createItem('sample', admin, publicFolder) - itemId = str(item['_id']) - - resp = server.request( - '/annotation', method='POST', user=admin, - params={'itemId': itemId}, type='application/json', - body=json.dumps(sampleAnnotation)) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation', method='POST', user=admin, - params={'itemId': itemId}, type='application/json', body='badJSON') - assert utilities.respStatus(resp) == 400 - resp = server.request( - '/annotation', method='POST', user=admin, - params={'itemId': itemId}, type='application/json', - body=json.dumps({'key': 'not an annotation'})) - assert utilities.respStatus(resp) == 400 - - def testUpdateAnnotation(self, server, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - item = Item().createItem('sample', admin, publicFolder) - annot = Annotation().createAnnotation(item, admin, sampleAnnotation) - import sys - sys.stderr.write('%r\n' % [annot]) # ##DWM:: - sys.stderr.write('%r\n' % [sampleAnnotation]) - resp = server.request(path='/annotation/%s' % annot['_id'], user=admin) - assert utilities.respStatus(resp) == 200 - annot = resp.json - annot['annotation']['elements'].extend([ - {'type': 'point', 'center': [20.0, 25.0, 0]}, - {'type': 'point', 'center': [10.0, 24.0, 0]}, - {'type': 'point', 'center': [25.5, 23.0, 0]}, - ]) - resp = server.request( - '/annotation/%s' % annot['_id'], method='PUT', user=admin, - type='application/json', body=json.dumps(annot['annotation'])) - assert utilities.respStatus(resp) == 200 - assert resp.json['annotation']['name'] == 'sample' - assert len(resp.json['annotation']['elements']) == 4 - # Test update without elements - annotNoElem = copy.deepcopy(annot) - del annotNoElem['annotation']['elements'] - annotNoElem['annotation']['name'] = 'newname' - resp = server.request( - '/annotation/%s' % annot['_id'], method='PUT', user=admin, - type='application/json', body=json.dumps(annotNoElem['annotation'])) - assert utilities.respStatus(resp) == 200 - assert resp.json['annotation']['name'] == 'newname' - assert 'elements' not in resp.json['annotation'] - # Test with passed item id - item2 = Item().createItem('sample2', admin, publicFolder) - resp = server.request( - '/annotation/%s' % annot['_id'], method='PUT', user=admin, - params={'itemId': item2['_id']}, type='application/json', - body=json.dumps(annot['annotation'])) - assert utilities.respStatus(resp) == 200 - assert resp.json['itemId'] == str(item2['_id']) - - def testAnnotationAccessControlEndpoints(self, server, user, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - # create an annotation - item = Item().createItem('userItem', user, publicFolder) - annot = Annotation().createAnnotation(item, admin, sampleAnnotation) - - # Try to get ACL's as a user - resp = server.request('/annotation/%s/access' % annot['_id'], user=user) - assert utilities.respStatus(resp) == 403 - - # Get the ACL's as an admin - resp = server.request('/annotation/%s/access' % annot['_id'], user=admin) - assert utilities.respStatus(resp) == 200 - access = dict(**resp.json) - - # Set the public flag to false and try to read as a user - resp = server.request( - '/annotation/%s/access' % annot['_id'], - method='PUT', - user=admin, - params={ - 'access': json.dumps(access), - 'public': False - } - ) - assert utilities.respStatus(resp) == 200 - resp = server.request( - '/annotation/%s' % annot['_id'], - user=user - ) - assert utilities.respStatus(resp) == 403 - # The admin should still be able to get the annotation with elements - resp = server.request( - '/annotation/%s' % annot['_id'], - user=admin - ) - assert utilities.respStatus(resp) == 200 - assert len(resp.json['annotation']['elements']) == 1 - - # Give the user admin access - access['users'].append({ - 'login': user['login'], - 'flags': [], - 'id': str(user['_id']), - 'level': AccessType.ADMIN - }) - resp = server.request( - '/annotation/%s/access' % annot['_id'], - method='PUT', - user=admin, - params={ - 'access': json.dumps(access) - } - ) - assert utilities.respStatus(resp) == 200 - - # Get the ACL's as a user - resp = server.request('/annotation/%s/access' % annot['_id'], user=user) - assert utilities.respStatus(resp) == 200 - - def testAnnotationHistoryEndpoints(self, server, user, admin): - privateFolder = utilities.namedFolder(admin, 'Private') - Setting().set(constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY, True) - item = Item().createItem('sample', admin, privateFolder) - # Create an annotation with some history - annot = Annotation().createAnnotation(item, admin, copy.deepcopy(sampleAnnotation)) - annot['annotation']['name'] = 'First Change' - annot['annotation']['elements'].extend([ - {'type': 'point', 'center': [20.0, 25.0, 0]}, - {'type': 'point', 'center': [10.0, 24.0, 0]}, - {'type': 'point', 'center': [25.5, 23.0, 0]}, - ]) - annot = Annotation().save(annot) - # simulate a concurrent save - dup = Annotation().findOne({'_id': annot['_id']}) - dup['_annotationId'] = dup.pop('_id') - dup['_active'] = False - Annotation().collection.insert_one(dup) - # Save again - annot['annotation']['name'] = 'Second Change' - annot['annotation']['elements'].pop(2) - annot = Annotation().save(annot) - - # Test the list of versions - resp = server.request('/annotation/%s/history' % annot['_id'], user=user) - assert utilities.respStatus(resp) == 200 - assert resp.json == [] - resp = server.request('/annotation/%s/history' % annot['_id'], user=admin) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 3 - versions = resp.json - - # Test getting a specific version - resp = server.request('/annotation/%s/history/%s' % ( - annot['_id'], versions[1]['_version']), user=user) - assert utilities.respStatus(resp) == 403 - resp = server.request('/annotation/%s/history/%s' % ( - annot['_id'], versions[1]['_version']), user=admin) - assert utilities.respStatus(resp) == 200 - assert resp.json['_annotationId'] == str(annot['_id']) - assert len(resp.json['annotation']['elements']) == 4 - resp = server.request('/annotation/%s/history/%s' % ( - annot['_id'], versions[0]['_version'] + 1), user=admin) - assert utilities.respStatus(resp) == 400 - - # Test revert - resp = server.request('/annotation/%s/history/revert' % ( - annot['_id']), method='PUT', user=user) - assert utilities.respStatus(resp) == 403 - resp = server.request( - '/annotation/%s/history/revert' % (annot['_id']), - method='PUT', user=admin, params={ - 'version': versions[0]['_version'] + 1 - }) - assert utilities.respStatus(resp) == 400 - resp = server.request( - '/annotation/%s/history/revert' % (annot['_id']), - method='PUT', user=admin, params={ - 'version': versions[1]['_version'] - }) - assert utilities.respStatus(resp) == 200 - loaded = Annotation().load(annot['_id'], user=admin) - assert len(loaded['annotation']['elements']) == 4 - - # Add tests for: - # find - - -@pytest.mark.plugin('large_image_annotation') -class TestLargeImageAnnotationElementGroups(object): - def makeAnnot(self, admin): - publicFolder = utilities.namedFolder(admin, 'Public') - - self.item = Item().createItem('sample', admin, publicFolder) - annotationModel = Annotation() - - self.noGroups = annotationModel.createAnnotation( - self.item, admin, - { - 'name': 'nogroups', - 'elements': [{ - 'type': 'rectangle', - 'center': [20.0, 25.0, 0], - 'width': 14.0, - 'height': 15.0, - }, { - 'type': 'rectangle', - 'center': [40.0, 15.0, 0], - 'width': 5.0, - 'height': 5.0 - }] - } - ) - - self.notMigrated = annotationModel.createAnnotation( - self.item, admin, - { - 'name': 'notmigrated', - 'elements': [{ - 'type': 'rectangle', - 'center': [20.0, 25.0, 0], - 'width': 14.0, - 'height': 15.0, - 'group': 'b' - }, { - 'type': 'rectangle', - 'center': [40.0, 15.0, 0], - 'width': 5.0, - 'height': 5.0, - 'group': 'a' - }] - } - ) - annotationModel.collection.update_one( - {'_id': self.notMigrated['_id']}, - {'$unset': {'groups': ''}} - ) - - self.hasGroups = annotationModel.createAnnotation( - self.item, admin, - { - 'name': 'hasgroups', - 'elements': [{ - 'type': 'rectangle', - 'center': [20.0, 25.0, 0], - 'width': 14.0, - 'height': 15.0, - 'group': 'a' - }, { - 'type': 'rectangle', - 'center': [40.0, 15.0, 0], - 'width': 5.0, - 'height': 5.0, - 'group': 'c' - }, { - 'type': 'rectangle', - 'center': [50.0, 10.0, 0], - 'width': 5.0, - 'height': 5.0 - }] - } - ) - - def testFindAnnotations(self, server, admin): - self.makeAnnot(admin) - resp = server.request('/annotation', user=admin, params={'itemId': self.item['_id']}) - assert utilities.respStatus(resp) == 200 - assert len(resp.json) == 3 - for annot in resp.json: - if annot['_id'] == str(self.noGroups['_id']): - assert annot['groups'] == [None] - elif annot['_id'] == str(self.notMigrated['_id']): - assert annot['groups'] == ['a', 'b'] - elif annot['_id'] == str(self.hasGroups['_id']): - assert annot['groups'] == ['a', 'c', None] - else: - raise Exception('Unexpected annot id') - - def testLoadAnnotation(self, server, admin): - self.makeAnnot(admin) - resp = server.request('/annotation/%s' % str(self.hasGroups['_id']), user=admin) - assert utilities.respStatus(resp) == 200 - assert resp.json['groups'] == ['a', 'c', None] - - def testCreateAnnotation(self, server, admin): - self.makeAnnot(admin) - annot = { - 'name': 'created', - 'elements': [{ - 'type': 'rectangle', - 'center': [20.0, 25.0, 0], - 'width': 14.0, - 'height': 15.0, - 'group': 'a' - }] - } - resp = server.request( - '/annotation', - user=admin, - method='POST', - params={'itemId': str(self.item['_id'])}, - type='application/json', - body=json.dumps(annot) - ) - assert utilities.respStatus(resp) == 200 - - resp = server.request('/annotation/%s' % resp.json['_id'], user=admin) - assert utilities.respStatus(resp) == 200 - assert resp.json['groups'] == ['a'] - - def testUpdateAnnotation(self, server, admin): - self.makeAnnot(admin) - annot = { - 'name': 'created', - 'elements': [{ - 'type': 'rectangle', - 'center': [20.0, 25.0, 0], - 'width': 14.0, - 'height': 15.0, - 'group': 'd' - }] - } - resp = server.request( - '/annotation/%s' % str(self.hasGroups['_id']), - user=admin, - method='PUT', - type='application/json', - body=json.dumps(annot) - ) - assert utilities.respStatus(resp) == 200 - - resp = server.request('/annotation/%s' % resp.json['_id'], user=admin) - assert utilities.respStatus(resp) == 200 - assert resp.json['groups'] == ['d'] - - @pytest.mark.plugin('large_image_annotation') class TestLargeImageAnnotationAccessMigration(object): def testMigrateAnnotationAccessControl(self, user, admin): diff --git a/girder_annotation/test_annotation/test_annotations_rest.py b/girder_annotation/test_annotation/test_annotations_rest.py new file mode 100644 index 000000000..2bcc5d03a --- /dev/null +++ b/girder_annotation/test_annotation/test_annotations_rest.py @@ -0,0 +1,730 @@ +# -*- coding: utf-8 -*- + +import copy +import json +import pytest +import struct + +from girder.constants import AccessType +from girder.models.item import Item +from girder.models.setting import Setting + +from girder_large_image_annotation.models.annotation import Annotation + +from girder_large_image import constants + +from . import girder_utilities as utilities +from .test_annotations import sampleAnnotationEmpty, sampleAnnotation, makeLargeSampleAnnotation + + +@pytest.mark.plugin('large_image_annotation') +class TestLargeImageAnnotationRest(object): + def testGetAnnotationSchema(self, server): + resp = server.request('/annotation/schema') + assert utilities.respStatus(resp) == 200 + assert '$schema' in resp.json + + def testGetAnnotation(self, server, admin): + publicFolder = utilities.namedFolder(admin, 'Public') + item = Item().createItem('sample', admin, publicFolder) + annot = Annotation().createAnnotation(item, admin, sampleAnnotation) + annotId = str(annot['_id']) + resp = server.request(path='/annotation/%s' % annotId, user=admin) + assert utilities.respStatus(resp) == 200 + assert (resp.json['annotation']['elements'][0]['center'] == + annot['annotation']['elements'][0]['center']) + largeSample = makeLargeSampleAnnotation() + annot = Annotation().createAnnotation(item, admin, largeSample) + annotId = str(annot['_id']) + resp = server.request(path='/annotation/%s' % annotId, user=admin) + assert utilities.respStatus(resp) == 200 + assert (len(resp.json['annotation']['elements']) == + len(largeSample['elements'])) # 7707 + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'limit': 100, + }) + assert utilities.respStatus(resp) == 200 + assert len(resp.json['annotation']['elements']) == 100 + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'left': 3000, + 'right': 4000, + 'top': 4500, + 'bottom': 6500, + }) + assert utilities.respStatus(resp) == 200 + assert len(resp.json['annotation']['elements']) == 157 + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'left': 3000, + 'right': 4000, + 'top': 4500, + 'bottom': 6500, + 'minimumSize': 16, + }) + assert utilities.respStatus(resp) == 200 + assert len(resp.json['annotation']['elements']) == 39 + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'maxDetails': 300, + }) + assert utilities.respStatus(resp) == 200 + assert len(resp.json['annotation']['elements']) == 75 + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'maxDetails': 300, + 'sort': 'size', + 'sortdir': -1, + }) + assert utilities.respStatus(resp) == 200 + elements = resp.json['annotation']['elements'] + assert (elements[0]['width'] * elements[0]['height'] > + elements[-1]['width'] * elements[-1]['height']) + resp = server.request( + path='/annotation/%s' % annotId, user=admin, params={ + 'maxDetails': 300, + 'sort': 'size', + 'sortdir': 1, + }) + assert utilities.respStatus(resp) == 200 + elements = resp.json['annotation']['elements'] + assert (elements[0]['width'] * elements[0]['height'] < + elements[-1]['width'] * elements[-1]['height']) + + def testGetAnnotationWithCentroids(self, server, admin): + publicFolder = utilities.namedFolder(admin, 'Public') + item = Item().createItem('sample', admin, publicFolder) + annot = Annotation().createAnnotation(item, admin, sampleAnnotation) + annotId = str(annot['_id']) + resp = server.request( + path='/annotation/%s' % annotId, user=admin, + params={'centroids': 'true'}, isJson=False) + assert utilities.respStatus(resp) == 200 + result = utilities.getBody(resp, text=False) + assert b'\x00' in result + elements = result.split(b'\x00', 1)[1].rsplit(b'\x00', 1)[0] + data = result.split(b'\x00', 1)[0] + result.rsplit(b'\x00', 1)[1] + data = json.loads(data.decode('utf8')) + assert len(data['_elementQuery']['props']) == 1 + assert len(elements) == 28 * 1 + x, y, r, s = struct.unpack('= '3.5' flake8-docstrings flake8-quotes pytest>=3.6 -pytest-cov==2.5 +pytest-cov>=2.6 pytest-girder>=3.0.3 pytest-xdist mock diff --git a/setup.cfg b/setup.cfg index 2df4b88e2..5da1a4bda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,14 +21,17 @@ cache_dir = build/pytest_cache testpaths = test girder/test_girder girder_annotation/test_annotation [coverage:paths] +# As of pytest-cov 2.6, all but the first source line is relative to the first +# source line. The first line is relative to the local path. Prior to 2.6, +# all lines were relative to the local path. source = large_image/ - girder/girder_large_image - girder_annotation/girder_large_image_annotation - sources/ - tasks/ - examples/ - build/tox/*/lib/*/site-packages/large_image/ + ../girder/girder_large_image + ../girder_annotation/girder_large_image_annotation + ../sources/ + ../tasks/ + ../examples/ + ../build/tox/*/lib/*/site-packages/large_image/ [coverage:run] data_file = build/coverage/.coverage