77 */
88
99
10- import { textSearch as geonodeTextSearch , getDatasetByPk , getResourceByPk } from '../GeoNode' ;
10+ import { v4 as uuid } from 'uuid' ;
11+ import isEmpty from 'lodash/isEmpty' ;
12+ import turfCenter from '@turf/center' ;
13+ import { textSearch as geonodeTextSearch , getDatasetByPk , getResourceByPk , getDocumentByPk } from '../GeoNode' ;
1114import { getLayerTitleTranslations } from '../../utils/LayersUtils' ;
15+ import { getMessageById } from '../../utils/LocaleUtils' ;
1216import {
1317 resourceToLayerConfig ,
1418 isDefaultDatasetSubtype ,
15- getTagConfig
19+ getTagConfig ,
20+ ResourceTypes ,
21+ GEONODE_DOCUMENTS_ROW_VIEWER
1622} from '../../utils/GeoNodeUtils' ;
23+ import { getPolygonFromExtent } from '../../utils/CoordinatesUtils' ;
1724import { getConfigProp } from '../../utils/ConfigUtils' ;
1825import {
1926 preprocess as commonPreprocess ,
@@ -92,6 +99,7 @@ export const getCatalogRecords = (records, options) => {
9299 tagFilterType,
93100 creator : record . owner ?. username ,
94101 identifier : record ?. pk ,
102+ icon : record . resource_type === ResourceTypes . DOCUMENT ? { glyph : 'document' } : undefined ,
95103 isValid : true
96104 } ;
97105 } ) ;
@@ -112,20 +120,158 @@ export const getLayerFromRecord = (record, options, asPromise = false) => {
112120 . then ( ( resource ) => resourceToLayerConfig ( { ...record , ...resource } , options ) ) ;
113121} ;
114122
115- export const getCapabilities = ( ) => {
123+ const calculateBbox = ( coordinates ) => {
124+ const validCoords = ( coordinates || [ ] ) . filter ( coord => coord && coord . length === 2 ) ;
125+ if ( validCoords . length === 0 ) {
126+ return null ;
127+ }
128+ const lons = validCoords . map ( coord => coord [ 0 ] ) ;
129+ const lats = validCoords . map ( coord => coord [ 1 ] ) ;
130+ return {
131+ bounds : {
132+ minx : Math . min ( ...lons ) ,
133+ miny : Math . min ( ...lats ) ,
134+ maxx : Math . max ( ...lons ) ,
135+ maxy : Math . max ( ...lats )
136+ } ,
137+ crs : 'EPSG:4326'
138+ } ;
139+ } ;
140+
141+ const documentMarkerSymbolizer = ( glyph ) => [ {
142+ kind : 'Icon' ,
143+ size : 46 ,
144+ image : { args : [ { color : 'blue' , glyph, shape : 'circle' } ] , name : 'msMarkerIcon' } ,
145+ anchor : 'bottom' ,
146+ rotate : 0 ,
147+ opacity : 1 ,
148+ symbolizerId : '01' ,
149+ msBringToFront : false ,
150+ msHeightReference : 'none'
151+ } ] ;
152+
153+ const getDocumentsStyle = ( locales ) => ( {
154+ format : 'geostyler' ,
155+ metadata : { editorType : 'visual' } ,
156+ body : {
157+ rules : [
158+ { name : getMessageById ( locales , 'catalog.subtypes.video' ) , ruleId : '01' , mandatory : false , filter : [ '&&' , [ '==' , 'subtype' , 'video' ] ] , symbolizers : documentMarkerSymbolizer ( 'video-camera' ) } ,
159+ { name : getMessageById ( locales , 'catalog.subtypes.image' ) , ruleId : '02' , mandatory : false , filter : [ '&&' , [ '==' , 'subtype' , 'image' ] ] , symbolizers : documentMarkerSymbolizer ( 'camera' ) } ,
160+ { name : getMessageById ( locales , 'catalog.subtypes.file' ) , ruleId : '03' , mandatory : false , filter : [ '&&' , [ '!=' , 'subtype' , 'image' ] , [ '!=' , 'subtype' , 'video' ] ] , symbolizers : documentMarkerSymbolizer ( 'file' ) }
161+ ]
162+ }
163+ } ) ;
164+
165+ /**
166+ * Build a single MapStore vector layer that collects the given GeoNode documents
167+ * as point features (located at the center of each document extent). Documents
168+ * without an extent are skipped.
169+ */
170+ export const documentsToLayerConfig = ( documents = [ ] , options = { } ) => {
171+ const baseURL = options ?. service ?. url ;
172+ const locales = options ?. locales ;
173+ // resilient per document: a failed fetch is skipped, not fatal to the whole layer
174+ return Promise . all ( documents . map ( doc => getDocumentByPk ( baseURL , doc . pk ) . catch ( ( ) => null ) ) )
175+ . then ( ( fullDocs ) => {
176+ const features = fullDocs
177+ . map ( ( doc ) => {
178+ const extent = doc ?. extent ?. coords ;
179+ const polygon = ! isEmpty ( extent ) ? getPolygonFromExtent ( extent ) : null ;
180+ const center = polygon ? turfCenter ( polygon ) : null ;
181+ if ( ! center ) {
182+ return null ;
183+ }
184+ return {
185+ type : 'Feature' ,
186+ properties : doc ,
187+ geometry : {
188+ type : 'Point' ,
189+ coordinates : center . geometry . coordinates
190+ } ,
191+ id : doc . pk
192+ } ;
193+ } )
194+ . filter ( Boolean ) ;
195+ const bbox = calculateBbox ( features . map ( feature => feature . geometry . coordinates ) ) ;
196+ return {
197+ id : uuid ( ) ,
198+ type : 'vector' ,
199+ visibility : true ,
200+ name : 'Documents' ,
201+ title : `${ getMessageById ( locales , 'catalog.resourceTypes.document' ) } (${ features . length } )` ,
202+ ...( bbox && { bbox } ) ,
203+ features,
204+ style : getDocumentsStyle ( locales ) ,
205+ rowViewer : GEONODE_DOCUMENTS_ROW_VIEWER
206+ } ;
207+ } ) ;
208+ } ;
209+
210+ /**
211+ * Process the whole selected record set into map content (N records -> M layers).
212+ * GeoNode documents collapse into a single vector layer; every other record type
213+ * is converted through getLayerFromRecord. Always resolves with `{ layers, groups }`.
214+ */
215+ export const processRecords = ( records = [ ] , options = { } , locales ) => {
216+ const protectedId = options ?. service ?. protectedId ;
217+ const applySecurity = ( layer ) => layer && protectedId
218+ ? { ...layer , security : { type : 'basic' , sourceId : protectedId } }
219+ : layer ;
220+ const documents = records . filter ( record => record . resource_type === ResourceTypes . DOCUMENT ) ;
221+ const others = records . filter ( record => record . resource_type !== ResourceTypes . DOCUMENT ) ;
222+ const otherLayersPromise = Promise . all (
223+ // resilient per record: a failed conversion is skipped, not fatal to the batch
224+ others . map ( record => getLayerFromRecord ( record , options , true ) . then ( applySecurity ) . catch ( ( ) => null ) )
225+ ) ;
226+ const documentsLayerPromise = documents . length
227+ ? documentsToLayerConfig ( documents , { ...options , locales } ) . catch ( ( ) => null )
228+ : Promise . resolve ( null ) ;
229+ return Promise . all ( [ otherLayersPromise , documentsLayerPromise ] )
230+ . then ( ( [ otherLayers , documentsLayer ] ) => ( {
231+ layers : [ ...otherLayers , documentsLayer ] . filter ( Boolean ) ,
232+ groups : [ ]
233+ } ) ) ;
234+ } ;
235+
236+ export const getCapabilities = ( { service } = { } ) => {
237+
238+ const subtypes = [
239+ ...( service ?. resourceTypes ?. includes ( 'dataset' ) ? [
240+ 'vector' ,
241+ 'raster' ,
242+ 'vector_time' ,
243+ '3dtiles' ,
244+ 'tabular'
245+ ] : [ ] ) ,
246+ ...( service ?. resourceTypes ?. includes ( 'document' ) ? [
247+ 'image' ,
248+ 'video' ,
249+ 'audio' ,
250+ 'text' ,
251+ 'archive' ,
252+ 'presentation'
253+ ] : [ ] )
254+ ] ;
116255 return {
117256 filterSupport : true ,
118257 orderBySupport : true ,
119- getTagFilterKey : ( service ) => {
120- const tagFilterType = resolveTagFilterType ( service ) ;
258+ getTagFilterKey : ( _service ) => {
259+ const tagFilterType = resolveTagFilterType ( _service ) ;
121260 return getTagConfig ( tagFilterType ) . filterKey ;
122261 } ,
123262 filterFormFields : [
124- { id : 'category' , type : 'select' , order : 5 , facet : 'category' , label : 'Category' , key : 'filter{category.identifier.in}' } ,
125- { id : 'keyword' , type : 'select' , order : 6 , facet : 'keyword' , label : 'Keyword' , key : 'filter{keywords.slug.in}' } ,
126- { id : 'region' , type : 'select' , order : 7 , facet : 'place' , label : 'Region' , key : 'filter{regions.code.in}' } ,
263+ ...( subtypes ?. length ? [ {
264+ id : 'subtype' ,
265+ labelId : 'catalog.subtypes.label' ,
266+ type : 'select' ,
267+ order : 4 ,
268+ options : subtypes . map ( ( value ) => ( { value, labelId : `catalog.subtypes.${ value } ` } ) )
269+ } ] : [ ] ) ,
270+ { id : 'category' , type : 'select' , order : 5 , facet : 'category' , labelId : 'catalog.filterFields.category' , key : 'filter{category.identifier.in}' } ,
271+ { id : 'keyword' , type : 'select' , order : 6 , facet : 'keyword' , labelId : 'catalog.filterFields.keyword' , key : 'filter{keywords.slug.in}' } ,
272+ { id : 'region' , type : 'select' , order : 7 , facet : 'place' , labelId : 'catalog.filterFields.region' , key : 'filter{regions.code.in}' } ,
127273 { type : 'date-range' , filterKey : 'date' , labelId : 'resourcesCatalog.creationFilter' } ,
128- { labelId : 'Extent Filter ' , type : 'extent' }
274+ { labelId : 'catalog.filterFields.extent ' , type : 'extent' }
129275 ]
130276 } ;
131277} ;
0 commit comments