Skip to content
Open
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
5 changes: 4 additions & 1 deletion web/client/api/GeoNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { getConfigProp } from '../utils/ConfigUtils';
import { resolveApiPresetParams, paramsSerializer, mergePresetParams } from '../utils/GeoNodeUtils';

export const GEONODE_RESOURCE_TYPE_FILTER = 'filter{resource_type.in}';
// default sort applied to the catalog resources request when the caller does not provide one
// (matches the "Most recent" default shown in the catalog toolbar)
export const GEONODE_DEFAULT_SORT = '-date';


export const RESOURCES = 'resources';
Expand Down Expand Up @@ -211,7 +214,7 @@ export const getRecords = (url, startPosition, maxRecords, text, options) => {
baseUrl: url,
...(resourceTypes.length && { [GEONODE_RESOURCE_TYPE_FILTER]: resourceTypes }),
...options?.options?.filters,
sort: options?.options?.sort,
sort: options?.options?.sort ?? service?.defaultSort ?? GEONODE_DEFAULT_SORT,
...(service?.apiPresetKey && { apiPresetKey: service.apiPresetKey })
});
};
Expand Down
28 changes: 28 additions & 0 deletions web/client/api/__tests__/GeoNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,32 @@ describe('Test correctness of the GeoNode APIs (mock axios)', () => {
}
});
});

it('getRecords applies the default sort when none is provided', (done) => {
mockAxios.onGet().reply((config) => {
try {
expect(config.params.sort).toEqual(['-date']);
} catch (e) {
done(e);
}
return [200, { resources: [], total: 0, page: 1, page_size: 4, links: {} }];
});

API.getRecords('https://example.com', 1, 4, '', {}).then(() => done()).catch(done);
});

it('getRecords prefers the service defaultSort over the global default', (done) => {
mockAxios.onGet().reply((config) => {
try {
expect(config.params.sort).toEqual(['-popular_count']);
} catch (e) {
done(e);
}
return [200, { resources: [], total: 0, page: 1, page_size: 4, links: {} }];
});

API.getRecords('https://example.com', 1, 4, '', {
options: { service: { defaultSort: '-popular_count' } }
}).then(() => done()).catch(done);
});
});
162 changes: 153 additions & 9 deletions web/client/api/catalog/GeoNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
*/


import { textSearch as geonodeTextSearch, getDatasetByPk, getResourceByPk } from '../GeoNode';
import { v4 as uuid } from 'uuid';
import isEmpty from 'lodash/isEmpty';
import turfCenter from '@turf/center';
import { textSearch as geonodeTextSearch, getDatasetByPk, getResourceByPk, getDocumentByPk } from '../GeoNode';
import { getLayerTitleTranslations } from '../../utils/LayersUtils';
import {
resourceToLayerConfig,
isDefaultDatasetSubtype,
getTagConfig
getTagConfig,
ResourceTypes,
GEONODE_DOCUMENTS_ROW_VIEWER
} from '../../utils/GeoNodeUtils';
import { getPolygonFromExtent } from '../../utils/CoordinatesUtils';
import { getConfigProp } from '../../utils/ConfigUtils';
import {
preprocess as commonPreprocess,
Expand Down Expand Up @@ -92,6 +98,7 @@ export const getCatalogRecords = (records, options) => {
tagFilterType,
creator: record.owner?.username,
identifier: record?.pk,
icon: record.resource_type === ResourceTypes.DOCUMENT ? { glyph: 'document' } : undefined,
isValid: true
};
});
Expand All @@ -112,20 +119,157 @@ export const getLayerFromRecord = (record, options, asPromise = false) => {
.then((resource) => resourceToLayerConfig({ ...record, ...resource }, options));
};

