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
9 changes: 9 additions & 0 deletions web/client/api/GeoNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions web/client/api/__tests__/GeoNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
155 changes: 51 additions & 104 deletions web/client/api/catalog/GeoNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
};
});
Expand All @@ -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
Expand All @@ -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 } = {}) => {
Expand Down
41 changes: 0 additions & 41 deletions web/client/api/catalog/__tests__/GeoNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import axios from '../../../libs/ajax';
import {
getCatalogRecords,
getLayerFromRecord,
documentsToLayerConfig,
processRecords
} from '../GeoNode';

Expand Down Expand Up @@ -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 }];
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const TAG_FILTER_TYPE_OPTIONS = [

const RESOURCE_TYPE_OPTIONS = [
{ value: 'dataset', label: <Message msgId="catalog.resourceTypes.dataset" /> },
{ value: 'document', label: <Message msgId="catalog.resourceTypes.document" /> }
{ value: 'document', label: <Message msgId="catalog.resourceTypes.document" /> },
{ value: 'map', label: <Message msgId="catalog.resourceTypes.map" /> }
];

/**
Expand Down
3 changes: 2 additions & 1 deletion web/client/translations/data.de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1741,7 +1741,8 @@
"resourceTypes": {
"label": "Ressourcentypen",
"dataset": "Datensätze",
"document": "Dokumente"
"document": "Dokumente",
"map": "Karten"
},
"subtypes": {
"label": "Inhaltstyp",
Expand Down
3 changes: 2 additions & 1 deletion web/client/translations/data.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,8 @@
"resourceTypes": {
"label": "Resource types",
"dataset": "Datasets",
"document": "Documents"
"document": "Documents",
"map": "Maps"
},
"subtypes": {
"label": "Content type",
Expand Down
3 changes: 2 additions & 1 deletion web/client/translations/data.es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,8 @@
"resourceTypes": {
"label": "Tipos de recurso",
"dataset": "Conjuntos de datos",
"document": "Documentos"
"document": "Documentos",
"map": "Mapas"
},
"subtypes": {
"label": "Tipo de contenido",
Expand Down
3 changes: 2 additions & 1 deletion web/client/translations/data.fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion web/client/translations/data.it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -1701,7 +1701,8 @@
"resourceTypes": {
"label": "Tipi di risorsa",
"dataset": "Dataset",
"document": "Documenti"
"document": "Documenti",
"map": "Mappe"
},
"subtypes": {
"label": "Tipo di contenuto",
Expand Down
Loading
Loading