geonetwork-ui 2.11.0-dev.96d0c6ce2 → 2.11.0-dev.9bd41a961

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 (95) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1506 -938
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +295 -127
  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/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts +13 -1
  8. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +130 -37
  9. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  10. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  11. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
  12. package/src/libs/common/domain/src/lib/model/search/sort-by.model.ts +1 -5
  13. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  14. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  15. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  16. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  17. package/src/libs/feature/map/src/index.ts +0 -1
  18. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  19. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  20. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  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/router.service.ts +1 -1
  27. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  28. package/src/libs/feature/search/src/index.ts +2 -0
  29. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  30. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  31. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +5 -1
  32. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  33. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  34. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
  35. package/src/libs/feature/search/src/lib/sort-by/sort-by.component.ts +1 -1
  36. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
  37. package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
  38. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  39. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  40. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  41. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  42. package/src/libs/ui/inputs/src/index.ts +2 -0
  43. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  44. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  45. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  46. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  47. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  48. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  49. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  50. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  51. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  52. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  53. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  54. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  55. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  56. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  57. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  58. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -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 +3 -0
  61. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  62. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  63. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  64. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  65. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  66. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  67. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  68. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  69. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  70. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  71. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  72. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  73. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  74. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  75. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  76. package/src/libs/util/shared/src/index.ts +1 -0
  77. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  78. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  79. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  80. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  81. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  82. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  83. package/tailwind.base.css +5 -0
  84. package/translations/de.json +18 -4
  85. package/translations/en.json +17 -3
  86. package/translations/es.json +15 -1
  87. package/translations/fr.json +18 -4
  88. package/translations/it.json +16 -2
  89. package/translations/nl.json +15 -1
  90. package/translations/pt.json +15 -1
  91. package/translations/sk.json +16 -2
  92. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  93. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  94. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  95. /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
@@ -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,10 @@ 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
+ > = {}
52
61
 
53
62
  // we're using getters in case the defined languages change over time
54
63
  private get metadataLang(): LanguageCode {
@@ -90,8 +99,8 @@ export class ElasticsearchService {
90
99
  const addMapping = (fieldName: string) => {
91
100
  if (!payload.runtime_mappings) payload.runtime_mappings = {}
92
101
  payload.runtime_mappings[fieldName] = {
93
- type: 'keyword',
94
- script: this.runtimeFields[fieldName],
102
+ type: this.runtimeFields[fieldName].type,
103
+ script: this.runtimeFields[fieldName].script,
95
104
  }
96
105
  }
97
106
  const lookForField = (node: unknown) => {
@@ -112,6 +121,14 @@ export class ElasticsearchService {
112
121
  ) {
113
122
  addMapping(runtimeField)
114
123
  }
124
+ if (
125
+ runtimeField in node &&
126
+ typeof node[runtimeField] === 'object' &&
127
+ node[runtimeField] !== null &&
128
+ ('gte' in node[runtimeField] || 'lte' in node[runtimeField])
129
+ ) {
130
+ addMapping(runtimeField)
131
+ }
115
132
  if (
116
133
  'query' in node &&
117
134
  typeof node.query === 'string' &&
@@ -130,8 +147,12 @@ export class ElasticsearchService {
130
147
  return payload
131
148
  }
132
149
 
133
- registerRuntimeField(fieldName: string, expression: string) {
134
- this.runtimeFields[fieldName] = expression
150
+ registerRuntimeField(
151
+ fieldName: string,
152
+ expression: string,
153
+ type: 'keyword' | 'date' = 'keyword'
154
+ ) {
155
+ this.runtimeFields[fieldName] = { script: expression, type }
135
156
  }
136
157
 
137
158
  getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
@@ -141,6 +162,7 @@ export class ElasticsearchService {
141
162
  values: uuids,
142
163
  },
143
164
  },
165
+ size: uuids.length,
144
166
  }
145
167
  }
146
168
 
@@ -199,9 +221,27 @@ export class ElasticsearchService {
199
221
  }
200
222
 
201
223
  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] }))
