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