diff --git a/web/client/api/GeoNode.js b/web/client/api/GeoNode.js
index 612b96cc78a..d8a2e180e08 100644
--- a/web/client/api/GeoNode.js
+++ b/web/client/api/GeoNode.js
@@ -28,6 +28,7 @@ export const GEONODE_DEFAULT_SORT = '-date';
export const RESOURCES = 'resources';
export const DATASETS = 'datasets';
export const DOCUMENTS = 'documents';
+export const MAPS = 'maps';
export const FACETS = 'facets';
let endpoints = {
@@ -160,6 +161,14 @@ export const getDocumentByPk = (baseUrl, pk) => {
.then(({ data }) => data.document);
};
+export const getMapByPk = (baseUrl, pk) => {
+ return axios.get(getEndpointUrl(baseUrl, MAPS, pk), {
+ params: mergePresetParams('VIEWER_COMMON', 'MAP'),
+ ...paramsSerializer()
+ })
+ .then(({ data }) => data.map);
+};
+
export const getResources = ({
q,
pageSize = 10,
diff --git a/web/client/api/__tests__/GeoNode-test.js b/web/client/api/__tests__/GeoNode-test.js
index 66ddb09b5b0..24fc27a44fc 100644
--- a/web/client/api/__tests__/GeoNode-test.js
+++ b/web/client/api/__tests__/GeoNode-test.js
@@ -221,4 +221,26 @@ describe('Test correctness of the GeoNode APIs (mock axios)', () => {
options: { service: { defaultSort: '-popular_count' } }
}).then(() => done()).catch(done);
});
+
+ it('getMapByPk requests the maps endpoint and unwraps the map', (done) => {
+ mockAxios.onGet().reply((config) => {
+ try {
+ expect(config.url).toBe('https://example.com/api/v2/maps/2');
+ } catch (e) {
+ done(e);
+ }
+ return [200, { map: { pk: 2, title: 'mappa_gruppi', data: { map: { layers: [], groups: [] } } } }];
+ });
+
+ API.getMapByPk('https://example.com', 2).then((map) => {
+ try {
+ expect(map.pk).toBe(2);
+ expect(map.title).toBe('mappa_gruppi');
+ expect(map.data.map).toEqual({ layers: [], groups: [] });
+ done();
+ } catch (e) {
+ done(e);
+ }
+ }).catch(done);
+ });
});
diff --git a/web/client/api/catalog/GeoNode.js b/web/client/api/catalog/GeoNode.js
index 82ee226fff8..037062d7419 100644
--- a/web/client/api/catalog/GeoNode.js
+++ b/web/client/api/catalog/GeoNode.js
@@ -6,21 +6,16 @@
* LICENSE file in the root directory of this source tree.
*/
-
-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 { textSearch as geonodeTextSearch, getDatasetByPk, getResourceByPk, getDocumentByPk, getMapByPk } from '../GeoNode';
import { getLayerTitleTranslations } from '../../utils/LayersUtils';
-import { getMessageById } from '../../utils/LocaleUtils';
import {
resourceToLayerConfig,
isDefaultDatasetSubtype,
getTagConfig,
- ResourceTypes,
- GEONODE_DOCUMENTS_ROW_VIEWER
+ documentsToLayerConfig as documentsToLayerConfigSync,
+ resourceMapToLayerGroup,
+ ResourceTypes
} from '../../utils/GeoNodeUtils';
-import { getPolygonFromExtent } from '../../utils/CoordinatesUtils';
import { getConfigProp } from '../../utils/ConfigUtils';
import {
preprocess as commonPreprocess,
@@ -99,7 +94,11 @@ export const getCatalogRecords = (records, options) => {
tagFilterType,
creator: record.owner?.username,
identifier: record?.pk,
- icon: record.resource_type === ResourceTypes.DOCUMENT ? { glyph: 'document' } : undefined,
+ icon: record.resource_type === ResourceTypes.DOCUMENT
+ ? { glyph: 'document' }
+ : record.resource_type === ResourceTypes.MAP
+ ? { glyph: '1-map' }
+ : undefined,
isValid: true
};
});
@@ -120,93 +119,6 @@ export const getLayerFromRecord = (record, options, asPromise = false) => {
.then((resource) => resourceToLayerConfig({ ...record, ...resource }, options));
};
-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 getDocumentsStyle = (locales) => ({
- format: 'geostyler',
- metadata: { editorType: 'visual' },
- body: {
- rules: [
- { name: getMessageById(locales, 'catalog.subtypes.video'), ruleId: '01', mandatory: false, filter: ['&&', ['==', 'subtype', 'video']], symbolizers: documentMarkerSymbolizer('video-camera') },
- { name: getMessageById(locales, 'catalog.subtypes.image'), ruleId: '02', mandatory: false, filter: ['&&', ['==', 'subtype', 'image']], symbolizers: documentMarkerSymbolizer('camera') },
- { name: getMessageById(locales, 'catalog.subtypes.file'), 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;
- const locales = options?.locales;
- // 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: `${getMessageById(locales, 'catalog.resourceTypes.document')} (${features.length})`,
- ...(bbox && { bbox }),
- features,
- style: getDocumentsStyle(locales),
- 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
@@ -217,20 +129,55 @@ export const processRecords = (records = [], options = {}, locales) => {
const applySecurity = (layer) => layer && protectedId
? { ...layer, security: { type: 'basic', sourceId: protectedId } }
: layer;
+
+ const others = records.filter(record => ![ResourceTypes.DOCUMENT, ResourceTypes.MAP].includes(record.resource_type));
const documents = records.filter(record => record.resource_type === ResourceTypes.DOCUMENT);
- const others = records.filter(record => record.resource_type !== ResourceTypes.DOCUMENT);
+ const maps = records.filter(record => record.resource_type === ResourceTypes.MAP);
+
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, locales }).catch(() => null)
+ ? Promise.all(
+ documents.map(doc => getDocumentByPk(options?.service?.url, doc.pk).catch(() => null))
+ )
+ .then((docs) => documentsToLayerConfigSync(docs, locales))
+ .catch(() => null)
: Promise.resolve(null);
- return Promise.all([otherLayersPromise, documentsLayerPromise])
- .then(([otherLayers, documentsLayer]) => ({
- layers: [...otherLayers, documentsLayer].filter(Boolean),
- groups: []
- }));
+
+ const mapContentsPromise = Promise.all(
+ maps.map(record => getMapByPk(options?.service?.url, record.pk)
+ .then((mapResource) => resourceMapToLayerGroup(mapResource))
+ .catch(() => null))
+ );
+ return Promise.all([
+ otherLayersPromise,
+ documentsLayerPromise,
+ mapContentsPromise
+ ])
+ .then(([otherLayers, documentsLayer, mapContents]) => {
+ const validMapContents = mapContents.filter(Boolean);
+ return {
+ layers: [
+ ...otherLayers,
+ documentsLayer,
+ ...validMapContents.flatMap(content => content.layers.map(applySecurity))
+ ].filter(Boolean),
+ groups: validMapContents.flatMap(content => content.groups)
+ };
+ });
+};
+
+// used by https://github.com/GeoNode/geonode-mapstore-client/issues/2583
+export const documentsToLayerConfig = (documents = [], options = {}) => {
+ const baseURL = options?.service?.url;
+ const locales = options?.locales;
+ // 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) => {
+ return documentsToLayerConfigSync(fullDocs, locales);
+ });
};
export const getCapabilities = ({ service } = {}) => {
diff --git a/web/client/api/catalog/__tests__/GeoNode-test.js b/web/client/api/catalog/__tests__/GeoNode-test.js
index 9ea6bca22dc..1ab70a6e803 100644
--- a/web/client/api/catalog/__tests__/GeoNode-test.js
+++ b/web/client/api/catalog/__tests__/GeoNode-test.js
@@ -12,7 +12,6 @@ import axios from '../../../libs/ajax';
import {
getCatalogRecords,
getLayerFromRecord,
- documentsToLayerConfig,
processRecords
} from '../GeoNode';
@@ -152,27 +151,6 @@ describe('GeoNode catalog processRecords / documents', () => {
links: [{ link_type: 'OGC:WMS', url: 'http://sample?name=layer1' }]
};
- it('documentsToLayerConfig builds a single vector layer of point features', (done) => {
- mockDocuments();
- documentsToLayerConfig([{ pk: 10 }, { pk: 11 }], { service: { url: 'http://gn' } })
- .then((layer) => {
- try {
- expect(layer.type).toBe('vector');
- expect(layer.name).toBe('Documents');
- expect(layer.rowViewer).toBe('GEONODE_DOCUMENTS_ROW_VIEWER');
- // doc 11 has no extent -> skipped
- expect(layer.features.length).toBe(1);
- expect(layer.features[0].id).toBe(10);
- expect(layer.features[0].geometry.type).toBe('Point');
- expect(layer.features[0].geometry.coordinates).toEqual([5, 5]);
- expect(layer.bbox).toExist();
- done();
- } catch (e) {
- done(e);
- }
- });
- });
-
it('processRecords collapses documents and converts other records to layers', (done) => {
mockDocuments();
const records = [datasetRecord, { resource_type: 'document', pk: 10 }];
@@ -223,25 +201,6 @@ describe('GeoNode catalog processRecords / documents', () => {
});
});
- it('documentsToLayerConfig skips documents whose fetch fails', (done) => {
- mockAxios.onGet().reply((config) => {
- if (config.url.indexOf('/documents/10') !== -1) {
- return [200, { document: { pk: 10, title: 'Doc 10', subtype: 'image', extent: { coords: [0, 0, 10, 10] } } }];
- }
- return [500];
- });
- documentsToLayerConfig([{ pk: 10 }, { pk: 12 }], { service: { url: 'http://gn' } })
- .then((layer) => {
- try {
- expect(layer.features.length).toBe(1);
- expect(layer.features[0].id).toBe(10);
- done();
- } catch (e) {
- done(e);
- }
- });
- });
-
it('processRecords skips records that fail and keeps the rest', (done) => {
mockAxios.onGet().reply((config) => {
if (config.url.indexOf('/documents/10') !== -1) {
diff --git a/web/client/components/catalog/editor/AdvancedSettings/GeoNodeAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/GeoNodeAdvancedSettings.jsx
index 0a58b095976..40b9bcfee71 100644
--- a/web/client/components/catalog/editor/AdvancedSettings/GeoNodeAdvancedSettings.jsx
+++ b/web/client/components/catalog/editor/AdvancedSettings/GeoNodeAdvancedSettings.jsx
@@ -24,7 +24,8 @@ const TAG_FILTER_TYPE_OPTIONS = [
const RESOURCE_TYPE_OPTIONS = [
{ value: 'dataset', label: },
- { value: 'document', label: }
+ { value: 'document', label: },
+ { value: 'map', label: }
];
/**
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index 7b9591c41ac..f9edfabff02 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -1741,7 +1741,8 @@
"resourceTypes": {
"label": "Ressourcentypen",
"dataset": "Datensätze",
- "document": "Dokumente"
+ "document": "Dokumente",
+ "map": "Karten"
},
"subtypes": {
"label": "Inhaltstyp",
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index 65e6bfa4eae..bfed712d7a8 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -1702,7 +1702,8 @@
"resourceTypes": {
"label": "Resource types",
"dataset": "Datasets",
- "document": "Documents"
+ "document": "Documents",
+ "map": "Maps"
},
"subtypes": {
"label": "Content type",
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index dd3f3016138..21c59adf63b 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -1702,7 +1702,8 @@
"resourceTypes": {
"label": "Tipos de recurso",
"dataset": "Conjuntos de datos",
- "document": "Documentos"
+ "document": "Documentos",
+ "map": "Mapas"
},
"subtypes": {
"label": "Tipo de contenido",
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index 96a1135d2c8..bb5e86bc73b 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -1702,7 +1702,8 @@
"resourceTypes": {
"label": "Types de ressource",
"dataset": "Jeux de données",
- "document": "Documents"
+ "document": "Documents",
+ "map": "Cartes"
},
"subtypes": {
"label": "Type de contenu",
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index 895bc0ee713..bc6a173c315 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -1701,7 +1701,8 @@
"resourceTypes": {
"label": "Tipi di risorsa",
"dataset": "Dataset",
- "document": "Documenti"
+ "document": "Documenti",
+ "map": "Mappe"
},
"subtypes": {
"label": "Tipo di contenuto",
diff --git a/web/client/utils/GeoNodeUtils.js b/web/client/utils/GeoNodeUtils.js
index 2be83fe3c7f..473a38b5bcf 100644
--- a/web/client/utils/GeoNodeUtils.js
+++ b/web/client/utils/GeoNodeUtils.js
@@ -6,14 +6,18 @@
* LICENSE file in the root directory of this source tree.
*/
-import { isImageServerUrl } from './ArcGISUtils';
-import { getConfigProp } from './ConfigUtils';
-import { getSupportedLocales, shortLocale } from './LocaleUtils';
+
import { v4 as uuid } from 'uuid';
import { isEmpty } from 'lodash';
import queryString from 'query-string';
import url from 'url';
+import turfCenter from '@turf/center';
+
+import { isImageServerUrl } from './ArcGISUtils';
+import { getConfigProp } from './ConfigUtils';
+import { getSupportedLocales, shortLocale, getMessageById } from './LocaleUtils';
import { ServerTypes } from './LayersUtils';
+import { getPolygonFromExtent } from './CoordinatesUtils';
export const SOURCE_TYPES = {
LOCAL: 'LOCAL',
@@ -305,6 +309,63 @@ const getLocalizedValues = (resource, key, defaultValue) => {
return defaultValue;
};
+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 getDocumentsStyle = (locales) => ({
+ format: 'geostyler',
+ metadata: { editorType: 'visual' },
+ body: {
+ rules: [
+ { name: getMessageById(locales, 'catalog.subtypes.video'),
+ ruleId: '01',
+ mandatory: false,
+ filter: ['&&', ['==', 'subtype', 'video']],
+ symbolizers: documentMarkerSymbolizer('video-camera')
+ },
+ { name: getMessageById(locales, 'catalog.subtypes.image'),
+ ruleId: '02',
+ mandatory: false,
+ filter: ['&&', ['==', 'subtype', 'image']],
+ symbolizers: documentMarkerSymbolizer('camera')
+ },
+ { name: getMessageById(locales, 'catalog.subtypes.file'),
+ ruleId: '03',
+ mandatory: false,
+ filter: ['&&', ['!=', 'subtype', 'image'], ['!=', 'subtype', 'video']],
+ symbolizers: documentMarkerSymbolizer('file')
+ }
+ ]
+ }
+});
+
/**
* convert resource layer configuration to a mapstore layer object
* @param {object} resource geonode layer resource
@@ -461,25 +522,88 @@ export const resourceToLayerConfig = (resource, options) => {
};
-// For map : if we need to also add map then we need this
-export const resourceToLayers = (resource) => {
- if (resource?.resource_type === ResourceTypes.DATASET) {
- return [{...resourceToLayerConfig(resource), isDataset: true}];
- }
- if (resource.maplayers && resource?.resource_type === ResourceTypes.MAP) {
- return resource.maplayers
- .map(maplayer => {
- maplayer.dataset ? resourceToLayerConfig(maplayer.dataset) : null;
- if (maplayer.dataset) {
- const layer = resourceToLayerConfig(maplayer.dataset);
- return {
- ...layer,
- style: maplayer.current_style
- };
- }
+/**
+ * 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.
+ * @param {array} documents - array of GeoNode document resources
+ * @param {object} options - options object, may include locales
+ * @returns {object} a MapStore vector layer config
+ */
+export const documentsToLayerConfig = (documents = [], locales) => {
+ const features = documents
+ .map((doc) => {
+ const extent = doc?.extent?.coords;
+ const polygon = !isEmpty(extent) ? getPolygonFromExtent(extent) : null;
+ const center = polygon ? turfCenter(polygon) : null;
+ if (!center) {
return null;
- })
- .filter(value => value);
- }
- return [];
+ }
+ 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: `${getMessageById(locales, 'catalog.resourceTypes.document')} (${features.length})`,
+ ...(bbox && { bbox }),
+ features,
+ style: getDocumentsStyle(locales),
+ rowViewer: GEONODE_DOCUMENTS_ROW_VIEWER
+ };
+};
+
+/**
+* Convert a GeoNode map resource into structure `{ layers, groups }` ready for the catalog container:
+* all the map layers exclude backgrounds, nested under a new parent group titled how the map name.
+* @param {object} - GeoNode map resource
+* @returns {object} - `{ layers, groups }` where `layers` is an array of layer objects and `groups` is an array of group objects
+*/
+export const resourceMapToLayerGroup = (mapResource) => {
+ const mapConfig = mapResource?.data?.map || {};
+ const parentId = `Default.${uuid()}`;
+ const parentGroup = {
+ title: mapResource?.title,
+ parent: 'Default',
+ options: { id: parentId },
+ asFirst: true
+ };
+ const oldToNewPath = { Default: parentId };
+ const groups = (mapConfig.groups || [])
+ .filter((group) => group?.id && group.id !== 'Default')
+ .sort((a, b) => a.id.split('.').length - b.id.split('.').length)
+ .map((group) => {
+ const segments = group.id.split('.');
+ const oldParentPath = segments.slice(0, -1).join('.');
+ const newParentPath = oldToNewPath[oldParentPath] || parentId;
+ const newSegment = uuid();
+ const newPath = `${newParentPath}.${newSegment}`;
+ oldToNewPath[group.id] = newPath;
+ const { id, title, ...groupOptions } = group;
+ return {
+ title,
+ parent: newParentPath,
+ options: { ...groupOptions, id: newPath, name: newSegment },
+ asFirst: false
+ };
+ });
+ const layers = (mapConfig.layers || [])
+ .filter((layer) => layer?.group !== 'background')
+ .map((layer) => ({
+ ...layer,
+ id: uuid(),
+ group: oldToNewPath[layer?.group] || parentId
+ }));
+ return { layers, groups: [parentGroup, ...groups] };
};
diff --git a/web/client/utils/__tests__/GeoNodeUtils-test.js b/web/client/utils/__tests__/GeoNodeUtils-test.js
index 0665a2d6ef9..2cbffb6e276 100644
--- a/web/client/utils/__tests__/GeoNodeUtils-test.js
+++ b/web/client/utils/__tests__/GeoNodeUtils-test.js
@@ -9,7 +9,7 @@
import expect from 'expect';
import { setSupportedLocales, getSupportedLocales } from '../LocaleUtils';
-import { resourceToLayerConfig, getDimensions, resolveApiPresetParams, mergePresetParams } from '../GeoNodeUtils';
+import { resourceToLayerConfig, getDimensions, resolveApiPresetParams, mergePresetParams, documentsToLayerConfig } from '../GeoNodeUtils';
describe('GeoNodeUtils', () => {
describe('resourceToLayerConfig', () => {
@@ -277,4 +277,33 @@ describe('GeoNodeUtils', () => {
expect(merged.exclude).toContain('*');
});
});
+
+ describe('GeoNode catalog documents, maps', () => {
+
+ it('documentsToLayerConfig builds a single vector layer of point features', () => {
+ const layer = documentsToLayerConfig([
+ { pk: 10, title: 'Doc 10', subtype: 'image', detail_url: '/documents/10', extent: { coords: [0, 0, 10, 10] } },
+ { pk: 11, title: 'Doc 11', subtype: 'document', detail_url: '/documents/11' }
+ ]);
+ expect(layer.type).toBe('vector');
+ expect(layer.name).toBe('Documents');
+ expect(layer.rowViewer).toBe('GEONODE_DOCUMENTS_ROW_VIEWER');
+ // doc 11 has no extent -> skipped
+ expect(layer.features.length).toBe(1);
+ expect(layer.features[0].id).toBe(10);
+ expect(layer.features[0].geometry.type).toBe('Point');
+ expect(layer.features[0].geometry.coordinates).toEqual([5, 5]);
+ expect(layer.bbox).toExist();
+ });
+
+ it('documentsToLayerConfig skips documents whose fetch fails', () => {
+ const layer = documentsToLayerConfig([
+ { pk: 10, title: 'Doc 10', subtype: 'image', extent: { coords: [0, 0, 10, 10] } },
+ null
+ ]);
+ expect(layer.features.length).toBe(1);
+ expect(layer.features[0].id).toBe(10);
+ });
+ });
+
});