224
+ if (!sortBy || sortBy.length === 0) return undefined
225
+ const fields = Array.isArray(sortBy[0])
226
+ ? (sortBy as FieldSort[])
227
+ : [sortBy as FieldSort]
228
+ return fields.map((field) => {
229
+ // Sort by nested array of dates only works with the explicit syntax
230
+ if (field[1].endsWith('.date')) {
231
+ const nestedPath = field[1].slice(0, field[1].lastIndexOf('.date'))
232
+ return {
233
+ [field[1]]: {
234
+ order: field[0] as 'desc' | 'asc',
235
+ mode: field[0] === 'desc' ? 'max' : 'min',
236
+ missing: '_last',
237
+ nested: {
238
+ path: nestedPath,
239
+ },
240
+ },
241
+ }
242
+ }
243
+ return { [field[1]]: field[0] }
244
+ })
205
245
  }
206
246
 
207
247
  private injectLangInQueryStringFields(
@@ -244,8 +284,55 @@ export class ElasticsearchService {
244
284
  return this.metadataLang === 'current'
245
285
  }
246
286
 
247
- private filtersToQuery(
287
+ private findSpatialFilterExtent(
248
288
  filters: FieldFilters | FiltersAggregationParams | string
289
+ ): BoundingBox | undefined {
290
+ if (typeof filters === 'string') {
291
+ return undefined
292
+ }
293
+ return Object.values(filters).find(isBoundingBox)
294
+ }
295
+
296
+ // if a field registered min/max runtime fields, its dates form an interval
297
+ // the record matches if that interval intersects the filter range
298
+ private buildDateRangeQuery(searchField: string, dateRange: DateRange) {
299
+ const minField = `${searchField}Min`
300
+ const maxField = `${searchField}Max`
301
+ if (minField in this.runtimeFields && maxField in this.runtimeFields) {
302
+ const filter = [
303
+ dateRange.start && {
304
+ range: {
305
+ [maxField]: {
306
+ gte: formatDate(dateRange.start),
307
+ format: 'yyyy-MM-dd',
308
+ },
309
+ },
310
+ },
311
+ dateRange.end && {
312
+ range: {
313
+ [minField]: {
314
+ lte: formatDate(dateRange.end),
315
+ format: 'yyyy-MM-dd',
316
+ },
317
+ },
318
+ },
319
+ ].filter(Boolean)
320
+ return { bool: { filter } }
321
+ }
322
+ return {
323
+ range: {
324
+ [searchField]: {
325
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
326
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
327
+ format: 'yyyy-MM-dd',
328
+ },
329
+ },
330
+ }
331
+ }
332
+
333
+ private filtersToQuery(
334
+ filters: FieldFilters | FiltersAggregationParams | string,
335
+ spatialFilterExtent = this.findSpatialFilterExtent(filters)
249
336
  ): FilterQuery {
250
337
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
251
338
  const makeQuery = (filter: FieldFilter): string => {
@@ -266,7 +353,9 @@ export class ElasticsearchService {
266
353
  ? filters
267
354
  : Object.keys(filters)
268
355
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
356
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
269
357
  .filter((fieldname) => !isDateRange(filters[fieldname]))
358
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
270
359
  .filter(
271
360
  (fieldname) =>
272
361
  filters[fieldname] &&
@@ -279,37 +368,33 @@ export class ElasticsearchService {
279
368
  if (filters['gn-ui-crossFieldFilter']) {
280
369
  queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
281
370
  }
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]
371
+ const queryRanges = Object.entries(filters).filter(([, value]) =>
372
+ isDateRange(value)
373
+ ) as [string, DateRange][]
293
374
  const queryParts = [
294
375
  queryString && {
295
376
  query_string: {
296
377
  query: queryString,
297
378
  },
298
379
  },
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',
380
+ ...queryRanges.map(([searchField, dateRange]) =>
381
+ this.buildDateRangeQuery(searchField, dateRange)
382
+ ),
383
+ spatialFilterExtent && {
384
+ geo_shape: {
385
+ geom: {
386
+ shape: {
387
+ type: 'envelope',
388
+ // spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
389
+ coordinates: [
390
+ [spatialFilterExtent[0], spatialFilterExtent[3]],
391
+ [spatialFilterExtent[2], spatialFilterExtent[1]],
392
+ ],
310
393
  },
394
+ relation: 'intersects',
311
395
  },
312
396
  },
397
+ },
313
398
  ].filter(Boolean)
314
399
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
315
400
  }
@@ -348,7 +433,12 @@ export class ElasticsearchService {
348
433
  },
349
434
  })
350
435
  }
351
- const queryFilters = this.filtersToQuery(fieldSearchFilters)
436
+ // a spatial extent filter takes precedence over the preference geometry for boosting
437
+ const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
438
+ const queryFilters = this.filtersToQuery(
439
+ fieldSearchFilters,
440
+ spatialFilterExtent
441
+ )
352
442
  if (queryFilters) {
353
443
  filter.push(...queryFilters)
354
444
  }
@@ -359,7 +449,10 @@ export class ElasticsearchService {
359
449
  },
360
450
  })
