Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/controllers/v1/taxa_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,7 @@ TaxaController.nearby = async req => {
TaxaController.mapLayers = async req => {
const taxon = await Taxon.findByID( req.params.id );
if ( !taxon ) {
throw new Error( 404 );
throw util.httpError( 404, "Not Found" );
}

let query = squel.select( ).field( "source_identifier" )
Expand Down
11 changes: 11 additions & 0 deletions lib/controllers/v2/taxa_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,20 @@ const suggest = async req => {
return ctrlv1.suggest( req );
};

const mapLayers = async req => {
const v1Response = await ctrlv1.mapLayers( req );
return {
total_results: 1,
page: 1,
per_page: 1,
results: [v1Response]
};
};

module.exports = {
autocomplete: ctrlv1.autocomplete,
iconic: ctrlv1.iconic,
mapLayers,
search: ctrlv1.search,
show,
suggest,
Expand Down
46 changes: 46 additions & 0 deletions openapi/paths/v2/heatmap/{zoom}/{x}/{y}.grid.json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const _ = require( "lodash" );
const InaturalistMapserver = require( "../../../../../../lib/inaturalist_map_server" );
const transform = require( "../../../../../joi_to_openapi_parameter" );
const observationsSearchSchema = require( "../../../../../schema/request/observations_search" );
const { tilePathParams } = require( "../../../../../common_parameters" );

const inheritdObsSearchParams = _.filter(
observationsSearchSchema.$_terms.keys, p => !_.includes( ["fields"], p.key )
);
const transformedObsSearchParams = _.map( inheritdObsSearchParams, p => (
transform( p.schema.label( p.key ) )
) );

module.exports = sendWrapper => {
async function GET( req, res ) {
req.params.style = "heatmap";
req.params.format = "grid.json";
await InaturalistMapserver.defaultRoute( req, res );
sendWrapper( req, res );
}

GET.apiDoc = {
tags: ["UTFGrid"],
summary: "JSON for heatmap tiles",
security: [{
userJwtOptional: []
}],
parameters: tilePathParams.concat( transformedObsSearchParams ),
responses: {
200: {
description: "Returns a UTFGrid.",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/UtfGrid"
}
}
}
}
}
};

return {
GET
};
};
2 changes: 1 addition & 1 deletion openapi/paths/v2/heatmap/{zoom}/{x}/{y}.png.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module.exports = sendWrapper => {

GET.apiDoc = {
tags: ["Observation Tiles"],
summary: "Grid Tiles",
summary: "Heatmap Tiles",
security: [{
userJwtOptional: []
}],
Expand Down
2 changes: 1 addition & 1 deletion openapi/paths/v2/points/{zoom}/{x}/{y}.png.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module.exports = sendWrapper => {

GET.apiDoc = {
tags: ["Observation Tiles"],
summary: "Grid Tiles",
summary: "Points Tiles",
security: [{
userJwtOptional: []
}],
Expand Down
53 changes: 53 additions & 0 deletions openapi/paths/v2/taxa/{id}/map_layers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const _ = require( "lodash" );
const Joi = require( "joi" );
const taxaWantedSchema = require( "../../../../schema/request/taxa_wanted" );
const transform = require( "../../../../joi_to_openapi_parameter" );
const TaxaController = require( "../../../../../lib/controllers/v2/taxa_controller" );

module.exports = sendWrapper => {
async function GET( req, res ) {
const results = await TaxaController.mapLayers( req );
sendWrapper( req, res, null, results );
}

const parameters = _.map(
taxaWantedSchema.$_terms.keys,
child => transform( child.schema.label( child.key ) )
);
parameters.push(
transform(
Joi.number( ).integer( )
.label( "id" )
.meta( { in: "path" } )
.required( )
.description( "A single ID" )
),
transform( Joi.string( ).label( "fields" ).meta( { in: "query" } ) ),
transform( Joi.string( ).label( "X-HTTP-Method-Override" ).meta( { in: "header" } ) )
);

GET.apiDoc = {
tags: ["Taxa"],
summary: "Fetch information about which map layers are relevent for a taxon.",
security: [{
userJwtOptional: []
}],
parameters,
responses: {
200: {
description: "An array of taxon map layer data.",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/ResultsTaxaMapLayers"
}
}
}
}
}
};

return {
GET
};
};
16 changes: 16 additions & 0 deletions openapi/schema/response/results_taxa_map_layers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const Joi = require( "joi" );

module.exports = Joi.object( ).keys( {
total_results: Joi.number( ).integer( ).required( ),
page: Joi.number( ).integer( ).required( ),
per_page: Joi.number( ).integer( ).required( ),
results: Joi.array( ).items( Joi.object( ).keys( {
id: Joi.number( ).integer( )
.description( "Unique auto-increment integer identifier." )
.required( ),
ranges: Joi.boolean( ).required( ),
gbif_id: Joi.number( ).integer( ).valid( null ).required( ),
listed_places: Joi.boolean( ).required( ),
geomodel: Joi.boolean( ).required( )
} ) ).required( )
} ).unknown( false );
46 changes: 46 additions & 0 deletions schema/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,13 @@
"updated_at": "2021-10-08 01:00:00"
}
],
"geo_model_taxa": [
{
"taxon_id": 123,
"created_at": "2026-07-09 00:00:00",
"updated_at": "2026-07-09 00:00:00"
}
],
"identifications": [
{
"id": 102,
Expand Down Expand Up @@ -2584,6 +2591,10 @@
{
"id": 1000,
"title": "DetailsListedTaxonList"
},
{
"id": 1001,
"title": "List for taxon places"
}
],
"listed_taxa": [
Expand All @@ -2608,6 +2619,11 @@
"list_id": 1000,
"place_id": 432,
"establishment_means": "endemic"
},
{
"taxon_id": 123,
"list_id": 1001,
"place_id": 1
}
],
"messages": [
Expand Down Expand Up @@ -3102,6 +3118,13 @@
"height": 300
}
],
"place_geometries": [
{
"id": 1,
"place_id": 1,
"geom": "POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))"
}
],
"places": [
{
"id": 1,
Expand Down Expand Up @@ -3736,6 +3759,29 @@
"iconic_taxon_id": 113
}
],
"taxon_ranges": [
{
"taxon_id": 123
}
],
"taxon_scheme_taxa": [
{
"id": 1,
"taxon_scheme_id": 1,
"taxon_id": 123,
"source_identifier": 12345,
"created_at": "2026-07-09 00:00:00",
"updated_at": "2026-07-09 00:00:00"
}
],
"taxon_schemes": [
{
"id": 1,
"title": "GBIF",
"created_at": "2026-07-09 00:00:00",
"updated_at": "2026-07-09 00:00:00"
}
],
"users": [
{
"id": 1,
Expand Down
27 changes: 27 additions & 0 deletions test/integration/v1/taxa.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,33 @@ describe( "Taxa", ( ) => {
} );
} );

