geonetwork-ui 2.11.0-dev.80a8b0bfa → 2.11.0-dev.840dcc932

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 (105) hide show
  1. package/fesm2022/geonetwork-ui.mjs +2273 -1474
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +342 -136
  4. package/index.d.ts.map +1 -1
  5. package/package.json +7 -5
  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/mel.records.dataset+featurecatalog.ts +395 -0
  8. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +35 -8
  9. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +19 -0
  10. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +17 -6
  11. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +110 -0
  12. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +131 -0
  13. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +9 -0
  14. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +2 -1
  15. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +134 -35
  16. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +83 -35
  17. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +11 -0
  18. package/src/libs/common/domain/src/lib/model/record/flags.model.ts +1 -0
  19. package/src/libs/common/domain/src/lib/model/record/index.ts +1 -0
  20. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +14 -18
  21. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  22. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +24 -2
  23. package/src/libs/common/domain/src/lib/platform.service.interface.ts +1 -0
  24. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +20 -1
  25. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
  26. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +32 -19
  27. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints/form-field-constraints.component.ts +3 -5
  28. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +2 -2
  29. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.html +18 -0
  30. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.ts +86 -0
  31. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +100 -88
  32. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +11 -4
  33. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +18 -4
  34. package/src/libs/feature/editor/src/lib/expressions.ts +67 -0
  35. package/src/libs/feature/editor/src/lib/fields.config.ts +13 -0
  36. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +10 -4
  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.effects.ts +3 -1
  43. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -1
  44. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  45. package/src/libs/feature/search/src/index.ts +2 -0
  46. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +18 -4
  47. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +80 -19
  48. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +105 -0
  49. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +18 -0
  50. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +114 -0
  51. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +19 -8
  52. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  53. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +64 -4
  54. package/src/libs/feature/search/src/lib/utils/service/fields.ts +92 -9
  55. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +3 -3
  56. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  57. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  58. package/src/libs/ui/inputs/src/index.ts +2 -0
  59. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  60. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +12 -3
  61. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +11 -3
  62. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  63. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  64. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  65. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  66. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  67. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  68. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  69. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  70. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  71. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  72. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  73. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  74. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  75. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  76. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +117 -0
  77. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +244 -0
  78. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  79. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  80. package/src/libs/util/app-config/src/lib/app-config.ts +37 -0
  81. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  82. package/src/libs/util/app-config/src/lib/model.ts +25 -1
  83. package/src/libs/util/shared/src/index.ts +1 -0
  84. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  85. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  86. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  87. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  88. package/src/libs/util/shared/src/lib/utils/geojson.ts +24 -0
  89. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  90. package/tailwind.base.css +5 -0
  91. package/translations/de.json +28 -4
  92. package/translations/en.json +27 -3
  93. package/translations/es.json +26 -2
  94. package/translations/fr.json +32 -8
  95. package/translations/it.json +26 -2
  96. package/translations/nl.json +26 -2
  97. package/translations/pt.json +26 -2
  98. package/translations/sk.json +26 -2
  99. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  100. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  101. package/src/libs/feature/map/src/lib/geocoding.service.ts +0 -57
  102. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  103. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  104. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
  105. /package/src/libs/feature/{map/src/lib/geocoding/geocoding.component.css → editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.css} +0 -0
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  CatalogRecord,
3
+ DatasetFeatureAttribute,
4
+ DatasetFeatureAttributeValue,
5
+ DatasetFeatureType,
3
6
  DatasetRecord,
4
7
  Individual,
5
8
  LanguageCode,
@@ -616,3 +619,131 @@ export function writeAssociatedRecords(
616
619
  )
617
620
  )(rootEl)
618
621
  }