export const getCapabilities = () => {
const calculateBbox = (coordinates) => {
const validCoords = (coordinates || []).filter(coord => coord && coord.length === 2);
if (validCoords.length === 0) {
return null;
}
const lons = validCoords.map(coord => coord[0]);
const lats = validCoords.map(coord => coord[1]);
return {
bounds: {
minx: Math.min(...lons),
miny: Math.min(...lats),
maxx: Math.max(...lons),
maxy: Math.max(...lats)
},
crs: 'EPSG:4326'
};
};

const documentMarkerSymbolizer = (glyph) => [{
kind: 'Icon',
size: 46,
image: { args: [{ color: 'blue', glyph, shape: 'circle' }], name: 'msMarkerIcon' },
anchor: 'bottom',
rotate: 0,
opacity: 1,
symbolizerId: '01',
msBringToFront: false,
msHeightReference: 'none'
}];

const DOCUMENTS_STYLE = {
format: 'geostyler',
metadata: { editorType: 'visual' },
body: {
rules: [
{ name: 'Videos', ruleId: '01', mandatory: false, filter: ['&&', ['==', 'subtype', 'video']], symbolizers: documentMarkerSymbolizer('video-camera') },
{ name: 'Images', ruleId: '02', mandatory: false, filter: ['&&', ['==', 'subtype', 'image']], symbolizers: documentMarkerSymbolizer('camera') },
{ name: 'Files', ruleId: '03', mandatory: false, filter: ['&&', ['!=', 'subtype', 'image'], ['!=', 'subtype', 'video']], symbolizers: documentMarkerSymbolizer('file') }
]
}
};

/**
* Build a single MapStore vector layer that collects the given GeoNode documents
* as point features (located at the center of each document extent). Documents
* without an extent are skipped.
*/
export const documentsToLayerConfig = (documents = [], options = {}) => {
const baseURL = options?.service?.url;
// resilient per document: a failed fetch is skipped, not fatal to the whole layer
return Promise.all(documents.map(doc => getDocumentByPk(baseURL, doc.pk).catch(() => null)))
.then((fullDocs) => {
const features = fullDocs
.map((doc) => {
const extent = doc?.extent?.coords;
const polygon = !isEmpty(extent) ? getPolygonFromExtent(extent) : null;
const center = polygon ? turfCenter(polygon) : null;
if (!center) {
return null;
}
return {
type: 'Feature',
properties: doc,
geometry: {
type: 'Point',
coordinates: center.geometry.coordinates
},
id: doc.pk
};
})
.filter(Boolean);
const bbox = calculateBbox(features.map(feature => feature.geometry.coordinates));
return {
id: uuid(),
type: 'vector',
visibility: true,
name: 'Documents',
title: 'Documents',
...(bbox && { bbox }),
features,
style: DOCUMENTS_STYLE,
rowViewer: GEONODE_DOCUMENTS_ROW_VIEWER
};
});
};

/**
* Process the whole selected record set into map content (N records -> M layers).
* GeoNode documents collapse into a single vector layer; every other record type
* is converted through getLayerFromRecord. Always resolves with `{ layers, groups }`.
*/
export const processRecords = (records = [], options = {}) => {
const protectedId = options?.service?.protectedId;
const applySecurity = (layer) => layer && protectedId
? { ...layer, security: { type: 'basic', sourceId: protectedId } }
: layer;
const documents = records.filter(record => record.resource_type === ResourceTypes.DOCUMENT);
const others = records.filter(record => record.resource_type !== ResourceTypes.DOCUMENT);
const otherLayersPromise = Promise.all(
// resilient per record: a failed conversion is skipped, not fatal to the batch
others.map(record => getLayerFromRecord(record, options, true).then(applySecurity).catch(() => null))
);
const documentsLayerPromise = documents.length
? documentsToLayerConfig(documents, options).catch(() => null)
: Promise.resolve(null);
return Promise.all([otherLayersPromise, documentsLayerPromise])
.then(([otherLayers, documentsLayer]) => ({
layers: [...otherLayers, documentsLayer].filter(Boolean),
groups: []
}));
};

export const getCapabilities = ({ service } = {}) => {

const subtypes = [
...(service?.resourceTypes?.includes('dataset') ? [
'vector',
'raster',
'vector_time',
'3dtiles',
'tabular'
] : []),
...(service?.resourceTypes?.includes('document') ? [
'image',
'video',
'audio',
'text',
'archive',
'presentation'
] : [])
];
return {
filterSupport: true,
orderBySupport: true,
getTagFilterKey: (service) => {
const tagFilterType = resolveTagFilterType(service);
getTagFilterKey: (_service) => {
const tagFilterType = resolveTagFilterType(_service);
return getTagConfig(tagFilterType).filterKey;
},
filterFormFields: [
{ id: 'category', type: 'select', order: 5, facet: 'category', label: 'Category', key: 'filter{category.identifier.in}' },
{ id: 'keyword', type: 'select', order: 6, facet: 'keyword', label: 'Keyword', key: 'filter{keywords.slug.in}' },
{ id: 'region', type: 'select', order: 7, facet: 'place', label: 'Region', key: 'filter{regions.code.in}' },
...(subtypes?.length ? [{
id: 'subtype',
labelId: 'catalog.subtypes.label',
type: 'select',
order: 4,
options: subtypes.map((value) => ({ value, labelId: `catalog.subtypes.${value}` }))
}] : []),
{ id: 'category', type: 'select', order: 5, facet: 'category', labelId: 'catalog.filterFields.category', key: 'filter{category.identifier.in}' },
{ id: 'keyword', type: 'select', order: 6, facet: 'keyword', labelId: 'catalog.filterFields.keyword', key: 'filter{keywords.slug.in}' },
{ id: 'region', type: 'select', order: 7, facet: 'place', labelId: 'catalog.filterFields.region', key: 'filter{regions.code.in}' },
{ type: 'date-range', filterKey: 'date', labelId: 'resourcesCatalog.creationFilter' },
{ labelId: 'Extent Filter', type: 'extent' }
{ labelId: 'catalog.filterFields.extent', type: 'extent' }
]
};
};
Expand Down
Loading
Loading