geonetwork-ui 2.11.0-dev.5c2e5295e → 2.11.0-dev.6804f3b43

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 (99) hide show
  1. package/fesm2022/geonetwork-ui.mjs +2533 -1984
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +323 -153
  4. package/index.d.ts.map +1 -1
  5. package/package.json +12 -11
  6. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +43 -13
  7. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +135 -35
  8. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  9. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  10. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  11. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +24 -2
  12. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  13. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  14. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  15. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  16. package/src/libs/feature/map/src/index.ts +0 -2
  17. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  18. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  19. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  20. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  21. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  22. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  23. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  24. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  25. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  26. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  27. package/src/libs/feature/search/src/index.ts +2 -0
  28. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  29. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +63 -19
  30. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +16 -7
  31. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  32. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  33. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +19 -8
  34. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  35. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +58 -4
  36. package/src/libs/feature/search/src/lib/utils/service/fields.ts +92 -9
  37. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  38. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  39. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  40. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  41. package/src/libs/ui/inputs/src/index.ts +2 -0
  42. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  43. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  44. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  45. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  46. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  47. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  48. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  49. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  50. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  51. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  52. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  53. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  54. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  55. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  56. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  57. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +221 -0
  58. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  59. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  60. package/src/libs/util/app-config/src/lib/app-config.ts +25 -0
  61. package/src/libs/util/app-config/src/lib/fixtures.ts +33 -1
  62. package/src/libs/util/app-config/src/lib/model.ts +15 -1
  63. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  64. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  65. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  66. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  67. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  68. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  69. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  70. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  71. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  72. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  73. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  74. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  75. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  76. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  77. package/src/libs/util/shared/src/index.ts +1 -0
  78. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  79. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  80. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  81. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  82. package/src/libs/util/shared/src/lib/utils/geojson.ts +24 -0
  83. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  84. package/tailwind.base.css +5 -0
  85. package/translations/de.json +22 -4
  86. package/translations/en.json +21 -3
  87. package/translations/es.json +19 -1
  88. package/translations/fr.json +26 -8
  89. package/translations/it.json +20 -2
  90. package/translations/nl.json +19 -1
  91. package/translations/pt.json +19 -1
  92. package/translations/sk.json +20 -2
  93. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  94. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  95. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  96. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  97. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
  98. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  99. /package/src/libs/{feature/map/src/lib/geocoding/geocoding.component.css → ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css} +0 -0
@@ -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 {
@@ -142,6 +168,7 @@ export class ElasticsearchService {
142
168
  values: uuids,
143
169
  },
144
170
  },
171
+ size: uuids.length,
145
172
  }
146
173
  }
147
174
 
@@ -263,8 +290,68 @@ export class ElasticsearchService {
263
290
  return this.metadataLang === 'current'
264
291
  }
265
292
 
266
- private filtersToQuery(
293
+ private findSpatialFilterExtent(
267
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)
268
355
  ): FilterQuery {
269
356
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
270
357
  const makeQuery = (filter: FieldFilter): string => {
@@ -285,50 +372,49 @@ export class ElasticsearchService {
285
372
  ? filters
286
373
  : Object.keys(filters)
287
374
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
375
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
288
376
  .filter((fieldname) => !isDateRange(filters[fieldname]))
377
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
289
378
  .filter(
290
379
  (fieldname) =>
291
380
  filters[fieldname] &&
292
381
  JSON.stringify(filters[fieldname]) !== '{}'
293
382
  )
294
383
  .map(
295
- (fieldname) => `${fieldname}:(${makeQuery(filters[fieldname])})`
384
+ (fieldname) =>
385
+ `${this.fieldAliases[fieldname] ?? fieldname}:(${makeQuery(filters[fieldname])})`
296
386
  )
297
387
  .join(' AND ')
298
388
  if (filters['gn-ui-crossFieldFilter']) {
299
389
  queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
300
390
  }
301
- const queryRange = Object.entries(filters)
302
- .filter(([, value]) => isDateRange(value))
303
- .map(([searchField, dateRange]) => {
304
- return {
305
- searchField,
306
- dateRange,
307
- } as {
308
- searchField: string
309
- dateRange: DateRange
310
- }
311
- })[0]
391
+ const queryRanges = Object.entries(filters).filter(([, value]) =>
392
+ isDateRange(value)
393
+ ) as [string, DateRange][]
312
394
  const queryParts = [
313
395
  queryString && {
314
396
  query_string: {
315
397
  query: queryString,
316
398
  },
317
399
  },
318
- queryRange &&
319
- queryRange.dateRange && {
320
- range: {
321
- [queryRange.searchField]: {
322
- ...(queryRange.dateRange.start && {
323
- gte: formatDate(queryRange.dateRange.start),
324
- }),
325
- ...(queryRange.dateRange.end && {
326
- lte: formatDate(queryRange.dateRange.end),
327
- }),
328
- 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
+ ],
329
413
  },
414
+ relation: 'intersects',
330
415
  },
331
416
  },
417
+ },
332
418
  ].filter(Boolean)
333
419
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
334
420
  }