622
+
623
+ function createNilElement(name: string, nilReason: string) {
624
+ return pipe(createElement(name), writeAttribute('gco:nilReason', nilReason))
625
+ }
626
+
627
+ function createFeatureCatalogueShell() {
628
+ return pipe(
629
+ createNestedElement(
630
+ 'mdb:contentInfo',
631
+ 'mrc:MD_FeatureCatalogue',
632
+ 'mrc:featureCatalogue',
633
+ 'gfc:FC_FeatureCatalogue'
634
+ ),
635
+ // create mandatory elements which are not part of the model yet
636
+ appendChildren(
637
+ createNilElement('cat:name', 'missing'),
638
+ createNilElement('cat:scope', 'missing'),
639
+ createNilElement('cat:versionNumber', 'missing'),
640
+ createNilElement('cat:versionDate', 'missing'),
641
+ createNilElement('gfc:producer', 'missing')
642
+ )
643
+ )
644
+ }
645
+
646
+ function createListedValue(value: DatasetFeatureAttributeValue) {
647
+ return pipe(
648
+ createNestedElement('gfc:listedValue', 'gfc:FC_ListedValue'),
649
+ appendChildren(
650
+ value.label
651
+ ? pipe(createElement('gfc:label'), writeCharacterString(value.label))
652
+ : createNilElement('gfc:label', 'missing'),
653
+ value.value
654
+ ? pipe(createElement('gfc:code'), writeCharacterString(value.value))
655
+ : null,
656
+ value.description
657
+ ? pipe(
658
+ createElement('gfc:definition'),
659
+ writeCharacterString(value.description)
660
+ )
661
+ : null
662
+ )
663
+ )
664
+ }
665
+
666
+ function createFeatureAttribute(attribute: DatasetFeatureAttribute) {
667
+ return pipe(
668
+ createNestedElement(
669
+ 'gfc:carrierOfCharacteristics',
670
+ 'gfc:FC_FeatureAttribute'
671
+ ),
672
+ appendChildren(
673
+ pipe(createElement('gfc:memberName'), setTextContent(attribute.name)),
674
+ attribute.description
675
+ ? pipe(
676
+ createElement('gfc:definition'),
677
+ writeCharacterString(attribute.description)
678
+ )
679
+ : null,
680
+ createNilElement('gfc:cardinality', 'missing'),
681
+ attribute.label
682
+ ? pipe(
683
+ createElement('gfc:designation'),
684
+ writeCharacterString(attribute.label)
685
+ )
686
+ : null,
687
+ attribute.code
688
+ ? pipe(createElement('gfc:code'), writeCharacterString(attribute.code))
689
+ : null,
690
+ attribute.type
691
+ ? pipe(
692
+ createNestedElement('gfc:valueType', 'gco:TypeName', 'gco:aName'),
693
+ writeCharacterString(attribute.type)
694
+ )
695
+ : null,
696
+ ...(attribute.values ?? [])
697
+ .filter((value) => value.value || value.label)
698
+ .map(createListedValue)
699
+ )
700
+ )
701
+ }
702
+
703
+ function createFeatureType(featureType: DatasetFeatureType) {
704
+ return pipe(
705
+ createNestedElement('gfc:featureType', 'gfc:FC_FeatureType'),
706
+ appendChildren(
707
+ pipe(createElement('gfc:typeName'), setTextContent(featureType.name)),
708
+ featureType.description
709
+ ? pipe(
710
+ createElement('gfc:definition'),
711
+ writeCharacterString(featureType.description)
712
+ )
713
+ : null,
714
+ pipe(
715
+ createNestedElement('gfc:isAbstract', 'gco:Boolean'),
716
+ setTextContent('false')
717
+ ),
718
+ featureType.label
719
+ ? pipe(
720
+ createElement('gfc:designation'),
721
+ writeCharacterString(featureType.label)
722
+ )
723
+ : null,
724
+ ...featureType.attributes.map(createFeatureAttribute),
725
+ createNilElement('gfc:featureCatalogue', 'missing')
726
+ )
727
+ )
728
+ }
729
+
730
+ export function writeFeatureTypeDescriptions(
731
+ record: DatasetRecord,
732
+ rootEl: XmlElement
733
+ ) {
734
+ const featureTypes = record.featureTypeDescriptions ?? []
735
+ const catalogues = findNestedElements(
736
+ 'mdb:contentInfo',
737
+ 'mrc:MD_FeatureCatalogue',
738
+ 'mrc:featureCatalogue',
739
+ 'gfc:FC_FeatureCatalogue'
740
+ )(rootEl)
741
+ catalogues.forEach((catalogue) =>
742
+ removeChildrenByName('gfc:featureType')(catalogue)
743
+ )
744
+ if (!featureTypes.length) return
745
+
746
+ const catalogue =
747
+ catalogues[0] ?? appendChildTree(createFeatureCatalogueShell())(rootEl)
748
+ appendChildren(...featureTypes.map(createFeatureType))(catalogue)
749
+ }
@@ -118,6 +118,7 @@ export class Iso19139Converter extends BaseConverter<string> {
118
118
  lineage: readLineage,
119
119
  sourceRecords: readSourceRecords,
120
120
  associatedRecords: readAssociatedRecords,
121
+ featureTypeDescriptions: () => undefined,
121
122
  onlineResources: readOnlineResources,
122
123
  temporalExtents: readTemporalExtents,
123
124
  spatialExtents: readSpatialExtents,
@@ -161,6 +162,7 @@ export class Iso19139Converter extends BaseConverter<string> {
161
162
  lineage: writeLineage,
162
163
  sourceRecords: writeSourceRecords,
163
164
  associatedRecords: writeAssociatedRecords,
165
+ featureTypeDescriptions: () => undefined,
164
166
  onlineResources: writeOnlineResources,
165
167
  temporalExtents: writeTemporalExtents,
166
168
  spatialExtents: writeSpatialExtents,
@@ -306,6 +308,10 @@ export class Iso19139Converter extends BaseConverter<string> {
306
308
  const sourceRecords = this.readers['sourceRecords'](rootEl, tr)
307
309
  const associatedRecords = this.readers['associatedRecords'](rootEl, tr)
308
310
  const updateFrequency = this.readers['updateFrequency'](rootEl, tr)
311
+ const featureTypeDescriptions = this.readers['featureTypeDescriptions'](
312
+ rootEl,
313
+ tr
314
+ ) as DatasetRecord['featureTypeDescriptions']
309
315
 
310
316
  return this.afterRecordRead({
311
317
  ...this.readBaseRecord(rootEl, tr),
@@ -317,6 +323,7 @@ export class Iso19139Converter extends BaseConverter<string> {
317
323
  ...(spatialRepresentation && { spatialRepresentation }),
318
324
  temporalExtents,
319
325
  updateFrequency,
326
+ ...(featureTypeDescriptions?.length && { featureTypeDescriptions }),
320
327
  translations: tr,
321
328
  } as DatasetRecord)
322
329
  } else if (kind === 'reuse') {
@@ -416,6 +423,8 @@ export class Iso19139Converter extends BaseConverter<string> {
416
423
 
417
424
  if (record.kind === 'dataset') {
418
425
  fieldChanged('status') && this.writers['status'](record, rootEl)
426
+ fieldChanged('featureTypeDescriptions') &&
427
+ this.writers['featureTypeDescriptions'](record, rootEl)
419
428
  }
420
429
  if (record.kind === 'dataset' || record.kind === 'reuse') {
421
430
  fieldChanged('updateFrequency') &&
@@ -69,7 +69,8 @@ export function extractCharacterString(): ChainableFunction<
69
69
  fallback(
70
70
  findChildElement('gco:CharacterString', false),
71
71
  findChildElement('gmx:Anchor', false),
72
- findChildElement('gmx:MimeFileType', false)
72
+ findChildElement('gmx:MimeFileType', false),
73
+ findChildElement('gco:LocalName', false)
73
74
  ),
74
75
  readText()
75
76
  )
@@ -34,7 +34,12 @@ import {
34
34
  LanguageCode,
35
35
  } from '../../../../../../../libs/common/domain/src/lib/model/record'
36
36
  import { TranslateService } from '@ngx-translate/core'
37
- import { getGeometryBoundingBox } from '../../../../../../../libs/util/shared/src'
37
+ import {
38
+ bboxToPolygon,
39
+ BoundingBox,
40
+ getGeometryBoundingBox,
41
+ isBoundingBox,
42
+ } from '../../../../../../../libs/util/shared/src'
38
43
  import { getLength as getGeodesicLength } from 'ol/sphere.js'
39
44
  import { LineString } from 'ol/geom.js'
40
45
 
@@ -49,7 +54,12 @@ export class ElasticsearchService {
49
54
 
50
55
  // runtime fields are computed using a Painless script
51
56
  // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
52
- 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> = {}
53
63
 
54
64
  // we're using getters in case the defined languages change over time
55
65
  private get metadataLang(): LanguageCode {
@@ -91,8 +101,8 @@ export class ElasticsearchService {
91
101
  const addMapping = (fieldName: string) => {
92
102
  if (!payload.runtime_mappings) payload.runtime_mappings = {}
93
103
  payload.runtime_mappings[fieldName] = {
94
- type: 'keyword',
95
- script: this.runtimeFields[fieldName],
104
+ type: this.runtimeFields[fieldName].type,
105
+ script: this.runtimeFields[fieldName].script,
96
106
  }
97
107
  }
98
108
  const lookForField = (node: unknown) => {
@@ -113,6 +123,14 @@ export class ElasticsearchService {
113
123
  ) {
114
124
  addMapping(runtimeField)
115
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
+ }
116
134
  if (
117
135
  'query' in node &&
118
136
  typeof node.query === 'string' &&
@@ -131,8 +149,16 @@ export class ElasticsearchService {
131
149
  return payload
132
150
  }
133
151
 
134
- registerRuntimeField(fieldName: string, expression: string) {
135
- 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
136
162
  }
137
163
 
138
164
  getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
@@ -264,8 +290,68 @@ export class ElasticsearchService {
264
290
  return this.metadataLang === 'current'
265
291
  }
266
292
 
267
- private filtersToQuery(
293
+ private findSpatialFilterExtent(
268
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)
269
355
  ): FilterQuery {
270
356
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
271
357
  const makeQuery = (filter: FieldFilter): string => {
@@ -286,50 +372,49 @@ export class ElasticsearchService {
286
372
  ? filters
287
373
  : Object.keys(filters)
288
374
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
375
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
289
376
  .filter((fieldname) => !isDateRange(filters[fieldname]))
377
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
290
378
  .filter(
291
379
  (fieldname) =>
292
380
  filters[fieldname] &&
293
381
  JSON.stringify(filters[fieldname]) !== '{}'
294
382
  )
295
383
  .map(
296
- (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`
384
+ (fieldname) =>
385
+ `${this.fieldAliases[fieldname] ?? fieldname}:(${makeQuery(filters[fieldname])})`
297
386
  )
298
387
  .join(' AND ')
299
388
  if (filters['gn-ui-crossFieldFilter']) {
300
389
  queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
301
390
  }
302
- const queryRange = Object.entries(filters)
303
- .filter(([, value]) => isDateRange(value))
304
- .map(([searchField, dateRange]) => {
305
- return {
306
- searchField,
307
- dateRange,
308
- } as {
309
- searchField: string
310
- dateRange: DateRange
311
- }
312
- })[0]
391
+ const queryRanges = Object.entries(filters).filter(([, value]) =>
392
+ isDateRange(value)
393
+ ) as [string, DateRange][]
313
394
  const queryParts = [
314
395
  queryString && {
315
396
  query_string: {
316
397
  query: queryString,
317
398
  },
318
399
  },
319
- queryRange &&
320
- queryRange.dateRange && {
321
- range: {
322
- [queryRange.searchField]: {
323
- ...(queryRange.dateRange.start && {
324
- gte: formatDate(queryRange.dateRange.start),
325
- }),
326
- ...(queryRange.dateRange.end && {
327
- lte: formatDate(queryRange.dateRange.end),
328
- }),
329
- 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
+ ],
330
413
  },
414
+ relation: 'intersects',
331
415
  },
332
416
  },
417
+ },
333
418
  ].filter(Boolean)
334
419
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
335
420
  }
@@ -368,7 +453,12 @@ export class ElasticsearchService {
368
453
  },
369
454
  })
370
455
  }
371
- 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
+ )
372
462
  if (queryFilters) {
373
463
  filter.push(...queryFilters)
374
464
  }
@@ -379,7 +469,10 @@ export class ElasticsearchService {
379
469
  },
380
470
  })
381
471
  }
382
- if (geometry) {
472
+ const boostGeometry = spatialFilterExtent
473
+ ? bboxToPolygon(spatialFilterExtent)
474
+ : geometry
475
+ if (boostGeometry) {
383
476
  // boosts applied using the filter geometry:
384
477
  // * records completely within the geometry receive a boost of 5
385
478
  // * records intersecting the geometry receive a boost of 2
@@ -389,7 +482,7 @@ export class ElasticsearchService {
389
482
  {
390
483
  geo_shape: {
391
484
  geom: {
392
- shape: geometry,
485
+ shape: boostGeometry,
393
486
  relation: 'within',
394
487
  },
395
488
  boost: 5.0,
@@ -398,7 +491,7 @@ export class ElasticsearchService {
398
491
  {
399
492
  geo_shape: {
400
493
  geom: {
401
- shape: geometry,
494
+ shape: boostGeometry,
402
495
  relation: 'intersects',
403
496
  },
404
497
  boost: 2.0,
@@ -409,7 +502,7 @@ export class ElasticsearchService {
409
502
  // this will boost the results variably depending on their distance from the given geometry
410
503
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
411
504
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
412
- const bbox = getGeometryBoundingBox(geometry)
505
+ const bbox = getGeometryBoundingBox(boostGeometry)
413
506
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
414
507
  const northToCenter = new LineString([
415
508
  [center[0], bbox[3]],
@@ -635,6 +728,12 @@ export class ElasticsearchService {
635
728
  order: {
636
729
  [`_${aggregation.sort[1]}`]: aggregation.sort[0],
637
730
  },
731
+ ...(aggregation.includeValues && {
732
+ include: aggregation.includeValues,
733
+ }),
734
+ ...(aggregation.excludeValues && {
735
+ exclude: aggregation.excludeValues,
736
+ }),
638
737
  },
639
738
  }
640
739
  case 'histogram':
@@ -17,9 +17,9 @@ import {
17
17
  AssociatedRecord,
18
18
  CatalogRecord,
19
19
  DatasetFeatureCatalog,
20
- DatasetFeatureType,
21
20
  LanguageCode,
22
21
  LinkedRecord,
22
+ RecordFlag,
23
23
  RecordRelation,
24
24
  } from '../../../../../../libs/common/domain/src/lib/model/record'
25
25
  import {
@@ -37,6 +37,7 @@ import {
37
37
  LanguagesApiService,
38
38
  RecordsApiService,
39
39
  SearchApiService,
40
+ TagsApiService,
40
41
  } from '../../../../../../libs/data-access/gn4/src'
41
42
  import {
42
43
  combineLatest,
@@ -49,7 +50,13 @@ import {
49
50
  switchMap,
50
51
  throwError,
51
52
  } from 'rxjs'
52
- import { catchError, map, tap } from 'rxjs/operators'
53
+ import {
54
+ catchError,
55
+ map,
56
+ shareReplay,
57
+ tap,
58
+ withLatestFrom,
59
+ } from 'rxjs/operators'
53
60
  import { lt } from 'semver'
54
61
  import { ElasticsearchService } from './elasticsearch'
55
62
  import { toLang2 } from '../../../../../../libs/util/i18n/src'
@@ -81,6 +88,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
81
88
  private platformService = inject(PlatformServiceInterface)
82
89
  private gn4LanguagesApi = inject(LanguagesApiService)
83
90
  private settingsService = inject(Gn4SettingsService)
91
+ private gn4TagsApi = inject(TagsApiService)
84
92
  private disableDraft = inject(DISABLE_DRAFT, { optional: true }) ?? false
85
93
  private defaultConverter = inject(DEFAULT_RECORD_CONVERTER)
86
94
 
@@ -182,43 +190,15 @@ export class Gn4Repository implements RecordsRepositoryInterface {
182
190
  )
183
191
  }
184
192
 
185
- private mapEmbeddedFeatureCatalog(
186
- featureTypes: Array<DatasetFeatureType>
187
- ): DatasetFeatureCatalog {
188
- return {
189
- featureTypes: featureTypes.map((featureType) => ({
190
- name: featureType.typeName || '',
191
- definition: featureType.definition || '',
192
- attributes: Array.isArray(featureType.attributeTable)
193
- ? featureType.attributeTable.map((attr) => {
194
- const values = attr.values
195
- ?.filter((v) => v.code || v.label)
196
- .map((v) => ({
197
- code: v.code,
198
- label: v.label,
199
- }))
200
- return {
201
- name: attr.name,
202
- code: attr.code,
203
- definition: attr.definition,
204
- type: attr.type,
205
- ...(values?.length > 0 ? { values } : {}),
206
- }
207
- })
208
- : [],
209
- })),
210
- }
211
- }
212
- getFeatureCatalog(
193
+ queryLinkedFeatureCatalog(
213
194
  record: CatalogRecord,
214
195
  visited: Set<string> = new Set() // prevent looping
215
196
  ): Observable<DatasetFeatureCatalog | null> {
216
197
  if (
217
- record.extras?.['featureTypes'] &&
218
- Array.isArray(record.extras['featureTypes']) &&
219
- record.extras['featureTypes'].length > 0
198
+ 'featureTypeDescriptions' in record &&
199
+ record.featureTypeDescriptions?.length > 0
220
200
  ) {
221
- return of(this.mapEmbeddedFeatureCatalog(record.extras['featureTypes']))
201
+ return of({ featureTypes: record.featureTypeDescriptions })
222
202
  }
223
203
 
224
204
  const featureCatalogIdentifier = record.extras[
@@ -228,7 +208,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
228
208
  visited.add(featureCatalogIdentifier)
229
209
  return this.getRecord(featureCatalogIdentifier).pipe(
230
210
  switchMap((record) =>
231
- record ? this.getFeatureCatalog(record, visited) : of(null)
211
+ record ? this.queryLinkedFeatureCatalog(record, visited) : of(null)
232
212
  )
233
213
  )
234
214
  }
@@ -704,4 +684,72 @@ export class Gn4Repository implements RecordsRepositoryInterface {
704
684
  private removeRecordFromLocalStorage(recordId: string): void {
705
685
  window.localStorage.removeItem(this.getLocalStorageKeyForRecord(recordId))
706
686
  }
687
+
688
+ /*
689
+ * FLAGS
690
+ * Note: in GN4 API, categories are named "tags"
691
+ */
692
+
693
+ categoriesIdentifierForFlags: Observable<Record<RecordFlag, number>> =
694
+ this.gn4TagsApi.getTags().pipe(
695
+ map((categories) =>
696
+ categories.reduce(
697
+ (acc, category) => {
698
+ if (category.name.startsWith('geonetwork-ui-flag:')) {
699
+ acc[
700
+ category.name.replace(/^geonetwork-ui-flag:/, '') as RecordFlag
701
+ ] = category.id
702
+ }
703
+ return acc
704
+ },
705
+ {} as Record<RecordFlag, number>
706
+ )
707
+ ),
708
+ shareReplay(1)
709
+ )
710
+
711
+ private getCategoryIdForFlag(
712
+ flagName: RecordFlag
713
+ ): Observable<number | null> {
714
+ return this.categoriesIdentifierForFlags.pipe(
715
+ map((categories) => {
716
+ const categoryId = categories[flagName]
717
+ if (categoryId === undefined) {
718
+ console.warn(
719
+ `The record flag ${flagName} is not created on GeoNetwork backend; please create a category with the name geonetwork-ui-flag:${flagName} for this to work.`
720
+ )
721
+ return null
722
+ }
723
+ return categoryId
724
+ })
725
+ )
726
+ }
727
+
728
+ setRecordFlag(
729
+ uniqueIdentifier: string,
730
+ flagName: RecordFlag,
731
+ value: boolean
732
+ ): Observable<void> {
733
+ return this.getCategoryIdForFlag(flagName).pipe(
734
+ switchMap((categoryId) => {
735
+ if (categoryId === null) return of()
736
+ return value
737
+ ? this.gn4RecordsApi.tagRecord(uniqueIdentifier, [categoryId], false)
738
+ : this.gn4RecordsApi.deleteTags(uniqueIdentifier, [categoryId])
739
+ })
740
+ )
741
+ }
742
+
743
+ getRecordFlag(
744
+ uniqueIdentifier: string,
745
+ flagName: RecordFlag
746
+ ): Observable<boolean> {
747
+ return this.gn4RecordsApi.getRecordTags(uniqueIdentifier).pipe(
748
+ withLatestFrom(this.getCategoryIdForFlag(flagName)),
749
+ map(([tags, categoryId]) => {
750
+ if (categoryId === null) return false
751
+ return Array.from(tags).some((tag) => tag.id === categoryId)
752
+ })
753
+ )
754
+ }
707
755
  }
@@ -92,6 +92,13 @@ export class Gn4PlatformService implements PlatformServiceInterface {
92
92
  switchMap((apiUser) => this.mapper.userFromMeApi(apiUser)),
93
93
  shareReplay({ bufferSize: 1, refCount: true })
94
94
  )
95
+ private isAdmin$ = this.disableAuth
96
+ ? of(false)
97
+ : of(true).pipe(
98
+ switchMap(() => this.meApi.getMe()),
99
+ map((apiUser) => !!apiUser?.admin),
100
+ shareReplay({ bufferSize: 1, refCount: true })
101
+ )
95
102
 
96
103
  /**
97
104
  * A map of already loaded thesauri (groups of keywords); the key is a URI
@@ -146,6 +153,10 @@ export class Gn4PlatformService implements PlatformServiceInterface {
146
153
  return this.isUserAnonymous$
147
154
  }
148
155
 
156
+ isAdministrator(): Observable<boolean> {
157
+ return this.isAdmin$
158
+ }
159
+
149
160
  getOrganizations(): Observable<Organization[]> {
150
161
  return undefined
151
162
  }
@@ -0,0 +1 @@
1
+ export type RecordFlag = 'IS_REFERENCE_DATASET'
@@ -3,3 +3,4 @@ export * from './metadata.model'
3
3
  export * from './organization.model'
4
4
  export * from './translation.model'
5
5
  export * from './user-feedbacks.model'
6
+ export * from './flags.model'