geonetwork-ui 2.10.3-dev.4154db07d → 2.11.0-dev.0559cfd9d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/fesm2022/geonetwork-ui.mjs +2983 -2095
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +408 -180
  4. package/index.d.ts.map +1 -1
  5. package/package.json +10 -8
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +2 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +5 -5
  8. package/src/libs/api/metadata-converter/src/lib/fixtures/mel.records.dataset+featurecatalog.ts +395 -0
  9. package/src/libs/api/metadata-converter/src/lib/fixtures/metawal.records.ts +5 -5
  10. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +78 -21
  11. package/src/libs/api/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts +13 -1
  12. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +19 -0
  13. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +17 -6
  14. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +110 -0
  15. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +133 -2
  16. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +9 -0
  17. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +11 -6
  18. package/src/libs/api/metadata-converter/src/lib/iso19139/utils/association-type.mapper.ts +12 -0
  19. package/src/libs/api/metadata-converter/src/lib/iso19139/write-parts.ts +2 -2
  20. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +157 -38
  21. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +45 -50
  22. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +54 -20
  23. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  24. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +24 -2
  25. package/src/libs/common/domain/src/lib/model/search/sort-by.model.ts +1 -5
  26. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +3 -3
  27. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
  28. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  29. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  30. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +54 -41
  31. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.html +33 -0
  32. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.ts +94 -0
  33. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-temporal-extents/form-field-temporal-extents.component.html +16 -12
  34. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +6 -0
  35. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +6 -0
  36. package/src/libs/feature/editor/src/lib/fields.config.ts +5 -0
  37. package/src/libs/feature/map/src/index.ts +0 -2
  38. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  39. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  40. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  41. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  42. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  43. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +10 -17
  44. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +2 -4
  45. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  46. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  47. package/src/libs/feature/router/src/lib/default/router.service.ts +1 -1
  48. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  49. package/src/libs/feature/search/src/index.ts +2 -0
  50. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +18 -4
  51. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +80 -19
  52. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +105 -0
  53. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +18 -0
  54. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +114 -0
  55. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +19 -8
  56. package/src/libs/feature/search/src/lib/sort-by/sort-by.component.ts +1 -1
  57. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  58. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +64 -4
  59. package/src/libs/feature/search/src/lib/utils/service/fields.ts +92 -9
  60. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  61. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  62. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  63. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +3 -3
  64. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  65. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  66. package/src/libs/ui/inputs/src/index.ts +2 -0
  67. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  68. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +12 -3
  69. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +11 -3
  70. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  71. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  72. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  73. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  74. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  75. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  76. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  77. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  78. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  79. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  80. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  81. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  82. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  83. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  84. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +117 -0
  85. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +244 -0
  86. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  87. package/src/libs/util/app-config/src/lib/app-config.ts +37 -0
  88. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  89. package/src/libs/util/app-config/src/lib/model.ts +25 -1
  90. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  91. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  92. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  93. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  94. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  95. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  96. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  97. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  98. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  99. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  100. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  101. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  102. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  103. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  104. package/src/libs/util/shared/src/index.ts +1 -0
  105. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  106. package/src/libs/util/shared/src/lib/gn-ui-version.ts +1 -1
  107. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  108. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  109. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  110. package/src/libs/util/shared/src/lib/utils/geojson.ts +24 -0
  111. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  112. package/tailwind.base.css +5 -0
  113. package/translations/de.json +38 -5
  114. package/translations/en.json +37 -4
  115. package/translations/es.json +35 -2
  116. package/translations/fr.json +42 -9
  117. package/translations/it.json +36 -3
  118. package/translations/nl.json +35 -2
  119. package/translations/pt.json +35 -2
  120. package/translations/sk.json +36 -3
  121. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  122. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  123. package/src/libs/feature/map/src/lib/geocoding.service.ts +0 -57
  124. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  125. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  126. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
  127. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  128. /package/src/libs/feature/{map/src/lib/geocoding/geocoding.component.css → editor/src/lib/components/record-form/form-field/form-field-associated-records/form-field-associated-records.component.css} +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  AggregationsParams,
12
12
  FieldFilter,
13
13
  FieldFilters,
14
+ FieldSort,
14
15
  FilterQuery,
15
16
  FiltersAggregationParams,
16
17
  SortByField,