describe( "map_layers", ( ) => {
it( "returns taxon map layers", function ( done ) {
request( this.app )
.get( "/v1/taxa/123/map_layers" )
.expect( res => {
expect( res.body.id ).to.eq( 123 );
expect( res.body.ranges ).to.be.true;
expect( res.body.gbif_id ).to.eq( 12345 );
expect( res.body.listed_places ).to.be.true;
expect( res.body.geomodel ).to.be.true;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );

it( "returns a 404 for unknown taxa", function ( done ) {
request( this.app )
.get( "/v1/taxa/987654321/map_layers" )
.expect( res => {
expect( res.body.status ).to.eq( 404 );
expect( res.body.error ).to.eq( "Not Found" );
} )
.expect( "Content-Type", /json/ )
.expect( 404, done );
} );
} );

describe( "suggest", ( ) => {
it( "should succeed when source is checklist and taxon_id specified", function ( done ) {
request( this.app ).get( "/v1/taxa/suggest?source=checklist&taxon_id=1" ).expect( 200, done );
Expand Down
57 changes: 57 additions & 0 deletions test/integration/v1/tileserver.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const request = require( "supertest" );
const { expect } = require( "chai" );

describe( "Tileserver", ( ) => {
describe( "taxon places", ( ) => {
Expand Down Expand Up @@ -41,6 +42,18 @@ describe( "Tileserver", ( ) => {
.expect( "Content-Type", /png/ )
.expect( 200, done );
} );

it( "renders heatmap UTFGrids", function ( done ) {
this.timeout( 5000 );
request( this.app ).get( "/v1/heatmap/1/0/0.grid.json" )
.expect( res => {
expect( res.body.grid ).not.to.be.empty;
expect( res.body.keys ).not.to.be.empty;
expect( res.body.data ).not.to.be.empty;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );
} );

describe( "colored heatmap", ( ) => {
Expand All @@ -50,6 +63,18 @@ describe( "Tileserver", ( ) => {
.expect( "Content-Type", /png/ )
.expect( 200, done );
} );

it( "renders colored_heatmap UTFGrids", function ( done ) {
this.timeout( 5000 );
request( this.app ).get( "/v1/colored_heatmap/1/0/0.grid.json" )
.expect( res => {
expect( res.body.grid ).not.to.be.empty;
expect( res.body.keys ).not.to.be.empty;
expect( res.body.data ).not.to.be.empty;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );
} );

describe( "points", ( ) => {
Expand All @@ -58,5 +83,37 @@ describe( "Tileserver", ( ) => {
.expect( "Content-Type", /png/ )
.expect( 200, done );
} );

it( "renders points UTFGrids", function ( done ) {
this.timeout( 5000 );
request( this.app ).get( "/v1/points/1/0/0.grid.json" )
.expect( res => {
expect( res.body.grid ).not.to.be.empty;
expect( res.body.keys ).not.to.be.empty;
expect( res.body.data ).not.to.be.empty;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );
} );

describe( "grid", ( ) => {
it( "renders grid tiles", function ( done ) {
request( this.app ).get( "/v1/grid/1/0/0.png" )
.expect( "Content-Type", /png/ )
.expect( 200, done );
} );

it( "renders grid UTFGrids", function ( done ) {
this.timeout( 5000 );
request( this.app ).get( "/v1/grid/1/0/0.grid.json" )
.expect( res => {
expect( res.body.grid ).not.to.be.empty;
expect( res.body.keys ).not.to.be.empty;
expect( res.body.data ).not.to.be.empty;
} )
.expect( "Content-Type", /json/ )
.expect( 200, done );
} );
} );
} );
Loading
Loading