361
451
  }
362
- if (geometry) {
452
+ const boostGeometry = spatialFilterExtent
453
+ ? bboxToPolygon(spatialFilterExtent)
454
+ : geometry
455
+ if (boostGeometry) {
363
456
  // boosts applied using the filter geometry:
364
457
  // * records completely within the geometry receive a boost of 5
365
458
  // * records intersecting the geometry receive a boost of 2
@@ -369,7 +462,7 @@ export class ElasticsearchService {
369
462
  {
370
463
  geo_shape: {
371
464
  geom: {
372
- shape: geometry,
465
+ shape: boostGeometry,
373
466
  relation: 'within',
374
467
  },
375
468
  boost: 5.0,
@@ -378,7 +471,7 @@ export class ElasticsearchService {
378
471
  {
379
472
  geo_shape: {
380
473
  geom: {
381
- shape: geometry,
474
+ shape: boostGeometry,
382
475
  relation: 'intersects',
383
476
  },
384
477
  boost: 2.0,
@@ -389,7 +482,7 @@ export class ElasticsearchService {
389
482
  // this will boost the results variably depending on their distance from the given geometry
390
483
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
391
484
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
392
- const bbox = getGeometryBoundingBox(geometry)
485
+ const bbox = getGeometryBoundingBox(boostGeometry)
393
486
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
394
487
  const northToCenter = new LineString([
395
488
  [center[0], bbox[3]],
@@ -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
@@ -6,11 +6,15 @@ 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 = {
@@ -19,4 +23,19 @@ export type QueryString = {
19
23
  export type QueryRange = {
20
24
  range: Record<string, { format: string; gte: string; lte: string }>
21
25
  }
22
- export type FilterQuery = Array<QueryString | QueryRange>
26
+ export type QueryGeoShape = {
27
+ geo_shape: {
28
+ geom: {
29
+ shape: { type: string; coordinates: number[][] }
30
+ relation: string
31
+ }
32
+ }
33
+ }
34
+ export type QueryBool = {
35
+ bool: {
36
+ filter: QueryRange[]
37
+ }
38
+ }
39
+ export type FilterQuery = Array<
40
+ QueryString | QueryRange | QueryGeoShape | QueryBool
41
+ >
@@ -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,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
  }
@@ -10,4 +10,3 @@ export * from './lib/style'
10
10
  export * from './lib/layers-panel/layers-panel.component'
11
11
  export * from './lib/add-layer-from-catalog/add-layer-from-catalog.component'
12
12
  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) {
@@ -3,6 +3,7 @@ import { createAction, props } from '@ngrx/store'
3
3
  import {
4
4
  CatalogRecord,
5
5
  DatasetFeatureCatalog,
6
+ LinkedRecord,
6
7
  UserFeedback,
7
8
  } from '../../../../../../libs/common/domain/src/lib/model/record'
8
9
 
@@ -54,14 +55,9 @@ export const setRelated = createAction(
54
55
  props<{ related: CatalogRecord[] }>()
55
56
  )
56
57
 
57
- export const setSources = createAction(
58
- '[Metadata view] Set sources',
59
- props<{ sources: CatalogRecord[] }>()
60
- )
61
-
62
- export const setSourceOf = createAction(
63
- '[Metadata view] Set has sources',
64
- props<{ sourceOf: CatalogRecord[] }>()
58
+ export const setLinkedRecords = createAction(
59
+ '[Metadata view] Set associated records',
60
+ props<{ linkedRecords: LinkedRecord[] }>()
65
61
  )
66
62
 
67
63
  /*