@@ -33,7 +34,12 @@ import {
33
34
  LanguageCode,
34
35
  } from '../../../../../../../libs/common/domain/src/lib/model/record'
35
36
  import { TranslateService } from '@ngx-translate/core'
36
- import { getGeometryBoundingBox } from '../../../../../../../libs/util/shared/src'
37
+ import {
38
+ bboxToPolygon,
39
+ BoundingBox,
40
+ getGeometryBoundingBox,
41
+ isBoundingBox,
42
+ } from '../../../../../../../libs/util/shared/src'
37
43
  import { getLength as getGeodesicLength } from 'ol/sphere.js'
38
44
  import { LineString } from 'ol/geom.js'
39
45
 
@@ -48,7 +54,12 @@ export class ElasticsearchService {
48
54
 
49
55
  // runtime fields are computed using a Painless script
50
56
  // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
51
- private runtimeFields: Record<string, string> = {}
57
+ private runtimeFields: Record<
58
+ string,
59
+ { script: string; type: 'keyword' | 'date' }
60
+ > = {}
61
+
62
+ private fieldAliases: Record<string, string> = {}
52
63
 
53
64
  // we're using getters in case the defined languages change over time
54
65
  private get metadataLang(): LanguageCode {
@@ -90,8 +101,8 @@ export class ElasticsearchService {
90
101
  const addMapping = (fieldName: string) => {
91
102
  if (!payload.runtime_mappings) payload.runtime_mappings = {}
92
103
  payload.runtime_mappings[fieldName] = {
93
- type: 'keyword',
94
- script: this.runtimeFields[fieldName],
104
+ type: this.runtimeFields[fieldName].type,
105
+ script: this.runtimeFields[fieldName].script,
95
106
  }
96
107
  }
97
108
  const lookForField = (node: unknown) => {
@@ -112,6 +123,14 @@ export class ElasticsearchService {
112
123
  ) {
113
124
  addMapping(runtimeField)
114
125
  }
126
+ if (
127
+ runtimeField in node &&
128
+ typeof node[runtimeField] === 'object' &&
129
+ node[runtimeField] !== null &&
130
+ ('gte' in node[runtimeField] || 'lte' in node[runtimeField])
131
+ ) {
132
+ addMapping(runtimeField)
133
+ }
115
134
  if (
116
135
  'query' in node &&
117
136
  typeof node.query === 'string' &&
@@ -130,8 +149,16 @@ export class ElasticsearchService {
130
149
  return payload
131
150
  }
132
151
 
133
- registerRuntimeField(fieldName: string, expression: string) {
134
- this.runtimeFields[fieldName] = expression
152
+ registerRuntimeField(
153
+ fieldName: string,
154
+ expression: string,
155
+ type: 'keyword' | 'date' = 'keyword'
156
+ ) {
157
+ this.runtimeFields[fieldName] = { script: expression, type }
158
+ }
159
+
160
+ registerFieldAlias(fieldIdentifier: string, esFieldName: string) {
161
+ this.fieldAliases[fieldIdentifier] = esFieldName
135
162
  }
136
163
 
137
164
  getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
@@ -141,6 +168,7 @@ export class ElasticsearchService {
141
168
  values: uuids,
142
169
  },
143
170
  },
171
+ size: uuids.length,
144
172
  }
145
173
  }
146
174
 
@@ -199,9 +227,27 @@ export class ElasticsearchService {
199
227
  }
200
228
 
201
229
  private buildPayloadSort(sortBy: SortByField): SortParams {
202
- if (sortBy === null) return undefined
203
- const fields = Array.isArray(sortBy[0]) ? sortBy : [sortBy]
204
- return fields.map((field) => ({ [field[1]]: field[0] }))
230
+ if (!sortBy || sortBy.length === 0) return undefined
231
+ const fields = Array.isArray(sortBy[0])
232
+ ? (sortBy as FieldSort[])
233
+ : [sortBy as FieldSort]
234
+ return fields.map((field) => {
235
+ // Sort by nested array of dates only works with the explicit syntax
236
+ if (field[1].endsWith('.date')) {
237
+ const nestedPath = field[1].slice(0, field[1].lastIndexOf('.date'))
238
+ return {
239
+ [field[1]]: {
240
+ order: field[0] as 'desc' | 'asc',
241
+ mode: field[0] === 'desc' ? 'max' : 'min',
242
+ missing: '_last',
243
+ nested: {
244
+ path: nestedPath,
245
+ },
246
+ },
247
+ }
248
+ }
249
+ return { [field[1]]: field[0] }
250
+ })
205
251
  }
206
252
 
207
253
  private injectLangInQueryStringFields(
@@ -244,8 +290,68 @@ export class ElasticsearchService {
244
290
  return this.metadataLang === 'current'
245
291
  }
246
292
 
247
- private filtersToQuery(
293
+ private findSpatialFilterExtent(
248
294
  filters: FieldFilters | FiltersAggregationParams | string
295
+ ): BoundingBox | undefined {
296
+ if (typeof filters === 'string') {
297
+ return undefined
298
+ }
299
+ return Object.values(filters).find(isBoundingBox)
300
+ }
301
+
302
+ // builds a query matching records whose dates intersect the filter range
303
+ private buildDateRangeQuery(searchField: string, dateRange: DateRange) {
304
+ // fields ending with 'DateRange' maps as ES range type
305
+ if (searchField.endsWith('DateRange')) {
306
+ return {
307
+ range: {
308
+ [searchField]: {
309
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
310
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
311
+ format: 'yyyy-MM-dd',
312
+ relation: 'intersects',
313
+ },
314
+ },
315
+ }
316
+ }
317
+ const minField = `${searchField}Min`
318
+ const maxField = `${searchField}Max`
319
+ if (minField in this.runtimeFields && maxField in this.runtimeFields) {
320
+ // min/max runtime fields form an interval per record; match if it intersects the filter range
321
+ const filter = [
322
+ dateRange.start && {
323
+ range: {
324
+ [maxField]: {
325
+ gte: formatDate(dateRange.start),
326
+ format: 'yyyy-MM-dd',
327
+ },
328
+ },
329
+ },
330
+ dateRange.end && {
331
+ range: {
332
+ [minField]: {
333
+ lte: formatDate(dateRange.end),
334
+ format: 'yyyy-MM-dd',
335
+ },
336
+ },
337
+ },
338
+ ].filter(Boolean)
339
+ return { bool: { filter } }
340
+ }
341
+ return {
342
+ range: {
343
+ [searchField]: {
344
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
345
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
346
+ format: 'yyyy-MM-dd',
347
+ },
348
+ },
349
+ }
350
+ }
351
+
352
+ private filtersToQuery(
353
+ filters: FieldFilters | FiltersAggregationParams | string,
354
+ spatialFilterExtent = this.findSpatialFilterExtent(filters)
249
355
  ): FilterQuery {
250
356
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
251
357
  const makeQuery = (filter: FieldFilter): string => {
@@ -266,50 +372,49 @@ export class ElasticsearchService {
266
372
  ? filters
267
373
  : Object.keys(filters)
268
374
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
375
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
269
376
  .filter((fieldname) => !isDateRange(filters[fieldname]))
377
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
270
378
  .filter(
271
379
  (fieldname) =>
272
380
  filters[fieldname] &&
273
381
  JSON.stringify(filters[fieldname]) !== '{}'
274
382
  )
275
383
  .map(
276
- (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`
384
+ (fieldname) =>
385
+ `${this.fieldAliases[fieldname] ?? fieldname}:(${makeQuery(filters[fieldname])})`
277
386
  )
278
387
  .join(' AND ')
279
388
  if (filters['gn-ui-crossFieldFilter']) {
280
389
  queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
281
390
  }
282
- const queryRange = Object.entries(filters)
283
- .filter(([, value]) => isDateRange(value))
284
- .map(([searchField, dateRange]) => {
285
- return {
286
- searchField,
287
- dateRange,
288
- } as {
289
- searchField: string
290
- dateRange: DateRange
291
- }
292
- })[0]
391
+ const queryRanges = Object.entries(filters).filter(([, value]) =>
392
+ isDateRange(value)
393
+ ) as [string, DateRange][]
293
394
  const queryParts = [
294
395
  queryString && {
295
396
  query_string: {
296
397
  query: queryString,
297
398
  },
298
399
  },
299
- queryRange &&
300
- queryRange.dateRange && {
301
- range: {
302
- [queryRange.searchField]: {
303
- ...(queryRange.dateRange.start && {
304
- gte: formatDate(queryRange.dateRange.start),
305
- }),
306
- ...(queryRange.dateRange.end && {
307
- lte: formatDate(queryRange.dateRange.end),
308
- }),
309
- format: 'yyyy-MM-dd',
400
+ ...queryRanges.map(([searchField, dateRange]) =>
401
+ this.buildDateRangeQuery(searchField, dateRange)
402
+ ),
403
+ spatialFilterExtent && {
404
+ geo_shape: {
405
+ geom: {
406
+ shape: {
407
+ type: 'envelope',
408
+ // spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
409
+ coordinates: [
410
+ [spatialFilterExtent[0], spatialFilterExtent[3]],
411
+ [spatialFilterExtent[2], spatialFilterExtent[1]],
412
+ ],
310
413
  },
414
+ relation: 'intersects',
311
415
  },
312
416
  },
417
+ },
313
418
  ].filter(Boolean)
314
419
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
315
420
  }
@@ -348,7 +453,12 @@ export class ElasticsearchService {
348
453
  },
349
454
  })
350
455
  }
351
- const queryFilters = this.filtersToQuery(fieldSearchFilters)
456
+ // a spatial extent filter takes precedence over the preference geometry for boosting
457
+ const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
458
+ const queryFilters = this.filtersToQuery(
459
+ fieldSearchFilters,
460
+ spatialFilterExtent
461
+ )
352
462
  if (queryFilters) {
353
463
  filter.push(...queryFilters)
354
464
  }
@@ -359,7 +469,10 @@ export class ElasticsearchService {
359
469
  },
360
470
  })
361
471
  }
362
- if (geometry) {
472
+ const boostGeometry = spatialFilterExtent
473
+ ? bboxToPolygon(spatialFilterExtent)
474
+ : geometry
475
+ if (boostGeometry) {
363
476
  // boosts applied using the filter geometry:
364
477
  // * records completely within the geometry receive a boost of 5
365
478
  // * records intersecting the geometry receive a boost of 2
@@ -369,7 +482,7 @@ export class ElasticsearchService {
369
482
  {
370
483
  geo_shape: {
371
484
  geom: {
372
- shape: geometry,
485
+ shape: boostGeometry,
373
486
  relation: 'within',
374
487
  },
375
488
  boost: 5.0,
@@ -378,7 +491,7 @@ export class ElasticsearchService {
378
491
  {
379
492
  geo_shape: {
380
493
  geom: {
381
- shape: geometry,
494
+ shape: boostGeometry,
382
495
  relation: 'intersects',
383
496
  },
384
497
  boost: 2.0,
@@ -389,7 +502,7 @@ export class ElasticsearchService {
389
502
  // this will boost the results variably depending on their distance from the given geometry
390
503
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
391
504
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
392
- const bbox = getGeometryBoundingBox(geometry)
505
+ const bbox = getGeometryBoundingBox(boostGeometry)
393
506
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
394
507
  const northToCenter = new LineString([
395
508
  [center[0], bbox[3]],
@@ -615,6 +728,12 @@ export class ElasticsearchService {
615
728
  order: {
616
729
  [`_${aggregation.sort[1]}`]: aggregation.sort[0],
617
730
  },
731
+ ...(aggregation.includeValues && {
732
+ include: aggregation.includeValues,
733
+ }),
734
+ ...(aggregation.excludeValues && {
735
+ exclude: aggregation.excludeValues,
736
+ }),
618
737
  },
619
738
  }
620
739
  case 'histogram':
@@ -14,10 +14,12 @@ import {
14
14
  } from '../../../../../../libs/api/metadata-converter/src'
15
15
  import { PublicationVersionError } from '../../../../../../libs/common/domain/src/lib/model/error'
16
16
  import {
17
+ AssociatedRecord,
17
18
  CatalogRecord,
18
19
  DatasetFeatureCatalog,
19
- DatasetFeatureType,
20
20
  LanguageCode,
21
+ LinkedRecord,
22
+ RecordRelation,
21
23
  } from '../../../../../../libs/common/domain/src/lib/model/record'
22
24
  import {
23
25
  Aggregations,
@@ -145,7 +147,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
145
147
  return this.gn4SearchApi
146
148
  .search(
147
149
  'bucket',
148
- ['fcats', 'hassources'],
150
+ ['fcats', 'hassources', 'siblings', 'associated'],
149
151
  JSON.stringify(
150
152
  this.gn4SearchHelper.getMetadataByIdsPayload([uniqueIdentifier])
151
153
  )
@@ -179,43 +181,15 @@ export class Gn4Repository implements RecordsRepositoryInterface {
179
181
  )
180
182
  }
181
183
 
182
- private mapEmbeddedFeatureCatalog(
183
- featureTypes: Array<DatasetFeatureType>
184
- ): DatasetFeatureCatalog {
185
- return {
186
- featureTypes: featureTypes.map((featureType) => ({
187
- name: featureType.typeName || '',
188
- definition: featureType.definition || '',
189
- attributes: Array.isArray(featureType.attributeTable)
190
- ? featureType.attributeTable.map((attr) => {
191
- const values = attr.values
192
- ?.filter((v) => v.code || v.label)
193
- .map((v) => ({
194
- code: v.code,
195
- label: v.label,
196
- }))
197
- return {
198
- name: attr.name,
199
- code: attr.code,
200
- definition: attr.definition,
201
- type: attr.type,
202
- ...(values?.length > 0 ? { values } : {}),
203
- }
204
- })
205
- : [],
206
- })),
207
- }
208
- }
209
- getFeatureCatalog(
184
+ queryLinkedFeatureCatalog(
210
185
  record: CatalogRecord,
211
186
  visited: Set<string> = new Set() // prevent looping
212
187
  ): Observable<DatasetFeatureCatalog | null> {
213
188
  if (
214
- record.extras?.['featureTypes'] &&
215
- Array.isArray(record.extras['featureTypes']) &&
216
- record.extras['featureTypes'].length > 0
189
+ 'featureTypeDescriptions' in record &&
190
+ record.featureTypeDescriptions?.length > 0
217
191
  ) {
218
- return of(this.mapEmbeddedFeatureCatalog(record.extras['featureTypes']))
192
+ return of({ featureTypes: record.featureTypeDescriptions })
219
193
  }
220
194
 
221
195
  const featureCatalogIdentifier = record.extras[
@@ -225,7 +199,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
225
199
  visited.add(featureCatalogIdentifier)
226
200
  return this.getRecord(featureCatalogIdentifier).pipe(
227
201
  switchMap((record) =>
228
- record ? this.getFeatureCatalog(record, visited) : of(null)
202
+ record ? this.queryLinkedFeatureCatalog(record, visited) : of(null)
229
203
  )
230
204
  )
231
205
  }
@@ -249,22 +223,43 @@ export class Gn4Repository implements RecordsRepositoryInterface {
249
223
  )
250
224
  }
251
225
 
252
- getSources(record: CatalogRecord): Observable<CatalogRecord[]> {
253
- const sourcesIdentifiers = record.extras?.['sourcesIdentifiers'] as string[]
254
- if (sourcesIdentifiers && sourcesIdentifiers.length > 0) {
255
- return this.getMultipleRecords(sourcesIdentifiers)
256
- }
257
- return of(null)
258
- }
259
-
260
- getSourceOf(record: CatalogRecord): Observable<CatalogRecord[]> {
261
- const sourceOfIdentifiers = record.extras?.[
262
- 'sourceOfIdentifiers'
263
- ] as string[]
264
- if (sourceOfIdentifiers && sourceOfIdentifiers.length > 0) {
265
- return this.getMultipleRecords(sourceOfIdentifiers)
226
+ getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]> {
227
+ const siblings = (record.extras?.['siblings'] ?? []) as AssociatedRecord[]
228
+ const relations: Array<[RecordRelation, string[]]> = [
229
+ ['source', (record.extras?.['sourcesIdentifiers'] ?? []) as string[]],
230
+ ['sourceOf', (record.extras?.['sourceOfIdentifiers'] ?? []) as string[]],
231
+ ['sibling', siblings.map(({ uniqueIdentifier }) => uniqueIdentifier)],
232
+ [
233
+ 'associated',
234
+ (record.extras?.['associatedIdentifiers'] ?? []) as string[],
235
+ ],
236
+ ]
237
+ const requested = relations.filter(
238
+ ([, identifiers]) => identifiers.length > 0
239
+ )
240
+ if (requested.length === 0) {
241
+ return of([])
266
242
  }
267
- return of(null)
243
+ return forkJoin(
244
+ requested.map(([relation, identifiers]) =>
245
+ this.getMultipleRecords(identifiers).pipe(
246
+ map((records) =>
247
+ (records ?? []).map((record) => ({
248
+ record,
249
+ relation,
250
+ associationType:
251
+ relation === 'sibling'
252
+ ? siblings.find(
253
+ ({ uniqueIdentifier }) =>
254
+ uniqueIdentifier === record.uniqueIdentifier
255
+ )?.associationType
256
+ : undefined,
257
+ }))
258
+ ),
259
+ catchError(() => of([]))
260
+ )
261
+ )
262
+ ).pipe(map((groups) => groups.flat()))
268
263
  }
269
264
 
270
265
  aggregate(params: AggregationsParams): Observable<Aggregations> {
@@ -260,12 +260,50 @@ export interface SourceRecord {
260
260
  href?: string
261
261
  }
262
262
 
263
+ marker('domain.record.associationType.crossReference')
264
+ marker('domain.record.associationType.largerWorkCitation')
265
+ marker('domain.record.associationType.partOfSeamlessDatabase')
266
+ marker('domain.record.associationType.stereoMate')
267
+ marker('domain.record.associationType.isComposedOf')
268
+ marker('domain.record.associationType.collectiveTitle')
269
+ marker('domain.record.associationType.series')
270
+ marker('domain.record.associationType.dependency')
271
+ marker('domain.record.associationType.revisionOf')
272
+
273
+ // DS_AssociationTypeCode values of the ISO19115-3 codelist, any unrecognized value
274
+ // being mapped to 'crossReference'
275
+ export const associationTypeValues = [
276
+ 'crossReference',
277
+ 'largerWorkCitation',
278
+ 'partOfSeamlessDatabase',
279
+ 'stereoMate',
280
+ 'isComposedOf',
281
+ 'collectiveTitle',
282
+ 'series',
283
+ 'dependency',
284
+ 'revisionOf',
285
+ ] as const
286
+
287
+ export type AssociationType = (typeof associationTypeValues)[number]
288
+
263
289
  /**
264
290
  * Represents another record associated with this one (e.g. a parent/sibling dataset).
265
291
  */
266
292
  export interface AssociatedRecord {
267
- uuid: string
268
- associationType: string
293
+ uniqueIdentifier: string
294
+ associationType: AssociationType
295
+ }
296
+
297
+ /**
298
+ * Nature of the link between a record and another one; the association type is
299
+ * only known for 'sibling' relations, as it is held by the declaring record.
300
+ */
301
+ export type RecordRelation = 'source' | 'sourceOf' | 'sibling' | 'associated'
302
+
303
+ export interface LinkedRecord {
304
+ record: CatalogRecord
305
+ relation: RecordRelation
306
+ associationType?: AssociationType
269
307
  }
270
308
 
271
309
  export interface DatasetRecord extends BaseRecord {
@@ -278,6 +316,7 @@ export interface DatasetRecord extends BaseRecord {
278
316
  spatialExtents: Array<DatasetSpatialExtent>
279
317
  temporalExtents: Array<DatasetTemporalExtent>
280
318
  spatialRepresentation?: SpatialRepresentationType
319
+ featureTypeDescriptions?: Array<DatasetFeatureType>
281
320
  }
282
321
 
283
322
  export interface ServiceEndpoint {
@@ -309,33 +348,28 @@ export interface ReuseRecord extends BaseRecord {
309
348
  temporalExtents: Array<DatasetTemporalExtent>
310
349
  }
311
350
 
351
+ export interface DatasetFeatureType {
352
+ name: string
353
+ label?: string
354
+ description?: string
355
+ attributes: DatasetFeatureAttribute[]
356
+ }
312
357
  export type DatasetFeatureCatalog = {
313
- featureTypes: Array<{
314
- name: string
315
- definition: string
316
- attributes: DatasetFeatureAttribute[]
317
- }>
358
+ featureTypes: Array<DatasetFeatureType>
318
359
  }
319
360
  export interface DatasetFeatureAttributeValue {
320
- code?: string
321
- definition?: string
361
+ value: string // empty string when the value has no code
322
362
  label?: string
363
+ description?: string
323
364
  }
324
365
  export interface DatasetFeatureAttribute {
325
366
  name: string
326
- definition: string
327
- type: string
328
- code: string
367
+ label?: string
368
+ description?: string
369
+ type?: string
370
+ code?: string
329
371
  values?: Array<DatasetFeatureAttributeValue>
330
372
  }
331
- export interface DatasetFeatureType {
332
- aliases: string
333
- code: string
334
- isAbstract: string
335
- typeName: string
336
- definition: string
337
- attributeTable: Array<DatasetFeatureAttribute>
338
- }
339
373
 
340
374
  export type ReuseType = 'application' | 'map' | 'other'
341
375
 
@@ -7,6 +7,8 @@ export interface TermsAggregationParams {
7
7
  limit: number
8
8
  sort: AggregationSort
9
9
  filter?: string
10
+ includeValues?: string[]
11
+ excludeValues?: string[]
10
12
  }
11
13
  export interface HistogramAggregationParams {
12
14
  type: 'histogram'
@@ -6,17 +6,39 @@ export type FieldFilterByRange = {
6
6
  start?: Date
7
7
  end?: Date
8
8
  }
9
+ // matches util/shared's BoundingBox; duplicated here to avoid a circular
10
+ // dependency (util/shared already depends on common/domain)
11
+ export type FieldFilterByBoundingBox = [number, number, number, number]
9
12
 
10
13
  export type FieldFilter =
11
14
  | FieldFilterByExpression
12
15
  | FieldFilterByValues
13
16
  | FieldFilterByRange
17
+ | FieldFilterByBoundingBox
14
18
  export type FieldFilters = Record<FieldName, FieldFilter>
15
19
 
16
20
  export type QueryString = {
17
21
  query_string: string
18
22
  }
19
23
  export type QueryRange = {
20
- range: Record<string, { format: string; gte: string; lte: string }>
24
+ range: Record<
25
+ string,
26
+ { format: string; gte?: string; lte?: string; relation?: string }
27
+ >
21
28
  }
22
- export type FilterQuery = Array<QueryString | QueryRange>
29
+ export type QueryGeoShape = {
30
+ geo_shape: {
31
+ geom: {
32
+ shape: { type: string; coordinates: number[][] }
33
+ relation: string
34
+ }
35
+ }
36
+ }
37
+ export type QueryBool = {
38
+ bool: {
39
+ filter: QueryRange[]
40
+ }
41
+ }
42
+ export type FilterQuery = Array<
43
+ QueryString | QueryRange | QueryGeoShape | QueryBool
44
+ >
@@ -6,9 +6,5 @@ export const SortByEnum: Record<string, SortByField> = {
6
6
  RELEVANCY: ['desc', '_score'],
7
7
  QUALITY_SCORE: ['desc', 'qualityScore'],
8
8
  CHANGE_DATE: ['desc', 'changeDate'],
9
- RESOURCE_DATES: [
10
- ['desc', 'revisionDateForResource'],
11
- ['desc', 'publicationDateForResource'],
12
- ['desc', 'creationDateForResource'],
13
- ],
9
+ RESOURCE_DATE: ['desc', 'resourceDate.date'],
14
10
  }
@@ -10,21 +10,21 @@ import {
10
10
  CatalogRecord,
11
11
  DatasetFeatureCatalog,
12
12
  LanguageCode,
13
+ LinkedRecord,
13
14
  } from '../model/record'
14
15
 
15
16
  export abstract class RecordsRepositoryInterface {
16
17
  abstract search(params: SearchParams): Observable<SearchResults>
17
18
  abstract getMatchesCount(filters: FieldFilters): Observable<number>
18
19
  abstract getRecord(uniqueIdentifier: string): Observable<CatalogRecord | null>
19
- abstract getFeatureCatalog(
20
+ abstract queryLinkedFeatureCatalog(
20
21
  record: CatalogRecord
21
22
  ): Observable<DatasetFeatureCatalog | null>
22
23
  abstract aggregate(params: AggregationsParams): Observable<Aggregations>
23
24
  abstract getSimilarRecords(
24
25
  similarTo: CatalogRecord
25
26
  ): Observable<CatalogRecord[]>
26
- abstract getSources(record: CatalogRecord): Observable<CatalogRecord[]>
27
- abstract getSourceOf(record: CatalogRecord): Observable<CatalogRecord[]>
27
+ abstract getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]>
28
28
  abstract fuzzySearch(query: string): Observable<SearchResults>
29
29
  abstract canDuplicate(record: CatalogRecord): boolean
30
30
  abstract canDelete(record: CatalogRecord): Observable<boolean>