geonetwork-ui 2.11.0-dev.c2c8dac56 → 2.11.0-dev.c830efcd7
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 +2052 -1468
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +303 -124
- package/index.d.ts.map +1 -1
- package/package.json +7 -5
- 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/mel.records.dataset+featurecatalog.ts +317 -0
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +35 -8
- 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 +2 -0
- package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +98 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +7 -0
- package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +2 -1
- package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +134 -35
- package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +5 -34
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +6 -13
- 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/repository/records-repository.interface.ts +1 -1
- package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
- package/src/libs/feature/dataviz/src/lib/service/data.service.ts +32 -19
- 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.effects.ts +3 -1
- package/src/libs/feature/record/src/lib/state/mdview.facade.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/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/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +2 -2
- 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.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/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
- 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/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/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 +24 -4
- package/translations/en.json +23 -3
- package/translations/es.json +22 -2
- package/translations/fr.json +28 -8
- package/translations/it.json +22 -2
- package/translations/nl.json +22 -2
- package/translations/pt.json +22 -2
- package/translations/sk.json +22 -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/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/{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 {
|
|
37
|
+
import {
|
|
38
|
+
bboxToPolygon,
|
|
39
|
+
BoundingBox,
|
|
40
|
+
getGeometryBoundingBox,
|
|
41
|
+
isBoundingBox,
|
|
42
|
+
} from '../../../../../../../libs/util/shared/src'
|
|
38
43
|
import { getLength as getGeodesicLength } from 'ol/sphere.js'
|
|
39
44
|
import { LineString } from 'ol/geom.js'
|
|
40
45
|
|
|
@@ -49,7 +54,12 @@ export class ElasticsearchService {
|
|
|
49
54
|
|
|
50
55
|
// runtime fields are computed using a Painless script
|
|
51
56
|
// see: https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
|
|
52
|
-
private runtimeFields: Record<
|
|
57
|
+
private runtimeFields: Record<
|
|
58
|
+
string,
|
|
59
|
+
{ script: string; type: 'keyword' | 'date' }
|
|
60
|
+
> = {}
|
|
61
|
+
|
|
62
|
+
private fieldAliases: Record<string, string> = {}
|
|
53
63
|
|
|
54
64
|
// we're using getters in case the defined languages change over time
|
|
55
65
|
private get metadataLang(): LanguageCode {
|
|
@@ -91,8 +101,8 @@ export class ElasticsearchService {
|
|
|
91
101
|
const addMapping = (fieldName: string) => {
|
|
92
102
|
if (!payload.runtime_mappings) payload.runtime_mappings = {}
|
|
93
103
|
payload.runtime_mappings[fieldName] = {
|
|
94
|
-
type:
|
|
95
|
-
script: this.runtimeFields[fieldName],
|
|
104
|
+
type: this.runtimeFields[fieldName].type,
|
|
105
|
+
script: this.runtimeFields[fieldName].script,
|
|
96
106
|
}
|
|
97
107
|
}
|
|
98
108
|
const lookForField = (node: unknown) => {
|
|
@@ -113,6 +123,14 @@ export class ElasticsearchService {
|
|
|
113
123
|
) {
|
|
114
124
|
addMapping(runtimeField)
|
|
115
125
|
}
|
|
126
|
+
if (
|
|
127
|
+
runtimeField in node &&
|
|
128
|
+
typeof node[runtimeField] === 'object' &&
|
|
129
|
+
node[runtimeField] !== null &&
|
|
130
|
+
('gte' in node[runtimeField] || 'lte' in node[runtimeField])
|
|
131
|
+
) {
|
|
132
|
+
addMapping(runtimeField)
|
|
133
|
+
}
|
|
116
134
|
if (
|
|
117
135
|
'query' in node &&
|
|
118
136
|
typeof node.query === 'string' &&
|
|
@@ -131,8 +149,16 @@ export class ElasticsearchService {
|
|
|
131
149
|
return payload
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
registerRuntimeField(
|
|
135
|
-
|
|
152
|
+
registerRuntimeField(
|
|
153
|
+
fieldName: string,
|
|
154
|
+
expression: string,
|
|
155
|
+
type: 'keyword' | 'date' = 'keyword'
|
|
156
|
+
) {
|
|
157
|
+
this.runtimeFields[fieldName] = { script: expression, type }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
registerFieldAlias(fieldIdentifier: string, esFieldName: string) {
|
|
161
|
+
this.fieldAliases[fieldIdentifier] = esFieldName
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
|
|
@@ -264,8 +290,68 @@ export class ElasticsearchService {
|
|
|
264
290
|
return this.metadataLang === 'current'
|
|
265
291
|
}
|
|
266
292
|
|
|
267
|
-
private
|
|
293
|
+
private findSpatialFilterExtent(
|
|
268
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)
|
|
269
355
|
): FilterQuery {
|
|
270
356
|
const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
|
|
271
357
|
const makeQuery = (filter: FieldFilter): string => {
|
|
@@ -286,50 +372,49 @@ export class ElasticsearchService {
|
|
|
286
372
|
? filters
|
|
287
373
|
: Object.keys(filters)
|
|
288
374
|
.filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
|
|
375
|
+
.filter((fieldname) => !isBoundingBox(filters[fieldname]))
|
|
289
376
|
.filter((fieldname) => !isDateRange(filters[fieldname]))
|
|
377
|
+
.filter((fieldname) => !Array.isArray(filters[fieldname]))
|
|
290
378
|
.filter(
|
|
291
379
|
(fieldname) =>
|
|
292
380
|
filters[fieldname] &&
|
|
293
381
|
JSON.stringify(filters[fieldname]) !== '{}'
|
|
294
382
|
)
|
|
295
383
|
.map(
|
|
296
|
-
(fieldname) =>
|
|
384
|
+
(fieldname) =>
|
|
385
|
+
`${this.fieldAliases[fieldname] ?? fieldname}:(${makeQuery(filters[fieldname])})`
|
|
297
386
|
)
|
|
298
387
|
.join(' AND ')
|
|
299
388
|
if (filters['gn-ui-crossFieldFilter']) {
|
|
300
389
|
queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
|
|
301
390
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
searchField,
|
|
307
|
-
dateRange,
|
|
308
|
-
} as {
|
|
309
|
-
searchField: string
|
|
310
|
-
dateRange: DateRange
|
|
311
|
-
}
|
|
312
|
-
})[0]
|
|
391
|
+
const queryRanges = Object.entries(filters).filter(([, value]) =>
|
|
392
|
+
isDateRange(value)
|
|
393
|
+
) as [string, DateRange][]
|
|
313
394
|
const queryParts = [
|
|
314
395
|
queryString && {
|
|
315
396
|
query_string: {
|
|
316
397
|
query: queryString,
|
|
317
398
|
},
|
|
318
399
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
],
|
|
330
413
|
},
|
|
414
|
+
relation: 'intersects',
|
|
331
415
|
},
|
|
332
416
|
},
|
|
417
|
+
},
|
|
333
418
|
].filter(Boolean)
|
|
334
419
|
return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
|
|
335
420
|
}
|
|
@@ -368,7 +453,12 @@ export class ElasticsearchService {
|
|
|
368
453
|
},
|
|
369
454
|
})
|
|
370
455
|
}
|
|
371
|
-
|
|
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
|
+
)
|
|
372
462
|
if (queryFilters) {
|
|
373
463
|
filter.push(...queryFilters)
|
|
374
464
|
}
|
|
@@ -379,7 +469,10 @@ export class ElasticsearchService {
|
|
|
379
469
|
},
|
|
380
470
|
})
|
|
381
471
|
}
|
|
382
|
-
|
|
472
|
+
const boostGeometry = spatialFilterExtent
|
|
473
|
+
? bboxToPolygon(spatialFilterExtent)
|
|
474
|
+
: geometry
|
|
475
|
+
if (boostGeometry) {
|
|
383
476
|
// boosts applied using the filter geometry:
|
|
384
477
|
// * records completely within the geometry receive a boost of 5
|
|
385
478
|
// * records intersecting the geometry receive a boost of 2
|
|
@@ -389,7 +482,7 @@ export class ElasticsearchService {
|
|
|
389
482
|
{
|
|
390
483
|
geo_shape: {
|
|
391
484
|
geom: {
|
|
392
|
-
shape:
|
|
485
|
+
shape: boostGeometry,
|
|
393
486
|
relation: 'within',
|
|
394
487
|
},
|
|
395
488
|
boost: 5.0,
|
|
@@ -398,7 +491,7 @@ export class ElasticsearchService {
|
|
|
398
491
|
{
|
|
399
492
|
geo_shape: {
|
|
400
493
|
geom: {
|
|
401
|
-
shape:
|
|
494
|
+
shape: boostGeometry,
|
|
402
495
|
relation: 'intersects',
|
|
403
496
|
},
|
|
404
497
|
boost: 2.0,
|
|
@@ -409,7 +502,7 @@ export class ElasticsearchService {
|
|
|
409
502
|
// this will boost the results variably depending on their distance from the given geometry
|
|
410
503
|
// note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
|
|
411
504
|
// combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
|
|
412
|
-
const bbox = getGeometryBoundingBox(
|
|
505
|
+
const bbox = getGeometryBoundingBox(boostGeometry)
|
|
413
506
|
const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
|
|
414
507
|
const northToCenter = new LineString([
|
|
415
508
|
[center[0], bbox[3]],
|
|
@@ -635,6 +728,12 @@ export class ElasticsearchService {
|
|
|
635
728
|
order: {
|
|
636
729
|
[`_${aggregation.sort[1]}`]: aggregation.sort[0],
|
|
637
730
|
},
|
|
731
|
+
...(aggregation.includeValues && {
|
|
732
|
+
include: aggregation.includeValues,
|
|
733
|
+
}),
|
|
734
|
+
...(aggregation.excludeValues && {
|
|
735
|
+
exclude: aggregation.excludeValues,
|
|
736
|
+
}),
|
|
638
737
|
},
|
|
639
738
|
}
|
|
640
739
|
case 'histogram':
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
AssociatedRecord,
|
|
18
18
|
CatalogRecord,
|
|
19
19
|
DatasetFeatureCatalog,
|
|
20
|
-
DatasetFeatureType,
|
|
21
20
|
LanguageCode,
|
|
22
21
|
LinkedRecord,
|
|
23
22
|
RecordRelation,
|
|
@@ -182,43 +181,15 @@ export class Gn4Repository implements RecordsRepositoryInterface {
|
|
|
182
181
|
)
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
|
|
186
|
-
featureTypes: Array<DatasetFeatureType>
|
|
187
|
-
): DatasetFeatureCatalog {
|
|
188
|
-
return {
|
|
189
|
-
featureTypes: featureTypes.map((featureType) => ({
|
|
190
|
-
name: featureType.typeName || '',
|
|
191
|
-
definition: featureType.definition || '',
|
|
192
|
-
attributes: Array.isArray(featureType.attributeTable)
|
|
193
|
-
? featureType.attributeTable.map((attr) => {
|
|
194
|
-
const values = attr.values
|
|
195
|
-
?.filter((v) => v.code || v.label)
|
|
196
|
-
.map((v) => ({
|
|
197
|
-
code: v.code,
|
|
198
|
-
label: v.label,
|
|
199
|
-
}))
|
|
200
|
-
return {
|
|
201
|
-
name: attr.name,
|
|
202
|
-
code: attr.code,
|
|
203
|
-
definition: attr.definition,
|
|
204
|
-
type: attr.type,
|
|
205
|
-
...(values?.length > 0 ? { values } : {}),
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
: [],
|
|
209
|
-
})),
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
getFeatureCatalog(
|
|
184
|
+
queryLinkedFeatureCatalog(
|
|
213
185
|
record: CatalogRecord,
|
|
214
186
|
visited: Set<string> = new Set() // prevent looping
|
|
215
187
|
): Observable<DatasetFeatureCatalog | null> {
|
|
216
188
|
if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
record.extras['featureTypes'].length > 0
|
|
189
|
+
'featureTypeDescriptions' in record &&
|
|
190
|
+
record.featureTypeDescriptions?.length > 0
|
|
220
191
|
) {
|
|
221
|
-
return of(
|
|
192
|
+
return of({ featureTypes: record.featureTypeDescriptions })
|
|
222
193
|
}
|
|
223
194
|
|
|
224
195
|
const featureCatalogIdentifier = record.extras[
|
|
@@ -228,7 +199,7 @@ export class Gn4Repository implements RecordsRepositoryInterface {
|
|
|
228
199
|
visited.add(featureCatalogIdentifier)
|
|
229
200
|
return this.getRecord(featureCatalogIdentifier).pipe(
|
|
230
201
|
switchMap((record) =>
|
|
231
|
-
record ? this.
|
|
202
|
+
record ? this.queryLinkedFeatureCatalog(record, visited) : of(null)
|
|
232
203
|
)
|
|
233
204
|
)
|
|
234
205
|
}
|
|
@@ -316,6 +316,7 @@ export interface DatasetRecord extends BaseRecord {
|
|
|
316
316
|
spatialExtents: Array<DatasetSpatialExtent>
|
|
317
317
|
temporalExtents: Array<DatasetTemporalExtent>
|
|
318
318
|
spatialRepresentation?: SpatialRepresentationType
|
|
319
|
+
featureTypeDescriptions?: DatasetFeatureCatalog['featureTypes']
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
export interface ServiceEndpoint {
|
|
@@ -350,30 +351,22 @@ export interface ReuseRecord extends BaseRecord {
|
|
|
350
351
|
export type DatasetFeatureCatalog = {
|
|
351
352
|
featureTypes: Array<{
|
|
352
353
|
name: string
|
|
353
|
-
|
|
354
|
+
description?: string
|
|
354
355
|
attributes: DatasetFeatureAttribute[]
|
|
355
356
|
}>
|
|
356
357
|
}
|
|
357
358
|
export interface DatasetFeatureAttributeValue {
|
|
358
359
|
code?: string
|
|
359
|
-
|
|
360
|
+
description?: string
|
|
360
361
|
label?: string
|
|
361
362
|
}
|
|
362
363
|
export interface DatasetFeatureAttribute {
|
|
363
364
|
name: string
|
|
364
|
-
|
|
365
|
-
type
|
|
366
|
-
code
|
|
365
|
+
description?: string
|
|
366
|
+
type?: string
|
|
367
|
+
code?: string
|
|
367
368
|
values?: Array<DatasetFeatureAttributeValue>
|
|
368
369
|
}
|
|
369
|
-
export interface DatasetFeatureType {
|
|
370
|
-
aliases: string
|
|
371
|
-
code: string
|
|
372
|
-
isAbstract: string
|
|
373
|
-
typeName: string
|
|
374
|
-
definition: string
|
|
375
|
-
attributeTable: Array<DatasetFeatureAttribute>
|
|
376
|
-
}
|
|
377
370
|
|
|
378
371
|
export type ReuseType = 'application' | 'map' | 'other'
|
|
379
372
|
|
|
@@ -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
|
+
>
|
|
@@ -17,7 +17,7 @@ export abstract class RecordsRepositoryInterface {
|
|
|
17
17
|
abstract search(params: SearchParams): Observable<SearchResults>
|
|
18
18
|
abstract getMatchesCount(filters: FieldFilters): Observable<number>
|
|
19
19
|
abstract getRecord(uniqueIdentifier: string): Observable<CatalogRecord | null>
|
|
20
|
-
abstract
|
|
20
|
+
abstract queryLinkedFeatureCatalog(
|
|
21
21
|
record: CatalogRecord
|
|
22
22
|
): Observable<DatasetFeatureCatalog | null>
|
|
23
23
|
abstract aggregate(params: AggregationsParams): Observable<Aggregations>
|
|
@@ -164,69 +164,49 @@ As such, **it is not very interesting at all.**`,
|
|
|
164
164
|
extras: {
|
|
165
165
|
isPublishedToAll: true,
|
|
166
166
|
edit: true,
|
|
167
|
-
featureTypes: [
|
|
168
|
-
{
|
|
169
|
-
attributeTable: [
|
|
170
|
-
{
|
|
171
|
-
code: 'OBJECTID',
|
|
172
|
-
name: 'OBJECTID',
|
|
173
|
-
link: '',
|
|
174
|
-
definition: 'Object identifier',
|
|
175
|
-
type: 'OID',
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
code: 'NOM',
|
|
179
|
-
name: 'Nom',
|
|
180
|
-
link: '',
|
|
181
|
-
definition: 'Nom de la rue',
|
|
182
|
-
type: 'String (48)',
|
|
183
|
-
values: [
|
|
184
|
-
{
|
|
185
|
-
code: 'Pomme',
|
|
186
|
-
label: 'Les Pommiers',
|
|
187
|
-
description: 'Sous les pommiers',
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
code: 'Cotton',
|
|
191
|
-
label: 'Rue Cotton',
|
|
192
|
-
description: 'Rue Cotton',
|
|
193
|
-
},
|
|
194
|
-
{ code: "Passage de l'échiquier" },
|
|
195
|
-
{ description: 'Rue du Charlieu' },
|
|
196
|
-
],
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
code: 'RUE',
|
|
200
|
-
name: 'Rue',
|
|
201
|
-
link: '',
|
|
202
|
-
definition: '',
|
|
203
|
-
type: 'String (50)',
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
code: '',
|
|
207
|
-
aliases: '',
|
|
208
|
-
typeName: "Catalogue d'attributs N°1",
|
|
209
|
-
definition: 'Définition du catalogue d attributs N°1',
|
|
210
|
-
isAbstract: 'false',
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
attributeTable: [
|
|
214
|
-
{
|
|
215
|
-
code: 'UniqueObject',
|
|
216
|
-
name: 'unique object ',
|
|
217
|
-
link: '',
|
|
218
|
-
definition: 'this is the only object of this catalog',
|
|
219
|
-
type: 'String (50)',
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
code: '',
|
|
223
|
-
aliases: '',
|
|
224
|
-
typeName: "Catalogue d'attributs N°2",
|
|
225
|
-
definition: 'Définition du catalogue d attributs N°2',
|
|
226
|
-
isAbstract: 'false',
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
167
|
},
|
|
168
|
+
featureTypeDescriptions: [
|
|
169
|
+
{
|
|
170
|
+
name: "Catalogue d'attributs N°1",
|
|
171
|
+
description: 'Définition du catalogue d attributs N°1',
|
|
172
|
+
attributes: [
|
|
173
|
+
{
|
|
174
|
+
name: 'OBJECTID',
|
|
175
|
+
description: 'Object identifier',
|
|
176
|
+
code: 'OBJECTID',
|
|
177
|
+
type: 'OID',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'Nom',
|
|
181
|
+
description: 'Nom de la rue',
|
|
182
|
+
code: 'NOM',
|
|
183
|
+
type: 'String (48)',
|
|
184
|
+
values: [
|
|
185
|
+
{ code: 'Pomme', label: 'Les Pommiers' },
|
|
186
|
+
{ code: 'Cotton', label: 'Rue Cotton' },
|
|
187
|
+
{ code: "Passage de l'échiquier" },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'Rue',
|
|
192
|
+
code: 'RUE',
|
|
193
|
+
type: 'String (50)',
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "Catalogue d'attributs N°2",
|
|
199
|
+
description: 'Définition du catalogue d attributs N°2',
|
|
200
|
+
attributes: [
|
|
201
|
+
{
|
|
202
|
+
name: 'unique object ',
|
|
203
|
+
description: 'this is the only object of this catalog',
|
|
204
|
+
code: 'UniqueObject',
|
|
205
|
+
type: 'String (50)',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
230
210
|
},
|
|
231
211
|
{
|
|
232
212
|
uniqueIdentifier: '7d002c4c-92ef-4b9f-a568-d732f740b99e',
|
|
@@ -209,7 +209,8 @@ export class DataService {
|
|
|
209
209
|
ogcApiLink: DatasetServiceDistribution
|
|
210
210
|
): Promise<DatasetOnlineResource[]> {
|
|
211
211
|
const collectionInfo = await this.getDownloadUrlsFromOgcApi(
|
|
212
|
-
ogcApiLink.url.href
|
|
212
|
+
ogcApiLink.url.href,
|
|
213
|
+
ogcApiLink.name
|
|
213
214
|
)
|
|
214
215
|
|
|
215
216
|
return Object.keys(collectionInfo.bulkDownloadLinks).map((mimeType) => {
|
|
@@ -229,31 +230,41 @@ export class DataService {
|
|
|
229
230
|
})
|
|
230
231
|
}
|
|
231
232
|
|
|
232
|
-
async
|
|
233
|
+
private async getTargetCollectionId(
|
|
234
|
+
endpoint: OgcApiEndpoint,
|
|
235
|
+
collectionId?: string
|
|
236
|
+
): Promise<string | undefined> {
|
|
237
|
+
const collections = await endpoint.featureCollections
|
|
238
|
+
return collections.length === 1 && !collectionId
|
|
239
|
+
? collections[0]
|
|
240
|
+
: collectionId
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async getDownloadUrlsFromOgcApi(
|
|
244
|
+
url: string,
|
|
245
|
+
collectionId?: string
|
|
246
|
+
): Promise<OgcApiCollectionInfo> {
|
|
233
247
|
const endpoint = new OgcApiEndpoint(url)
|
|
234
|
-
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
.
|
|
239
|
-
|
|
240
|
-
})
|
|
248
|
+
try {
|
|
249
|
+
const id = await this.getTargetCollectionId(endpoint, collectionId)
|
|
250
|
+
return await endpoint.getCollectionInfo(id)
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error('ogc.unreachable.unknown')
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
async getItemsFromOgcApi(
|
|
244
257
|
url: string,
|
|
258
|
+
collectionId?: string,
|
|
245
259
|
limit?: number
|
|
246
260
|
): Promise<OgcApiRecord[]> {
|
|
247
261
|
const endpoint = new OgcApiEndpoint(url)
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.catch(() => {
|
|
255
|
-
throw new Error(`ogc.unreachable.unknown`)
|
|
256
|
-
})
|
|
262
|
+
try {
|
|
263
|
+
const id = await this.getTargetCollectionId(endpoint, collectionId)
|
|
264
|
+
return await endpoint.getCollectionItems(id, limit)
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error('ogc.unreachable.unknown')
|
|
267
|
+
}
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
async getItemsFromStacApi(
|
|
@@ -381,7 +392,9 @@ export class DataService {
|
|
|
381
392
|
link.type === 'service' &&
|
|
382
393
|
link.accessServiceProtocol === 'ogcFeatures'
|
|
383
394
|
) {
|
|
384
|
-
return from(
|
|
395
|
+
return from(
|
|
396
|
+
this.getDownloadUrlsFromOgcApi(link.url.href, link.name)
|
|
397
|
+
).pipe(
|
|
385
398
|
switchMap((collectionInfo) => {
|
|
386
399
|
const isMimeTypeJson = (mimeType: string): boolean => {
|
|
387
400
|
return mimeType.toLowerCase().indexOf('json') > -1
|
|
@@ -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'
|
|
@@ -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 {}
|