geonetwork-ui 2.11.0-dev.a83f0f51a → 2.11.0-dev.b75ae786a

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 (98) hide show
  1. package/fesm2022/geonetwork-ui.mjs +2459 -1974
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +302 -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 +121 -34
  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/filter.model.ts +24 -2
  11. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  12. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  13. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  14. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  15. package/src/libs/feature/map/src/index.ts +0 -2
  16. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  17. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  18. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  19. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  20. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  21. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  22. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  23. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  24. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  25. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  26. package/src/libs/feature/search/src/index.ts +2 -0
  27. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  28. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +63 -19
  29. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +16 -7
  30. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  31. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  32. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +19 -8
  33. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  34. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +15 -1
  35. package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
  36. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  37. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  38. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  39. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  40. package/src/libs/ui/inputs/src/index.ts +2 -0
  41. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  42. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  43. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  44. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  45. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  46. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  47. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  48. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  49. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  50. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  51. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  52. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  53. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  54. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  55. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  56. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +221 -0
  57. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  58. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  59. package/src/libs/util/app-config/src/lib/app-config.ts +23 -0
  60. package/src/libs/util/app-config/src/lib/fixtures.ts +16 -1
  61. package/src/libs/util/app-config/src/lib/model.ts +15 -1
  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 +24 -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 +20 -3
  85. package/translations/en.json +19 -2
  86. package/translations/es.json +18 -1
  87. package/translations/fr.json +20 -3
  88. package/translations/it.json +18 -1
  89. package/translations/nl.json +18 -1
  90. package/translations/pt.json +18 -1
  91. package/translations/sk.json +18 -1
  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/feature/map/src/lib/style/index.ts +0 -2
  95. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  96. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
  97. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  98. /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,10 @@ 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
+ > = {}
53
61
 
54
62
  // we're using getters in case the defined languages change over time
55
63
  private get metadataLang(): LanguageCode {
@@ -91,8 +99,8 @@ export class ElasticsearchService {
91
99
  const addMapping = (fieldName: string) => {
92
100
  if (!payload.runtime_mappings) payload.runtime_mappings = {}
93
101
  payload.runtime_mappings[fieldName] = {
94
- type: 'keyword',
95
- script: this.runtimeFields[fieldName],
102
+ type: this.runtimeFields[fieldName].type,
103
+ script: this.runtimeFields[fieldName].script,
96
104
  }
97
105
  }
98
106
  const lookForField = (node: unknown) => {
@@ -113,6 +121,14 @@ export class ElasticsearchService {
113
121
  ) {
114
122
  addMapping(runtimeField)
115
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
+ }
116
132
  if (
117
133
  'query' in node &&
118
134
  typeof node.query === 'string' &&
@@ -131,8 +147,12 @@ export class ElasticsearchService {
131
147
  return payload
132
148
  }
133
149
 
134
- registerRuntimeField(fieldName: string, expression: string) {
135
- 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 }
136
156
  }
137
157
 
138
158
  getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
@@ -142,6 +162,7 @@ export class ElasticsearchService {
142
162
  values: uuids,
143
163
  },
144
164
  },
165
+ size: uuids.length,
145
166
  }
146
167
  }
147
168
 
@@ -263,8 +284,68 @@ export class ElasticsearchService {
263
284
  return this.metadataLang === 'current'
264
285
  }
265
286
 
266
- private filtersToQuery(
287
+ private findSpatialFilterExtent(
267
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
+ // builds a query matching records whose dates intersect the filter range
297
+ private buildDateRangeQuery(searchField: string, dateRange: DateRange) {
298
+ // fields ending with 'DateRange' maps as ES range type
299
+ if (searchField.endsWith('DateRange')) {
300
+ return {
301
+ range: {
302
+ [searchField]: {
303
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
304
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
305
+ format: 'yyyy-MM-dd',
306
+ relation: 'intersects',
307
+ },
308
+ },
309
+ }
310
+ }
311
+ const minField = `${searchField}Min`
312
+ const maxField = `${searchField}Max`
313
+ if (minField in this.runtimeFields && maxField in this.runtimeFields) {
314
+ // min/max runtime fields form an interval per record; match if it intersects the filter range
315
+ const filter = [
316
+ dateRange.start && {
317
+ range: {
318
+ [maxField]: {
319
+ gte: formatDate(dateRange.start),
320
+ format: 'yyyy-MM-dd',
321
+ },
322
+ },
323
+ },
324
+ dateRange.end && {
325
+ range: {
326
+ [minField]: {
327
+ lte: formatDate(dateRange.end),
328
+ format: 'yyyy-MM-dd',
329
+ },
330
+ },
331
+ },
332
+ ].filter(Boolean)
333
+ return { bool: { filter } }
334
+ }
335
+ return {
336
+ range: {
337
+ [searchField]: {
338
+ ...(dateRange.start && { gte: formatDate(dateRange.start) }),
339
+ ...(dateRange.end && { lte: formatDate(dateRange.end) }),
340
+ format: 'yyyy-MM-dd',
341
+ },
342
+ },
343
+ }
344
+ }
345
+
346
+ private filtersToQuery(
347
+ filters: FieldFilters | FiltersAggregationParams | string,
348
+ spatialFilterExtent = this.findSpatialFilterExtent(filters)
268
349
  ): FilterQuery {
269
350
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
270
351
  const makeQuery = (filter: FieldFilter): string => {
@@ -285,7 +366,9 @@ export class ElasticsearchService {
285
366
  ? filters
286
367
  : Object.keys(filters)
287
368
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
369
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
288
370
  .filter((fieldname) => !isDateRange(filters[fieldname]))
371
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
289
372
  .filter(
290
373
  (fieldname) =>
291
374
  filters[fieldname] &&
@@ -298,37 +381,33 @@ export class ElasticsearchService {
298
381
  if (filters['gn-ui-crossFieldFilter']) {
299
382
  queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
300
383
  }
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]
384
+ const queryRanges = Object.entries(filters).filter(([, value]) =>
385
+ isDateRange(value)
386
+ ) as [string, DateRange][]
312
387
  const queryParts = [
313
388
  queryString && {
314
389
  query_string: {
315
390
  query: queryString,
316
391
  },
317
392
  },
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',
393
+ ...queryRanges.map(([searchField, dateRange]) =>
394
+ this.buildDateRangeQuery(searchField, dateRange)
395
+ ),
396
+ spatialFilterExtent && {
397
+ geo_shape: {
398
+ geom: {
399
+ shape: {
400
+ type: 'envelope',
401
+ // spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
402
+ coordinates: [
403
+ [spatialFilterExtent[0], spatialFilterExtent[3]],
404
+ [spatialFilterExtent[2], spatialFilterExtent[1]],
405
+ ],
329
406
  },
407
+ relation: 'intersects',
330
408
  },
331
409
  },
410
+ },
332
411
  ].filter(Boolean)
333
412
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
334
413
  }
@@ -367,7 +446,12 @@ export class ElasticsearchService {
367
446
  },
368
447
  })
369
448
  }
370
- const queryFilters = this.filtersToQuery(fieldSearchFilters)
449
+ // a spatial extent filter takes precedence over the preference geometry for boosting
450
+ const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
451
+ const queryFilters = this.filtersToQuery(
452
+ fieldSearchFilters,
453
+ spatialFilterExtent
454
+ )
371
455
  if (queryFilters) {
372
456
  filter.push(...queryFilters)
373
457
  }
@@ -378,7 +462,10 @@ export class ElasticsearchService {
378
462
  },
379
463
  })