@@ -367,7 +453,12 @@ export class ElasticsearchService {
367
453
  },
368
454
  })
369
455
  }
370
- 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
+ )
371
462
  if (queryFilters) {
372
463
  filter.push(...queryFilters)
373
464
  }
@@ -378,7 +469,10 @@ export class ElasticsearchService {
378
469
  },
379
470
  })
380
471
  }
381
- if (geometry) {
472
+ const boostGeometry = spatialFilterExtent
473
+ ? bboxToPolygon(spatialFilterExtent)
474
+ : geometry
475
+ if (boostGeometry) {
382
476
  // boosts applied using the filter geometry:
383
477
  // * records completely within the geometry receive a boost of 5
384
478
  // * records intersecting the geometry receive a boost of 2
@@ -388,7 +482,7 @@ export class ElasticsearchService {
388
482
  {
389
483
  geo_shape: {
390
484
  geom: {
391
- shape: geometry,
485
+ shape: boostGeometry,
392
486
  relation: 'within',
393
487
  },
394
488
  boost: 5.0,
@@ -397,7 +491,7 @@ export class ElasticsearchService {
397
491
  {
398
492
  geo_shape: {
399
493
  geom: {
400
- shape: geometry,
494
+ shape: boostGeometry,
401
495
  relation: 'intersects',
402
496
  },
403
497
  boost: 2.0,
@@ -408,7 +502,7 @@ export class ElasticsearchService {
408
502
  // this will boost the results variably depending on their distance from the given geometry
409
503
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
410
504
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
411
- const bbox = getGeometryBoundingBox(geometry)
505
+ const bbox = getGeometryBoundingBox(boostGeometry)
412
506
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
413
507
  const northToCenter = new LineString([
414
508
  [center[0], bbox[3]],
@@ -634,6 +728,12 @@ export class ElasticsearchService {
634
728
  order: {
635
729
  [`_${aggregation.sort[1]}`]: aggregation.sort[0],
636
730
  },
731
+ ...(aggregation.includeValues && {
732
+ include: aggregation.includeValues,
733
+ }),
734
+ ...(aggregation.excludeValues && {
735
+ exclude: aggregation.excludeValues,
736
+ }),
637
737
  },
638
738
  }
639
739
  case 'histogram':
@@ -14,10 +14,13 @@ 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
20
  DatasetFeatureType,
20
21
  LanguageCode,
22
+ LinkedRecord,
23
+ RecordRelation,
21
24
  } from '../../../../../../libs/common/domain/src/lib/model/record'
22
25
  import {
23
26
  Aggregations,
@@ -145,7 +148,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
145
148
  return this.gn4SearchApi
146
149
  .search(
147
150
  'bucket',
148
- ['fcats', 'hassources'],
151
+ ['fcats', 'hassources', 'siblings', 'associated'],
149
152
  JSON.stringify(
150
153
  this.gn4SearchHelper.getMetadataByIdsPayload([uniqueIdentifier])
151
154
  )
@@ -249,22 +252,43 @@ export class Gn4Repository implements RecordsRepositoryInterface {
249
252
  )
250
253
  }
251
254
 
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)
255
+ getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]> {
256
+ const siblings = (record.extras?.['siblings'] ?? []) as AssociatedRecord[]
257
+ const relations: Array<[RecordRelation, string[]]> = [
258
+ ['source', (record.extras?.['sourcesIdentifiers'] ?? []) as string[]],
259
+ ['sourceOf', (record.extras?.['sourceOfIdentifiers'] ?? []) as string[]],
260
+ ['sibling', siblings.map(({ uniqueIdentifier }) => uniqueIdentifier)],
261
+ [
262
+ 'associated',
263
+ (record.extras?.['associatedIdentifiers'] ?? []) as string[],
264
+ ],
265
+ ]
266
+ const requested = relations.filter(
267
+ ([, identifiers]) => identifiers.length > 0
268
+ )
269
+ if (requested.length === 0) {
270
+ return of([])
266
271
  }
267
- return of(null)
272
+ return forkJoin(
273
+ requested.map(([relation, identifiers]) =>
274
+ this.getMultipleRecords(identifiers).pipe(
275
+ map((records) =>
276
+ (records ?? []).map((record) => ({
277
+ record,
278
+ relation,
279
+ associationType:
280
+ relation === 'sibling'
281
+ ? siblings.find(
282
+ ({ uniqueIdentifier }) =>
283
+ uniqueIdentifier === record.uniqueIdentifier
284
+ )?.associationType
285
+ : undefined,
286
+ }))
287
+ ),
288
+ catchError(() => of([]))
289
+ )
290
+ )
291
+ ).pipe(map((groups) => groups.flat()))
268
292
  }
269
293
 
270
294
  aggregate(params: AggregationsParams): Observable<Aggregations> {
@@ -294,6 +294,18 @@ export interface AssociatedRecord {
294
294
  associationType: AssociationType
295
295
  }
296
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
307
+ }
308
+
297
309
  export interface DatasetRecord extends BaseRecord {
298
310
  kind: 'dataset'
299
311
  status: RecordStatus
@@ -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
+ >
@@ -10,6 +10,7 @@ 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 {
@@ -23,8 +24,7 @@ export abstract class RecordsRepositoryInterface {
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>
@@ -2,9 +2,9 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  ChangeDetectorRef,
4
4
  Component,
5
+ inject,
5
6
  Input,
6
7
  Output,
7
- inject,
8
8
  } from '@angular/core'
9
9
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
10
10
  import {
@@ -183,7 +183,7 @@ export class ChartViewComponent {
183
183
  yChoices$ = this.properties$.pipe(
184
184
  map((properties) =>
185
185
  properties
186
- .filter((prop) => prop.type === 'number' || prop.type === 'date')
186
+ .filter((prop) => prop.type === 'number')
187
187
  .map((prop) => ({ value: prop.name, label: prop.label || prop.name }))
188
188
  ),
189
189
  tap((choices) => {
@@ -222,6 +222,7 @@ export class ChartViewComponent {
222
222
  switchMap(([dataset, xProp, yProp, aggregation]) => {
223
223
  const fieldAgg: FieldAggregation =
224
224
  aggregation === 'count' ? ['count'] : [aggregation, yProp]
225
+ this.loading = true
225
226
  return dataset
226
227
  .groupBy(['distinct', xProp])
227
228
  .aggregate(fieldAgg)
@@ -2,11 +2,11 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  ChangeDetectorRef,
4
4
  Component,
5
+ inject,
5
6
  Input,
6
7
  OnDestroy,
7
8
  OnInit,
8
9
  ViewChild,
9
- inject,
10
10
  } from '@angular/core'
11
11
  import {
12
12
  DataTableComponent,
@@ -75,6 +75,7 @@ export class GeoTableViewComponent implements OnInit, OnDestroy {
75
75
  }
76
76
 
77
77
  private async initMapContext(): Promise<MapContext> {
78
+ this.dataset.load()
78
79
  this.dataset.selectAll()
79
80
  return {
80
81
  layers: [
@@ -1,15 +1,15 @@
1
- import { Injectable, inject } from '@angular/core'
1
+ import { inject, Injectable } from '@angular/core'
2
2
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
3
3
  import {
4
+ GetCollectionItemsOptions,
4
5
  OgcApiCollectionInfo,
5
6
  OgcApiEndpoint,
6
7
  OgcApiRecord,
7
- WfsEndpoint,
8
- WfsVersion,
9
- TmsEndpoint,
10
8
  StacEndpoint,
11
- GetCollectionItemsOptions,
12
9
  StacItemsDocument,
10
+ TmsEndpoint,
11
+ WfsEndpoint,
12
+ WfsVersion,
13
13
  } from '@camptocamp/ogc-client'
14
14
  import {
15
15
  BaseReader,
@@ -26,7 +26,7 @@ import {
26
26
  } from '../../../../../../libs/util/shared/src'
27
27
  import type { FeatureCollection } from 'geojson'
28
28
  import { from, Observable, throwError } from 'rxjs'
29
- import { catchError, map, switchMap, tap } from 'rxjs/operators'
29
+ import { catchError, map, switchMap } from 'rxjs/operators'
30
30
  import {
31
31
  DatasetOnlineResource,
32
32
  DatasetServiceDistribution,
@@ -347,14 +347,11 @@ export class DataService {
347
347
  if (link.type === 'service' && link.accessServiceProtocol === 'wfs') {
348
348
  const wfsUrlEndpoint = this.proxy.getProxiedUrl(link.url.toString())
349
349
  return from(
350
- openDataset(
351
- wfsUrlEndpoint,
352
- 'wfs',
353
- {
354
- wfsFeatureType: link.name,
355
- },
356
- cacheActive
357
- )
350
+ openDataset(wfsUrlEndpoint, {
351
+ typeHint: 'wfs',
352
+ wfsFeatureType: link.name,
353
+ enableCache: cacheActive,
354
+ })
358
355
  )
359
356
  } else if (link.type === 'download') {
360
357
  const linkProxifiedUrl = this.proxy.getProxiedUrl(link.url.toString())
@@ -364,7 +361,10 @@ export class DataService {
364
361
  ? (format as SupportedType)
365
362
  : undefined
366
363
  return from(
367
- openDataset(linkProxifiedUrl, supportedType, undefined, cacheActive)
364
+ openDataset(linkProxifiedUrl, {
365
+ typeHint: supportedType,
366
+ enableCache: cacheActive,
367
+ })
368
368
  ).pipe()
369
369
  } else if (
370
370
  link.type === 'service' &&
@@ -374,7 +374,9 @@ export class DataService {
374
374
  link.url.toString(),
375
375
  'geojson'
376
376
  )
377
- return from(openDataset(url, 'geojson', undefined, cacheActive)).pipe()
377
+ return from(
378
+ openDataset(url, { typeHint: 'geojson', enableCache: cacheActive })
379
+ ).pipe()
378
380
  } else if (
379
381
  link.type === 'service' &&
380
382
  link.accessServiceProtocol === 'ogcFeatures'
@@ -395,12 +397,10 @@ export class DataService {
395
397
  }
396
398
  const urlWithoutLimit = new URL(geojsonUrl)
397
399
  urlWithoutLimit.searchParams.delete('limit')
398
- return openDataset(
399
- urlWithoutLimit.toString(),
400
- 'geojson',
401
- undefined,
402
- cacheActive
403
- )
400
+ return openDataset(urlWithoutLimit.toString(), {
401
+ typeHint: 'geojson',
402
+ enableCache: cacheActive,
403
+ })
404
404
  })
405
405
  )
406
406
  }
@@ -6,8 +6,6 @@ export * from './lib/feature-map.module'
6
6
  export * from './lib/map-state-container/map-state-container.component'
7
7
  export * from './lib/constant'
8
8
  export * from './lib/utils'
9
- export * from './lib/style'
10
9
  export * from './lib/layers-panel/layers-panel.component'
11
10
  export * from './lib/add-layer-from-catalog/add-layer-from-catalog.component'
12
11
  export * from './lib/add-layer-from-catalog/add-layer-record-preview/add-layer-record-preview.component'
13
- export * from './lib/geocoding/geocoding.component'
@@ -5,6 +5,7 @@
5
5
  (fileChange)="handleFileChange($event)"
6
6
  [accept]="acceptedMimeType.join(',')"
7
7
  [placeholder]="'map.addFromFile.placeholder' | translate"
8
+ textClass="text-gray-900 pl-2 py-2"
8
9
  class="placeholder-grey"
9
10
  ></gn-ui-drag-and-drop-file-input>
10
11
  </div>
@@ -2,18 +2,11 @@ import { NgModule } from '@angular/core'
2
2
  import { StoreModule } from '@ngrx/store'
3
3
  import * as fromMap from './+state/map.reducer'
4
4
  import { MapFacade } from './+state/map.facade'
5
- import { GEOCODING_PROVIDER, GeocodingProvider } from './geocoding.service'
6
5
 
7
6
  @NgModule({
8
7
  imports: [
9
8
  StoreModule.forFeature(fromMap.MAP_FEATURE_KEY, fromMap.mapReducer),
10
9
  ],
11
- providers: [
12
- MapFacade,
13
- {
14
- provide: GEOCODING_PROVIDER,
15
- useValue: ['geonames', { maxRows: 5 }] as GeocodingProvider,
16
- },
17
- ],
10
+ providers: [MapFacade],
18
11
  })
19
12
  export class FeatureMapModule {}
@@ -18,10 +18,12 @@ export class NotificationsService {
18
18
  error && console.error(error)
19
19
  const id = Math.floor(Math.random() * 1000000)
20
20
  this.notifications$.next([...this.notifications$.value, { ...content, id }])
21
- if (typeof timeoutMs === 'undefined') return
22
- setTimeout(() => {
23
- this.removeNotificationById(id)
24
- }, timeoutMs)
21
+ if (typeof timeoutMs !== 'undefined') {
22
+ setTimeout(() => {
23
+ this.removeNotificationById(id)
24
+ }, timeoutMs)
25
+ }
26
+ return id
25
27
  }
26
28
 
27
29
  removeNotificationById(id: number) {