From 238a1b2a2681ee233265c5a8337536a4b1c360b6 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Fri, 8 Mar 2024 16:07:23 +0100 Subject: [PATCH 1/7] adding API to fetch images with footprint in a geo point within a determined distance --- app/api/images/images.list.controller.js | 153 ++++++++++++------ app/api/images/images.openapi.yml | 39 +++++ app/api/images/images.routes.js | 6 + .../images/schemas/image-bound.schema.json | 14 ++ 4 files changed, 160 insertions(+), 52 deletions(-) create mode 100644 app/api/images/schemas/image-bound.schema.json diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 626b59e..9df1b45 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -77,7 +77,7 @@ const getImages = async (req, orderkey, count = true) => { const query = req.query; // TODO add image width for media url const attributes = parseAttributes(query); - const orderBy = orderkey ? orderkey: query.sortKey; + const orderBy = orderkey ? orderkey : query.sortKey; let whereClauses = []; const states = query.state ? query.state : ['waiting_validation', 'validated']; @@ -93,7 +93,7 @@ const getImages = async (req, orderkey, count = true) => { if (!userHasRole(req, 'owner_validator', 'owner_admin', 'super_admin')) { throw authorizationError('Unpublished images can only be accessed by respective owners or super administrators'); } - const { where: scopeOwner } = getOwnerScope(req); + const { where: scopeOwner } = getOwnerScope(req); // retrieve only unpublished images whereClauses.push({ is_published: false }); // restrict to current owner @@ -141,12 +141,13 @@ const getImages = async (req, orderkey, count = true) => { } if (query.keyword) { - whereClauses.push({ [Op.or]: { - original_id: iLikeFormatter(query.keyword), - title: iLikeFormatter(query.keyword), - caption: iLikeFormatter(query.keyword) - } - }); + whereClauses.push({ + [Op.or]: { + original_id: iLikeFormatter(query.keyword), + title: iLikeFormatter(query.keyword), + caption: iLikeFormatter(query.keyword) + } + }); } if (query.user_id) { @@ -243,10 +244,10 @@ const getImages = async (req, orderkey, count = true) => { if (query.wkt_roi) { whereClauses.push({ location: wktFormatter( - query.wkt_roi, - query.intersect_location || false, - query.intersect_footprint || false - ) + query.wkt_roi, + query.intersect_location || false, + query.intersect_footprint || false + ) }); } } @@ -254,7 +255,7 @@ const getImages = async (req, orderkey, count = true) => { // Filter locked images if (query.only_unlocked) { - whereClauses.push( models.sequelize.literal( + whereClauses.push(models.sequelize.literal( "last_start IS NULL OR (EXTRACT(EPOCH FROM now())-EXTRACT(EPOCH FROM last_start))/60 >= 240" )); } @@ -299,20 +300,21 @@ const getImages = async (req, orderkey, count = true) => { } const orderById = [["id"]]; - includeOption = [{ - model: models.apriori_locations, - attributes: [ - [models.sequelize.literal("ST_X(geom)"), "longitude"], - [models.sequelize.literal("ST_Y(geom)"), "latitude"], - "exact" - ], - where: whereClauseApriori, - required: true, - duplicating: false, - order: orderBy === 'distance' ? orderByApriori : undefined - }, - includeCollectionFilter - ]; + includeOption = [ + { + model: models.apriori_locations, + attributes: [ + [models.sequelize.literal("ST_X(geom)"), "longitude"], + [models.sequelize.literal("ST_Y(geom)"), "latitude"], + "exact" + ], + where: whereClauseApriori, + required: true, + duplicating: false, + order: orderBy === 'distance' ? orderByApriori : undefined + }, + includeCollectionFilter + ]; const sequelizeQuery = { subQuery: false, attributes: attributes, @@ -329,7 +331,7 @@ const getImages = async (req, orderkey, count = true) => { where: { [Op.and]: whereClauses }, include: includeOption, distinct: 'images.id' - }) + }); response.count = countPromise return response; } else { @@ -358,11 +360,11 @@ const getImages = async (req, orderkey, count = true) => { } }; -exports.getList = utils.route(async (req, res) => { +exports.getList = utils.route(async (req, res) => { const images = await getImages(req); //Build media if (!req.query.attributes || req.query.attributes.includes('media')) { //only return media if no specific attributes requested or if media requested - if(images.rows) { + if (images.rows) { await mediaUtils.setListImageUrl(images.rows, /* image_width */ 200, /* image_height */ null); } images.rows.forEach((image) => { @@ -392,7 +394,7 @@ exports.getListMetadata = utils.route(async (req, res) => { } else if (user.hasRole('owner_admin') || user.hasRole('owner_validator')) { // An owner administrator or validator can only request image metadata for // their owner. - const accessibleOwnerIds = [ user.owner_id ]; + const accessibleOwnerIds = [user.owner_id]; ownerIds = requestedOwnerIds ? intersection(requestedOwnerIds, accessibleOwnerIds) : accessibleOwnerIds; if (requestedOwnerIds && ownerIds.length !== requestedOwnerIds.length) { throw authorizationError('An owner validator or administrator can only access metadata for images linked to the same owner'); @@ -416,27 +418,27 @@ exports.getListMetadata = utils.route(async (req, res) => { // Include geolocation information and toponyms if geolocalisation is true const includeGeoloc = [ - { - model: models.geolocalisations, - attributes: [ - [models.sequelize.literal("st_X(geolocalisation.location)"), "longitude"], - [models.sequelize.literal("st_Y(geolocalisation.location)"), "latitude"], - [models.sequelize.literal("st_Z(geolocalisation.location)"), "altitude"], - "azimuth", - "tilt", - "roll", - "focal", - [models.sequelize.literal("st_AsText(geolocalisation.location)"), "point"], - [models.sequelize.literal("st_AsText(geolocalisation.footprint)"), "footprint"], - ], - where: { state: 'validated' }, - required: false - }, - { - model: models.geometadata, - attributes: [], - required: false - }]; + { + model: models.geolocalisations, + attributes: [ + [models.sequelize.literal("st_X(geolocalisation.location)"), "longitude"], + [models.sequelize.literal("st_Y(geolocalisation.location)"), "latitude"], + [models.sequelize.literal("st_Z(geolocalisation.location)"), "altitude"], + "azimuth", + "tilt", + "roll", + "focal", + [models.sequelize.literal("st_AsText(geolocalisation.location)"), "point"], + [models.sequelize.literal("st_AsText(geolocalisation.footprint)"), "footprint"], + ], + where: { state: 'validated' }, + required: false + }, + { + model: models.geometadata, + attributes: [], + required: false + }]; const countTotal = await models.images.count({ where: cleanedWhere @@ -545,3 +547,50 @@ exports.getStats = utils.route(async (req, res) => { rows: result }); }); + +exports.getImagesBound = utils.route(async (req, res) => { + const query = req.query; + const attributes = parseAttributes(query); + const sequelizeQuery = { + subQuery: false, + attributes: attributes, + where: { + [Op.and]: [ + { state: 'validated' }, + + Sequelize.where( + Sequelize.fn( + 'ST_Contains', + Sequelize.col('images.footprint'), + Sequelize.fn( + 'ST_SetSRID', + Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), + 4326 + ) + ), + true + ), + + Sequelize.where( + Sequelize.fn( + 'ST_Distance', + Sequelize.cast( + Sequelize.fn('ST_SetSRID', Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), 4326), + 'geography' + ), + Sequelize.cast( + Sequelize.col('images.location'), + 'geography' + ) + ), + { + [Op.lte]: query.distance // Distance in meters (20 km = 20000 meters) + } + ), + ], + } + }; + + const images = await models.images.findAll(sequelizeQuery); + res.status(200).send(images); +}); diff --git a/app/api/images/images.openapi.yml b/app/api/images/images.openapi.yml index 683413b..5e32a74 100644 --- a/app/api/images/images.openapi.yml +++ b/app/api/images/images.openapi.yml @@ -490,3 +490,42 @@ $ref: "#/components/responses/RequestParametersValidationErrors" "404": $ref: "#/components/responses/NotFoundError" + +/images/boundingbox: + get: + summary: Get images where frootprint covers a geo point. + operationId: getImagesBound + parameters: + - $ref: "#/components/parameters/LanguageParameter" + - name: latitude + in: query + description: Latitude of the bounding box center. + example: 46.841 + required: true + schema: + type: number + - name: longitude + in: query + description: Longitude of the bounding box center. + example: 7.663 + required: true + schema: + type: number + - name: distance + in: query + description: Distance from the center to the bounding box edges (in kilometers). + example: 2 + required: true + schema: + type: number + responses: + "200": + description: Successful request. + content: + application/json: + schema: + $ref: "#/components/schemas/ImagesBound" + "400": + $ref: "#/components/responses/RequestParametersValidationErrors" + "404": + $ref: "#/components/responses/NotFoundError" diff --git a/app/api/images/images.routes.js b/app/api/images/images.routes.js index ab1994a..8d8b5dc 100644 --- a/app/api/images/images.routes.js +++ b/app/api/images/images.routes.js @@ -80,6 +80,12 @@ module.exports = () => { controller.getFootprint ); + // Get images where frootprint covers a geo point + router.get("/images/boundingbox", + validateDocumentedRequestParametersFor('GET', '/images/boundingbox'), + listController.getImagesBound + ); + // Update the state of an image. router.put("/images/:id/state", authenticate(), diff --git a/app/api/images/schemas/image-bound.schema.json b/app/api/images/schemas/image-bound.schema.json new file mode 100644 index 0000000..4b7035f --- /dev/null +++ b/app/api/images/schemas/image-bound.schema.json @@ -0,0 +1,14 @@ +{ + "$id": "ImagesBound", + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "rows": { + "type": "array", + "items": { + "$ref": "Image#" + } + } + }, + "additionalProperties": false +} From 456b07a5159217cd6dde0b0df5531d7169c5086b Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 11 Mar 2024 16:26:58 +0100 Subject: [PATCH 2/7] adding media obj for images and the other filter options in the API --- app/api/images/images.list.controller.js | 6 +++ app/api/images/images.openapi.yml | 52 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 9df1b45..c6709a9 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -592,5 +592,11 @@ exports.getImagesBound = utils.route(async (req, res) => { }; const images = await models.images.findAll(sequelizeQuery); + if (images) { + await mediaUtils.setListImageUrl(images, /* image_width */ 200, /* image_height */ null); + } + images.forEach((image) => { + delete image.dataValues.iiif_data; + }); res.status(200).send(images); }); diff --git a/app/api/images/images.openapi.yml b/app/api/images/images.openapi.yml index 5e32a74..11b66e8 100644 --- a/app/api/images/images.openapi.yml +++ b/app/api/images/images.openapi.yml @@ -497,6 +497,58 @@ operationId: getImagesBound parameters: - $ref: "#/components/parameters/LanguageParameter" + - $ref: "#/components/parameters/LimitParameter" + - $ref: "#/components/parameters/OffsetParameter" + - name: collection_id + in: query + description: Select only the images belonging to the collection(s) with the specified ID(s). + example: 3 + schema: + $ref: "#/components/schemas/ApiIdListParameter" + - name: date_shot_min + in: query + description: Select only images shot after the specified date. + schema: + type: string + format: date + - name: date_shot_max + in: query + description: Select only images shot before the specified date. + schema: + type: string + format: date + - name: keyword + in: query + description: Select only images with a title, description or original ID containing at least one of the specified keywords. + schema: + errorMessage: should be a string or an array of strings + oneOf: + - type: string + - type: array + items: + type: string + - name: sortKey + in: query + description: Sort images by Distance, title or date shot. + schema: + errorMessage: should be a string + type: string + - name: owner_id + in: query + description: Select only the images belonging to the owner(s) with the specified ID(s). + example: 3 + schema: + $ref: "#/components/schemas/ApiIdListParameter" + - name: place_names + in: query + description: Select only images where the specified places are visible. + schema: + errorMessage: should be a string or an array of strings + oneOf: + - type: string + - type: array + items: + type: string - name: latitude in: query description: Latitude of the bounding box center. From 8433c536244b210454d4de64938080100b16d58c Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 12 Mar 2024 13:20:34 +0100 Subject: [PATCH 3/7] adding to where clause, limits and offsets --- app/api/images/images.list.controller.js | 132 +++++++++++++++++------ 1 file changed, 97 insertions(+), 35 deletions(-) diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index c6709a9..0a29b94 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -551,44 +551,106 @@ exports.getStats = utils.route(async (req, res) => { exports.getImagesBound = utils.route(async (req, res) => { const query = req.query; const attributes = parseAttributes(query); - const sequelizeQuery = { - subQuery: false, - attributes: attributes, - where: { - [Op.and]: [ - { state: 'validated' }, - - Sequelize.where( - Sequelize.fn( - 'ST_Contains', - Sequelize.col('images.footprint'), - Sequelize.fn( - 'ST_SetSRID', - Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), - 4326 - ) - ), - true - ), + const orderBy = query.sortKey; + const orderByNearest = models.sequelize.literal(`images.location <-> st_setsrid(ST_makepoint(${query.longitude}, ${query.latitude}), 4326)`); + + let whereClauses = []; + + if (query.owner_id) { + whereClauses.push({ owner_id: inUniqueOrList(query.owner_id) }); + } + + if (query.collection_id) { + whereClauses.push({ collection_id: inUniqueOrList(query.collection_id) }); + } + + if (query.keyword) { + whereClauses.push({ + [Op.or]: { + original_id: iLikeFormatter(query.keyword), + title: iLikeFormatter(query.keyword), + caption: iLikeFormatter(query.keyword) + } + }); + } + + if (query.place_names) { + whereClauses.push({ geotags_array: containsUniqueOrList(query.place_names) }); + } - Sequelize.where( - Sequelize.fn( - 'ST_Distance', - Sequelize.cast( - Sequelize.fn('ST_SetSRID', Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), 4326), - 'geography' - ), - Sequelize.cast( - Sequelize.col('images.location'), - 'geography' - ) - ), - { - [Op.lte]: query.distance // Distance in meters (20 km = 20000 meters) + if (query.date_shot_min || query.date_shot_max) { + whereClauses.push({ + // Single date + [Op.or]: { + date_shot: { + [Op.and]: { + [Op.not]: null, + [Op.gte]: query.date_shot_min, + [Op.lte]: query.date_shot_max + }, + }, + // Range of dates + [Op.and]: { + date_shot_min: { + [Op.and]: { + [Op.not]: null, + [Op.gte]: query.date_shot_min, + [Op.lte]: query.date_shot_max + } + }, + date_shot_max: { + [Op.and]: { + [Op.not]: null, + [Op.gte]: query.date_shot_min, + [Op.lte]: query.date_shot_max + } } + } + } + }); + } + + whereClauses.push( + Sequelize.where( + Sequelize.fn( + 'ST_Contains', + Sequelize.col('images.footprint'), + Sequelize.fn( + 'ST_SetSRID', + Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), + 4326 + ) + ), + true + ), + ); + + whereClauses.push( + Sequelize.where( + Sequelize.fn( + 'ST_Distance', + Sequelize.cast( + Sequelize.fn('ST_SetSRID', Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), 4326), + 'geography' ), - ], - } + Sequelize.cast( + Sequelize.col('images.location'), + 'geography' + ) + ), + { + [Op.lte]: query.distance // Distance in meters (20 km = 20000 meters) + } + ), + ); + + const sequelizeQuery = { + subQuery: false, + attributes: attributes, + where: { [Op.and]: whereClauses }, + order: orderBy === 'distance' ? orderByNearest : (orderBy === 'title' ? [['title']] : [['date_shot_min']]), + limit: query.limit || 30, + offset: query.offset || 0, }; const images = await models.images.findAll(sequelizeQuery); From c2b8a56d1a3a02c843c57168fd985ba2f2f8a0f9 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 18 Mar 2024 11:48:35 +0100 Subject: [PATCH 4/7] including count for geo images fetch --- app/api/images/images.list.controller.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 0a29b94..d116837 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -653,11 +653,17 @@ exports.getImagesBound = utils.route(async (req, res) => { offset: query.offset || 0, }; - const images = await models.images.findAll(sequelizeQuery); - if (images) { - await mediaUtils.setListImageUrl(images, /* image_width */ 200, /* image_height */ null); + const images = await models.images.findAndCountAll(sequelizeQuery); + // Count the total number of images matching the query + const countPromise = await models.images.count({ + where: { [Op.and]: whereClauses }, + distinct: 'images.id' + }); + images.count = countPromise; + if (images.rows) { + await mediaUtils.setListImageUrl(images.rows, /* image_width */ 200, /* image_height */ null); } - images.forEach((image) => { + images.rows.forEach((image) => { delete image.dataValues.iiif_data; }); res.status(200).send(images); From ad0a8e0fbc1ef3d81637f16e89aa57ce91c7908c Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 19 Mar 2024 17:17:06 +0100 Subject: [PATCH 5/7] adding image view_type to the filters API --- app/api/images/images.list.controller.js | 15 +++++++++++++-- app/api/images/images.openapi.params.yml | 10 ++++++++++ app/api/images/images.openapi.yml | 10 ++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 8d5ff1f..8259794 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -235,6 +235,10 @@ const getImages = async (req, orderkey, count = true) => { }); } + if (query.view_type) { + whereClauses.push({ view_type: inUniqueOrList(query.view_type) }); + } + if (isGeoref) { if (query.bbox) { whereClauses.push({ @@ -622,6 +626,10 @@ exports.getImagesBound = utils.route(async (req, res) => { }); } + if (query.view_type) { + whereClauses.push({ view_type: inUniqueOrList(query.view_type) }); + } + whereClauses.push( Sequelize.where( Sequelize.fn( @@ -650,8 +658,11 @@ exports.getImagesBound = utils.route(async (req, res) => { 'geography' ) ), - { - [Op.lte]: query.distance // Distance in meters (20 km = 20000 meters) + { // Distance in meters (20 km = 20000 meters) + [Op.and]: [ + { [Op.lte]: query.distance }, // Distance less than or equal to query.distance + { [Op.gt]: 4 } // To avoid unwanted images locatated on point ie looking away from point + ] } ), ); diff --git a/app/api/images/images.openapi.params.yml b/app/api/images/images.openapi.params.yml index 1d895a3..cbcae9a 100644 --- a/app/api/images/images.openapi.params.yml +++ b/app/api/images/images.openapi.params.yml @@ -188,3 +188,13 @@ schema: type: string format: date-time +- name: view_type + in: query + description: Select only image whos view_type is equal to. + example: ['terrestrial', 'lowOblique', 'highOblique', 'nadir'] + schema: + type: array + minItems: 1 + maxItems: 4 + items: + type: string diff --git a/app/api/images/images.openapi.yml b/app/api/images/images.openapi.yml index 11b66e8..2302115 100644 --- a/app/api/images/images.openapi.yml +++ b/app/api/images/images.openapi.yml @@ -570,6 +570,16 @@ required: true schema: type: number + - name: view_type + in: query + description: Select only image whos view_type is equal to. + example: ['terrestrial', 'lowOblique', 'highOblique', 'nadir'] + schema: + type: array + minItems: 1 + maxItems: 4 + items: + type: string responses: "200": description: Successful request. From 11de36880e85be185856bc7cbcbbdc578c469d1c Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 20 Mar 2024 10:24:28 +0100 Subject: [PATCH 6/7] adding inner join on geometadata for more accurate footprint --- app/api/images/images.list.controller.js | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 8259794..913e5f4 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -631,18 +631,12 @@ exports.getImagesBound = utils.route(async (req, res) => { } whereClauses.push( - Sequelize.where( - Sequelize.fn( - 'ST_Contains', - Sequelize.col('images.footprint'), - Sequelize.fn( - 'ST_SetSRID', - Sequelize.fn('ST_MakePoint', query.longitude, query.latitude), - 4326 - ) - ), - true - ), + Sequelize.literal( + `CASE + WHEN geometadatum.footprint IS NOT NULL AND ST_Contains(geometadatum.footprint, ST_SetSRID(ST_MakePoint(${query.longitude}, ${query.latitude}), 4326)) THEN true + ELSE ST_Contains(images.footprint, ST_SetSRID(ST_MakePoint(${query.longitude}, ${query.latitude}), 4326)) + END` + ) ); whereClauses.push( @@ -668,6 +662,11 @@ exports.getImagesBound = utils.route(async (req, res) => { ); const sequelizeQuery = { + include: [{ + model: models.geometadata, + required: true, // Ensure that only images with geometadata are retrieved + attributes: [], // Exclude geometadata attributes from the result, we only need it for the join + }], subQuery: false, attributes: attributes, where: { [Op.and]: whereClauses }, @@ -680,7 +679,12 @@ exports.getImagesBound = utils.route(async (req, res) => { // Count the total number of images matching the query const countPromise = await models.images.count({ where: { [Op.and]: whereClauses }, - distinct: 'images.id' + distinct: 'images.id', + include: [{ + model: models.geometadata, + required: true, // Ensure that only images with geometadata are retrieved + attributes: [], // Exclude geometadata attributes from the result, we only need it for the join + }], }); images.count = countPromise; if (images.rows) { From df77b41ddfe2236c6d4a1542c1695d6c2bac216c Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 27 Mar 2024 11:13:36 +0100 Subject: [PATCH 7/7] rework fetching images with or without count --- app/api/images/images.list.controller.js | 33 ++++++++----------- app/api/images/images.openapi.yml | 2 +- .../images/schemas/image-bound.schema.json | 14 -------- 3 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 app/api/images/schemas/image-bound.schema.json diff --git a/app/api/images/images.list.controller.js b/app/api/images/images.list.controller.js index 913e5f4..df88c97 100644 --- a/app/api/images/images.list.controller.js +++ b/app/api/images/images.list.controller.js @@ -281,6 +281,12 @@ const getImages = async (req, orderkey, count = true) => { const randomOrder = models.sequelize.literal("random()"); + // findAll returns an array where findAndCountAll returns object {rows, count} + const response = { + rows: [], + count: null + } + if (!isGeoref) { let whereClauseApriori = {} let includeOption = null; @@ -329,8 +335,8 @@ const getImages = async (req, orderkey, count = true) => { include: includeOption }; if (count) { - const response = await models.images.findAndCountAll(sequelizeQuery); - // Count the total number of images matching the query + response.rows = await models.images.findAll(sequelizeQuery); + // Count the total number of matching images, removing duplicates when in contribute mode, i.e. apriori_locations const countPromise = await models.images.count({ where: { [Op.and]: whereClauses }, include: includeOption, @@ -339,7 +345,7 @@ const getImages = async (req, orderkey, count = true) => { response.count = countPromise return response; } else { - const response = await models.images.findAll(sequelizeQuery); + response.rows = await models.images.findAll(sequelizeQuery); return response; } } else { @@ -355,10 +361,10 @@ const getImages = async (req, orderkey, count = true) => { include: [includeCollectionFilter] }; if (count) { - const response = await models.images.findAndCountAll(sequelizeQuery); - return response; + const imagesAndCount = await models.images.findAndCountAll(sequelizeQuery); + return imagesAndCount; } else { - const response = await models.images.findAll(sequelizeQuery); + response.rows = await models.images.findAll(sequelizeQuery); return response; } } @@ -382,9 +388,8 @@ exports.getList = utils.route(async (req, res) => { exports.getListId = utils.route(async (req, res) => { req.query = { ...req.query, attributes: ["id"] }; const images = await getImages(req, /*orderBy*/ 'id', /*count*/ false); - // Send flattened objects - res.status(200).send(images.map(obj => obj.id)); + res.status(200).send(images.rows.map(obj => obj.id)); }); exports.getListMetadata = utils.route(async (req, res) => { @@ -676,17 +681,7 @@ exports.getImagesBound = utils.route(async (req, res) => { }; const images = await models.images.findAndCountAll(sequelizeQuery); - // Count the total number of images matching the query - const countPromise = await models.images.count({ - where: { [Op.and]: whereClauses }, - distinct: 'images.id', - include: [{ - model: models.geometadata, - required: true, // Ensure that only images with geometadata are retrieved - attributes: [], // Exclude geometadata attributes from the result, we only need it for the join - }], - }); - images.count = countPromise; + if (images.rows) { await mediaUtils.setListImageUrl(images.rows, /* image_width */ 200, /* image_height */ null); } diff --git a/app/api/images/images.openapi.yml b/app/api/images/images.openapi.yml index 2302115..668af6b 100644 --- a/app/api/images/images.openapi.yml +++ b/app/api/images/images.openapi.yml @@ -586,7 +586,7 @@ content: application/json: schema: - $ref: "#/components/schemas/ImagesBound" + $ref: "#/components/schemas/ImageList" "400": $ref: "#/components/responses/RequestParametersValidationErrors" "404": diff --git a/app/api/images/schemas/image-bound.schema.json b/app/api/images/schemas/image-bound.schema.json deleted file mode 100644 index 4b7035f..0000000 --- a/app/api/images/schemas/image-bound.schema.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$id": "ImagesBound", - "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", - "properties": { - "rows": { - "type": "array", - "items": { - "$ref": "Image#" - } - } - }, - "additionalProperties": false -}