geonetwork-ui 2.10.3-dev.4154db07d → 2.11.0-dev.0ddf1324b
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 +1661 -986
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +298 -128
- package/index.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/libs/api/metadata-converter/src/lib/fixtures/generic.records.ts +5 -5
- 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 +43 -13
- package/src/libs/api/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts +13 -1
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +2 -2
- package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +9 -5
- 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 +130 -37
- package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +40 -2
- package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
- 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 +2 -2
- 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 +22 -22
- 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 -1
- 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/record/src/lib/state/mdview.actions.ts +4 -8
- package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
- package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
- 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 +12 -4
- package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
- package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +5 -1
- package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
- package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
- package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
- package/src/libs/feature/search/src/lib/sort-by/sort-by.component.ts +1 -1
- package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
- package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
- 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/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.html +10 -1
- package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
- 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/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 +111 -0
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
- package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
- package/src/libs/util/app-config/src/lib/model.ts +1 -0
- 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 +8 -0
- package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
- package/tailwind.base.css +5 -0
- package/translations/de.json +30 -4
- package/translations/en.json +29 -3
- package/translations/es.json +27 -1
- package/translations/fr.json +30 -4
- package/translations/it.json +28 -2
- package/translations/nl.json +27 -1
- package/translations/pt.json +27 -1
- package/translations/sk.json +28 -2
- 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/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
|
@@ -15,9 +15,11 @@ import {
|
|
|
15
15
|
} from './atomic-operations'
|
|
16
16
|
import { MetadataUrlService } from './metadata-url.service'
|
|
17
17
|
import { inject, Injectable } from '@angular/core'
|
|
18
|
+
import { getAssociationTypeFromCode } from '../iso19139/utils/association-type.mapper'
|
|
18
19
|
import { getStatusFromStatusCode } from '../iso19139/utils/status.mapper'
|
|
19
20
|
import { getUpdateFrequencyFromFrequencyCode } from '../iso19139/utils/update-frequency.mapper'
|
|
20
21
|
import {
|
|
22
|
+
AssociatedRecord,
|
|
21
23
|
CatalogRecord,
|
|
22
24
|
Constraint,
|
|
23
25
|
DatasetSpatialExtent,
|
|
@@ -318,25 +320,16 @@ export class Gn4FieldMapper {
|
|
|
318
320
|
output
|
|
319
321
|
),
|
|
320
322
|
related: (output, source) => {
|
|
323
|
+
const related = <SourceWithUnknownProps>selectField(source, 'related')
|
|
321
324
|
const fcatSource = selectField(
|
|
322
|
-
getFirstValue(
|
|
323
|
-
selectField(
|
|
324
|
-
<SourceWithUnknownProps>selectField(source, 'related'),
|
|
325
|
-
'fcats'
|
|
326
|
-
)
|
|
327
|
-
) ?? {},
|
|
325
|
+
getFirstValue(selectField(related, 'fcats')) ?? {},
|
|
328
326
|
'_source'
|
|
329
327
|
)
|
|
330
328
|
const featureCatalogIdentifier: string = selectField(
|
|
331
329
|
<SourceWithUnknownProps>fcatSource,
|
|
332
330
|
'uuid'
|
|
333
331
|
)
|
|
334
|
-
const sourceOfLinks = getAsArray(
|
|
335
|
-
selectField(
|
|
336
|
-
<SourceWithUnknownProps>selectField(source, 'related'),
|
|
337
|
-
'hassources'
|
|
338
|
-
)
|
|
339
|
-
)
|
|
332
|
+
const sourceOfLinks = getAsArray(selectField(related, 'hassources'))
|
|
340
333
|
const sourceOfIdentifiers: string[] = sourceOfLinks
|
|
341
334
|
.filter((link) => link['origin'] === 'catalog')
|
|
342
335
|
.map((link) => {
|
|
@@ -345,13 +338,50 @@ export class Gn4FieldMapper {
|
|
|
345
338
|
'uuid'
|
|
346
339
|
)
|
|
347
340
|
})
|
|
348
|
-
const
|
|
341
|
+
const siblingLinks = getAsArray(selectField(related, 'siblings'))
|
|
342
|
+
const siblings: AssociatedRecord[] = siblingLinks
|
|
343
|
+
.filter((link) => link['origin'] === 'catalog')
|
|
344
|
+
.map((link) => ({
|
|
345
|
+
uniqueIdentifier: selectField<string>(
|
|
346
|
+
selectField(<SourceWithUnknownProps>link, '_source'),
|
|
347
|
+
'uuid'
|
|
348
|
+
),
|
|
349
|
+
associationType: getAssociationTypeFromCode(
|
|
350
|
+
selectField<string>(
|
|
351
|
+
selectField(<SourceWithUnknownProps>link, 'properties'),
|
|
352
|
+
'associationType'
|
|
353
|
+
)
|
|
354
|
+
),
|
|
355
|
+
}))
|
|
356
|
+
const associatedLinks = getAsArray(selectField(related, 'associated'))
|
|
357
|
+
const associatedIdentifiers: string[] = associatedLinks
|
|
358
|
+
.filter((link) => link['origin'] === 'catalog')
|
|
359
|
+
.map((link) => {
|
|
360
|
+
return selectField<string>(
|
|
361
|
+
selectField(<SourceWithUnknownProps>link, '_source'),
|
|
362
|
+
'uuid'
|
|
363
|
+
)
|
|
364
|
+
})
|
|
365
|
+
.filter(
|
|
366
|
+
(uuid) =>
|
|
367
|
+
!siblings.some((sibling) => sibling.uniqueIdentifier === uuid)
|
|
368
|
+
)
|
|
369
|
+
const extraValues: Record<
|
|
370
|
+
string,
|
|
371
|
+
string | string[] | AssociatedRecord[]
|
|
372
|
+
> = {}
|
|
349
373
|
if (featureCatalogIdentifier) {
|
|
350
374
|
extraValues.featureCatalogIdentifier = featureCatalogIdentifier
|
|
351
375
|
}
|
|
352
376
|
if (sourceOfIdentifiers && sourceOfIdentifiers.length > 0) {
|
|
353
377
|
extraValues.sourceOfIdentifiers = sourceOfIdentifiers
|
|
354
378
|
}
|
|
379
|
+
if (associatedIdentifiers && associatedIdentifiers.length > 0) {
|
|
380
|
+
extraValues.associatedIdentifiers = associatedIdentifiers
|
|
381
|
+
}
|
|
382
|
+
if (siblings && siblings.length > 0) {
|
|
383
|
+
extraValues.siblings = siblings
|
|
384
|
+
}
|
|
355
385
|
return Object.keys(extraValues).length > 0
|
|
356
386
|
? this.addExtra(extraValues, output)
|
|
357
387
|
: output
|
|
@@ -2,7 +2,19 @@ export type SortOrder = 'asc' | 'desc'
|
|
|
2
2
|
interface SortParam {
|
|
3
3
|
[key: string]: SortOrder
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
type EsFieldSort = {
|
|
6
|
+
order: SortOrder
|
|
7
|
+
mode?: 'max' | 'min'
|
|
8
|
+
missing?: '_last'
|
|
9
|
+
nested?: {
|
|
10
|
+
path: string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export type SortParams =
|
|
14
|
+
| string
|
|
15
|
+
| SortParam
|
|
16
|
+
| { [property: string]: EsFieldSort }
|
|
17
|
+
| (string | SortParam | { [property: string]: EsFieldSort })[]
|
|
6
18
|
|
|
7
19
|
export interface EsRequestAggTerm {
|
|
8
20
|
field?: string
|
|
@@ -587,7 +587,7 @@ export function writeAssociatedRecords(
|
|
|
587
587
|
removeChildrenByName('mri:associatedResource'),
|
|
588
588
|
appendChildren(
|
|
589
589
|
...record.associatedRecords
|
|
590
|
-
.filter((assoc) => assoc.
|
|
590
|
+
.filter((assoc) => assoc.uniqueIdentifier && assoc.associationType)
|
|
591
591
|
.map((assoc) =>
|
|
592
592
|
pipe(
|
|
593
593
|
createNestedElement(
|
|
@@ -608,7 +608,7 @@ export function writeAssociatedRecords(
|
|
|
608
608
|
),
|
|
609
609
|
pipe(
|
|
610
610
|
createElement('mri:metadataReference'),
|
|
611
|
-
writeAttribute('uuidref', assoc.
|
|
611
|
+
writeAttribute('uuidref', assoc.uniqueIdentifier)
|
|
612
612
|
)
|
|
613
613
|
)
|
|
614
614
|
)
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
readText,
|
|
52
52
|
XmlElement,
|
|
53
53
|
} from '../xml-utils'
|
|
54
|
+
import { getAssociationTypeFromCode } from './utils/association-type.mapper'
|
|
54
55
|
import { readGeometry } from './utils/geometry'
|
|
55
56
|
import { fullNameToParts } from './utils/individual-name'
|
|
56
57
|
import { getKeywordTypeFromKeywordTypeCode } from './utils/keyword.mapper'
|
|
@@ -931,19 +932,22 @@ export function readSourceRecords(rootEl: XmlElement): SourceRecord[] {
|
|
|
931
932
|
export function extractAssociatedRecords(
|
|
932
933
|
containerName: string,
|
|
933
934
|
aggregateName: string,
|
|
934
|
-
|
|
935
|
+
readIdentifier: ChainableFunction<XmlElement, string>
|
|
935
936
|
): ChainableFunction<XmlElement, AssociatedRecord[]> {
|
|
936
937
|
return pipe(
|
|
937
938
|
findChildrenElement(containerName, false),
|
|
938
939
|
mapArray((el) => {
|
|
939
940
|
const aggregateEl = findChildElement(aggregateName, false)(el)
|
|
940
|
-
const
|
|
941
|
-
const
|
|
941
|
+
const uniqueIdentifier = readIdentifier(aggregateEl)
|
|
942
|
+
const associationTypeCode = pipe(
|
|
942
943
|
findNestedElement('gmd:associationType', 'gmd:DS_AssociationTypeCode'),
|
|
943
944
|
readAttribute('codeListValue')
|
|
944
945
|
)(aggregateEl)
|
|
945
|
-
if (!
|
|
946
|
-
return {
|
|
946
|
+
if (!uniqueIdentifier || !associationTypeCode) return null
|
|
947
|
+
return {
|
|
948
|
+
uniqueIdentifier,
|
|
949
|
+
associationType: getAssociationTypeFromCode(associationTypeCode),
|
|
950
|
+
}
|
|
947
951
|
}),
|
|
948
952
|
filterArray((r): r is AssociatedRecord => r !== null)
|
|
949
953
|
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssociationType,
|
|
3
|
+
associationTypeValues,
|
|
4
|
+
} from '../../../../../../../libs/common/domain/src/lib/model/record'
|
|
5
|
+
|
|
6
|
+
export function getAssociationTypeFromCode(
|
|
7
|
+
associationTypeCode: string
|
|
8
|
+
): AssociationType {
|
|
9
|
+
return associationTypeValues.includes(associationTypeCode as AssociationType)
|
|
10
|
+
? (associationTypeCode as AssociationType)
|
|
11
|
+
: 'crossReference'
|
|
12
|
+
}
|
|
@@ -1276,7 +1276,7 @@ export function writeAssociatedRecords(
|
|
|
1276
1276
|
removeChildrenByName('gmd:aggregationInfo'),
|
|
1277
1277
|
appendChildren(
|
|
1278
1278
|
...record.associatedRecords
|
|
1279
|
-
.filter((assoc) => assoc.
|
|
1279
|
+
.filter((assoc) => assoc.uniqueIdentifier && assoc.associationType)
|
|
1280
1280
|
.map((assoc) =>
|
|
1281
1281
|
pipe(
|
|
1282
1282
|
createNestedElement(
|
|
@@ -1290,7 +1290,7 @@ export function writeAssociatedRecords(
|
|
|
1290
1290
|
'gmd:MD_Identifier',
|
|
1291
1291
|
'gmd:code'
|
|
1292
1292
|
),
|
|
1293
|
-
writeCharacterString(assoc.
|
|
1293
|
+
writeCharacterString(assoc.uniqueIdentifier)
|
|
1294
1294
|
),
|
|
1295
1295
|
pipe(
|
|
1296
1296
|
createNestedElement(
|
|
@@ -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,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<
|
|
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:
|
|
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(
|
|
134
|
-
|
|
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 ===
|
|
203
|
-
const fields = Array.isArray(sortBy[0])
|
|
204
|
-
|
|
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
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
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> {
|
|
@@ -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 {
|
|
@@ -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
|
|
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
|
-
|
|
10
|
-
['desc', 'revisionDateForResource'],
|
|
11
|
-
['desc', 'publicationDateForResource'],
|
|
12
|
-
['desc', 'creationDateForResource'],
|
|
13
|
-
],
|
|
9
|
+
RESOURCE_DATE: ['desc', 'resourceDate.date'],
|
|
14
10
|
}
|