380
464
  }
381
- if (geometry) {
465
+ const boostGeometry = spatialFilterExtent
466
+ ? bboxToPolygon(spatialFilterExtent)
467
+ : geometry
468
+ if (boostGeometry) {
382
469
  // boosts applied using the filter geometry:
383
470
  // * records completely within the geometry receive a boost of 5
384
471
  // * records intersecting the geometry receive a boost of 2
@@ -388,7 +475,7 @@ export class ElasticsearchService {
388
475
  {
389
476
  geo_shape: {
390
477
  geom: {
391
- shape: geometry,
478
+ shape: boostGeometry,
392
479
  relation: 'within',
393
480
  },
394
481
  boost: 5.0,
@@ -397,7 +484,7 @@ export class ElasticsearchService {
397
484
  {
398
485
  geo_shape: {
399
486
  geom: {
400
- shape: geometry,
487
+ shape: boostGeometry,
401
488
  relation: 'intersects',
402
489
  },
403
490
  boost: 2.0,
@@ -408,7 +495,7 @@ export class ElasticsearchService {
408
495
  // this will boost the results variably depending on their distance from the given geometry
409
496
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
410
497
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
411
- const bbox = getGeometryBoundingBox(geometry)
498
+ const bbox = getGeometryBoundingBox(boostGeometry)
412
499
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
413
500
  const northToCenter = new LineString([
414
501
  [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,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) {
@@ -33,7 +33,6 @@ import { RecordsRepositoryInterface } from '../../../../../../libs/common/domain
33
33
  import { NOT_KNOWN_CONSTRAINT } from '../../../../../../libs/feature/editor/src'
34
34
  import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
35
35
  import { ButtonComponent, TextInputComponent } from '../../../../../../libs/ui/inputs/src'
36
- import { getOptionalEditorConfig } from '../../../../../../libs/util/app-config/src'
37
36
  import { NgIcon, provideIcons, provideNgIconsConfig } from '@ng-icons/core'
38
37
  import { iconoirAppWindow, iconoirPlusCircle } from '@ng-icons/iconoir'
39
38
  import { matCloseOutline } from '@ng-icons/material-icons/outline'
@@ -48,6 +47,10 @@ marker('notify.reuse.form.error.title')
48
47
  marker('notify.reuse.form.error.body')
49
48
 
50
49
  export const REUSE_FORM_URL = new InjectionToken<string>('reuseFormUrl')
50
+ // language assigned to the created reuse record; falls back to the current UI language
51
+ export const NEW_RECORD_DEFAULT_LANGUAGE = new InjectionToken<string>(
52
+ 'newRecordDefaultLanguage'
53
+ )
51
54
 
52
55
  @Component({
53
56
  selector: 'gn-ui-notify-reuse-form',
@@ -86,6 +89,9 @@ export class NotifyReuseFormComponent implements OnDestroy {
86
89
  private readonly notificationsService = inject(NotificationsService)
87
90
  private readonly platformServiceInterface = inject(PlatformServiceInterface)
88
91
  reuseFormUrl = inject(REUSE_FORM_URL, { optional: true })
92
+ private newRecordDefaultLanguage = inject(NEW_RECORD_DEFAULT_LANGUAGE, {
93
+ optional: true,
94
+ })
89
95
  private locationStrategy = inject(LocationStrategy)
90
96
 
91
97
  me = toSignal(this.platformServiceInterface.getMe())
@@ -158,8 +164,7 @@ export class NotifyReuseFormComponent implements OnDestroy {
158
164
  submit() {
159
165
  if (!this.isFormValid()) return
160
166
  const defaultLang =
161
- getOptionalEditorConfig()?.NEW_RECORD_DEFAULT_LANGUAGE ??
162
- this.translate.currentLang
167
+ this.newRecordDefaultLanguage ?? this.translate.currentLang
163
168
  const onlineResource: OnlineLinkResource = {
164
169
  type: 'link',
165
170
  url: new URL(this.url()),