geonetwork-ui 2.11.0-dev.e7b8ec331 → 2.11.0-dev.f5ee66b24
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 +1482 -928
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +280 -124
- package/index.d.ts.map +1 -1
- package/package.json +12 -11
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +43 -13
- package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +108 -34
- 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 +12 -0
- package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
- 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/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/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/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.html +111 -0
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -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 +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/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 +17 -3
- package/translations/en.json +16 -2
- package/translations/es.json +15 -1
- package/translations/fr.json +17 -3
- package/translations/it.json +15 -1
- package/translations/nl.json +15 -1
- package/translations/pt.json +15 -1
- package/translations/sk.json +15 -1
- 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 → 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,10 @@ export class ElasticsearchService {
|
|
|
49
54
|
|
|
50
55
|
// runtime fields are computed using a Painless script
|
|
51
56
|
// see: https://www.elastic.co/guide/en/elasticsearch/reference/current/runtime-mapping-fields.html
|
|
52
|
-
private runtimeFields: Record<
|
|
57
|
+
private runtimeFields: Record<
|
|
58
|
+
string,
|
|
59
|
+
{ script: string; type: 'keyword' | 'date' }
|
|
60
|
+
> = {}
|
|
53
61
|
|
|
54
62
|
// we're using getters in case the defined languages change over time
|
|
55
63
|
private get metadataLang(): LanguageCode {
|
|
@@ -91,8 +99,8 @@ export class ElasticsearchService {
|
|
|
91
99
|
const addMapping = (fieldName: string) => {
|
|
92
100
|
if (!payload.runtime_mappings) payload.runtime_mappings = {}
|
|
93
101
|
payload.runtime_mappings[fieldName] = {
|
|
94
|
-
type:
|
|
95
|
-
script: this.runtimeFields[fieldName],
|
|
102
|
+
type: this.runtimeFields[fieldName].type,
|
|
103
|
+
script: this.runtimeFields[fieldName].script,
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
106
|
const lookForField = (node: unknown) => {
|
|
@@ -113,6 +121,14 @@ export class ElasticsearchService {
|
|
|
113
121
|
) {
|
|
114
122
|
addMapping(runtimeField)
|
|
115
123
|
}
|
|
124
|
+
if (
|
|
125
|
+
runtimeField in node &&
|
|
126
|
+
typeof node[runtimeField] === 'object' &&
|
|
127
|
+
node[runtimeField] !== null &&
|
|
128
|
+
('gte' in node[runtimeField] || 'lte' in node[runtimeField])
|
|
129
|
+
) {
|
|
130
|
+
addMapping(runtimeField)
|
|
131
|
+
}
|
|
116
132
|
if (
|
|
117
133
|
'query' in node &&
|
|
118
134
|
typeof node.query === 'string' &&
|
|
@@ -131,8 +147,12 @@ export class ElasticsearchService {
|
|
|
131
147
|
return payload
|
|
132
148
|
}
|
|
133
149
|
|
|
134
|
-
registerRuntimeField(
|
|
135
|
-
|
|
150
|
+
registerRuntimeField(
|
|
151
|
+
fieldName: string,
|
|
152
|
+
expression: string,
|
|
153
|
+
type: 'keyword' | 'date' = 'keyword'
|
|
154
|
+
) {
|
|
155
|
+
this.runtimeFields[fieldName] = { script: expression, type }
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
getMetadataByIdsPayload(uuids: string[]): EsSearchParams {
|
|
@@ -142,6 +162,7 @@ export class ElasticsearchService {
|
|
|
142
162
|
values: uuids,
|
|
143
163
|
},
|
|
144
164
|
},
|
|
165
|
+
size: uuids.length,
|
|
145
166
|
}
|
|
146
167
|
}
|
|
147
168
|
|
|
@@ -263,8 +284,55 @@ export class ElasticsearchService {
|
|
|
263
284
|
return this.metadataLang === 'current'
|
|
264
285
|
}
|
|
265
286
|
|
|
266
|
-
private
|
|
287
|
+
private findSpatialFilterExtent(
|
|
267
288
|
filters: FieldFilters | FiltersAggregationParams | string
|
|
289
|
+
): BoundingBox | undefined {
|
|
290
|
+
if (typeof filters === 'string') {
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
return Object.values(filters).find(isBoundingBox)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 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)
|
|
268
336
|
): FilterQuery {
|
|
269
337
|
const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
|
|
270
338
|
const makeQuery = (filter: FieldFilter): string => {
|
|
@@ -285,7 +353,9 @@ export class ElasticsearchService {
|
|
|
285
353
|
? filters
|
|
286
354
|
: Object.keys(filters)
|
|
287
355
|
.filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
|
|
356
|
+
.filter((fieldname) => !isBoundingBox(filters[fieldname]))
|
|
288
357
|
.filter((fieldname) => !isDateRange(filters[fieldname]))
|
|
358
|
+
.filter((fieldname) => !Array.isArray(filters[fieldname]))
|
|
289
359
|
.filter(
|
|
290
360
|
(fieldname) =>
|
|
291
361
|
filters[fieldname] &&
|
|
@@ -298,37 +368,33 @@ export class ElasticsearchService {
|
|
|
298
368
|
if (filters['gn-ui-crossFieldFilter']) {
|
|
299
369
|
queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
|
|
300
370
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
searchField,
|
|
306
|
-
dateRange,
|
|
307
|
-
} as {
|
|
308
|
-
searchField: string
|
|
309
|
-
dateRange: DateRange
|
|
310
|
-
}
|
|
311
|
-
})[0]
|
|
371
|
+
const queryRanges = Object.entries(filters).filter(([, value]) =>
|
|
372
|
+
isDateRange(value)
|
|
373
|
+
) as [string, DateRange][]
|
|
312
374
|
const queryParts = [
|
|
313
375
|
queryString && {
|
|
314
376
|
query_string: {
|
|
315
377
|
query: queryString,
|
|
316
378
|
},
|
|
317
379
|
},
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
],
|
|
329
393
|
},
|
|
394
|
+
relation: 'intersects',
|
|
330
395
|
},
|
|
331
396
|
},
|
|
397
|
+
},
|
|
332
398
|
].filter(Boolean)
|
|
333
399
|
return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
|
|
334
400
|
}
|
|
@@ -367,7 +433,12 @@ export class ElasticsearchService {
|
|
|
367
433
|
},
|
|
368
434
|
})
|
|
369
435
|
}
|
|
370
|
-
|
|
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
|
+
)
|
|
371
442
|
if (queryFilters) {
|
|
372
443
|
filter.push(...queryFilters)
|
|
373
444
|
}
|
|
@@ -378,7 +449,10 @@ export class ElasticsearchService {
|
|
|
378
449
|
},
|
|
379
450
|
})
|
|
380
451
|
}
|
|
381
|
-
|
|
452
|
+
const boostGeometry = spatialFilterExtent
|
|
453
|
+
? bboxToPolygon(spatialFilterExtent)
|
|
454
|
+
: geometry
|
|
455
|
+
if (boostGeometry) {
|
|
382
456
|
// boosts applied using the filter geometry:
|
|
383
457
|
// * records completely within the geometry receive a boost of 5
|
|
384
458
|
// * records intersecting the geometry receive a boost of 2
|
|
@@ -388,7 +462,7 @@ export class ElasticsearchService {
|
|
|
388
462
|
{
|
|
389
463
|
geo_shape: {
|
|
390
464
|
geom: {
|
|
391
|
-
shape:
|
|
465
|
+
shape: boostGeometry,
|
|
392
466
|
relation: 'within',
|
|
393
467
|
},
|
|
394
468
|
boost: 5.0,
|
|
@@ -397,7 +471,7 @@ export class ElasticsearchService {
|
|
|
397
471
|
{
|
|
398
472
|
geo_shape: {
|
|
399
473
|
geom: {
|
|
400
|
-
shape:
|
|
474
|
+
shape: boostGeometry,
|
|
401
475
|
relation: 'intersects',
|
|
402
476
|
},
|
|
403
477
|
boost: 2.0,
|
|
@@ -408,7 +482,7 @@ export class ElasticsearchService {
|
|
|
408
482
|
// this will boost the results variably depending on their distance from the given geometry
|
|
409
483
|
// note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
|
|
410
484
|
// combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
|
|
411
|
-
const bbox = getGeometryBoundingBox(
|
|
485
|
+
const bbox = getGeometryBoundingBox(boostGeometry)
|
|
412
486
|
const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
|
|
413
487
|
const northToCenter = new LineString([
|
|
414
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> {
|
|
@@ -294,6 +294,18 @@ export interface AssociatedRecord {
|
|
|
294
294
|
associationType: AssociationType
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Nature of the link between a record and another one; the association type is
|
|
299
|
+
* only known for 'sibling' relations, as it is held by the declaring record.
|
|
300
|
+
*/
|
|
301
|
+
export type RecordRelation = 'source' | 'sourceOf' | 'sibling' | 'associated'
|
|
302
|
+
|
|
303
|
+
export interface LinkedRecord {
|
|
304
|
+
record: CatalogRecord
|
|
305
|
+
relation: RecordRelation
|
|
306
|
+
associationType?: AssociationType
|
|
307
|
+
}
|
|
308
|
+
|
|
297
309
|
export interface DatasetRecord extends BaseRecord {
|
|
298
310
|
kind: 'dataset'
|
|
299
311
|
status: RecordStatus
|
|
@@ -6,11 +6,15 @@ export type FieldFilterByRange = {
|
|
|
6
6
|
start?: Date
|
|
7
7
|
end?: Date
|
|
8
8
|
}
|
|
9
|
+
// matches util/shared's BoundingBox; duplicated here to avoid a circular
|
|
10
|
+
// dependency (util/shared already depends on common/domain)
|
|
11
|
+
export type FieldFilterByBoundingBox = [number, number, number, number]
|
|
9
12
|
|
|
10
13
|
export type FieldFilter =
|
|
11
14
|
| FieldFilterByExpression
|
|
12
15
|
| FieldFilterByValues
|
|
13
16
|
| FieldFilterByRange
|
|
17
|
+
| FieldFilterByBoundingBox
|
|
14
18
|
export type FieldFilters = Record<FieldName, FieldFilter>
|
|
15
19
|
|
|
16
20
|
export type QueryString = {
|
|
@@ -19,4 +23,19 @@ export type QueryString = {
|
|
|
19
23
|
export type QueryRange = {
|
|
20
24
|
range: Record<string, { format: string; gte: string; lte: string }>
|
|
21
25
|
}
|
|
22
|
-
export type
|
|
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
|
+
>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
CatalogRecord,
|
|
11
11
|
DatasetFeatureCatalog,
|
|
12
12
|
LanguageCode,
|
|
13
|
+
LinkedRecord,
|
|
13
14
|
} from '../model/record'
|
|
14
15
|
|
|
15
16
|
export abstract class RecordsRepositoryInterface {
|
|
@@ -23,8 +24,7 @@ export abstract class RecordsRepositoryInterface {
|
|
|
23
24
|
abstract getSimilarRecords(
|
|
24
25
|
similarTo: CatalogRecord
|
|
25
26
|
): Observable<CatalogRecord[]>
|
|
26
|
-
abstract
|
|
27
|
-
abstract getSourceOf(record: CatalogRecord): Observable<CatalogRecord[]>
|
|
27
|
+
abstract getLinkedRecords(record: CatalogRecord): Observable<LinkedRecord[]>
|
|
28
28
|
abstract fuzzySearch(query: string): Observable<SearchResults>
|
|
29
29
|
abstract canDuplicate(record: CatalogRecord): boolean
|
|
30
30
|
abstract canDelete(record: CatalogRecord): Observable<boolean>
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
ChangeDetectionStrategy,
|
|
3
3
|
ChangeDetectorRef,
|
|
4
4
|
Component,
|
|
5
|
+
inject,
|
|
5
6
|
Input,
|
|
6
7
|
Output,
|
|
7
|
-
inject,
|
|
8
8
|
} from '@angular/core'
|
|
9
9
|
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
|
|
10
10
|
import {
|
|
@@ -183,7 +183,7 @@ export class ChartViewComponent {
|
|
|
183
183
|
yChoices$ = this.properties$.pipe(
|
|
184
184
|
map((properties) =>
|
|
185
185
|
properties
|
|
186
|
-
.filter((prop) => prop.type === 'number'
|
|
186
|
+
.filter((prop) => prop.type === 'number')
|
|
187
187
|
.map((prop) => ({ value: prop.name, label: prop.label || prop.name }))
|
|
188
188
|
),
|
|
189
189
|
tap((choices) => {
|
|
@@ -222,6 +222,7 @@ export class ChartViewComponent {
|
|
|
222
222
|
switchMap(([dataset, xProp, yProp, aggregation]) => {
|
|
223
223
|
const fieldAgg: FieldAggregation =
|
|
224
224
|
aggregation === 'count' ? ['count'] : [aggregation, yProp]
|
|
225
|
+
this.loading = true
|
|
225
226
|
return dataset
|
|
226
227
|
.groupBy(['distinct', xProp])
|
|
227
228
|
.aggregate(fieldAgg)
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
ChangeDetectionStrategy,
|
|
3
3
|
ChangeDetectorRef,
|
|
4
4
|
Component,
|
|
5
|
+
inject,
|
|
5
6
|
Input,
|
|
6
7
|
OnDestroy,
|
|
7
8
|
OnInit,
|
|
8
9
|
ViewChild,
|
|
9
|
-
inject,
|
|
10
10
|
} from '@angular/core'
|
|
11
11
|
import {
|
|
12
12
|
DataTableComponent,
|
|
@@ -75,6 +75,7 @@ export class GeoTableViewComponent implements OnInit, OnDestroy {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
private async initMapContext(): Promise<MapContext> {
|
|
78
|
+
this.dataset.load()
|
|
78
79
|
this.dataset.selectAll()
|
|
79
80
|
return {
|
|
80
81
|
layers: [
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inject, Injectable } from '@angular/core'
|
|
2
2
|
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
|
|
3
3
|
import {
|
|
4
|
+
GetCollectionItemsOptions,
|
|
4
5
|
OgcApiCollectionInfo,
|
|
5
6
|
OgcApiEndpoint,
|
|
6
7
|
OgcApiRecord,
|
|
7
|
-
WfsEndpoint,
|
|
8
|
-
WfsVersion,
|
|
9
|
-
TmsEndpoint,
|
|
10
8
|
StacEndpoint,
|
|
11
|
-
GetCollectionItemsOptions,
|
|
12
9
|
StacItemsDocument,
|
|
10
|
+
TmsEndpoint,
|
|
11
|
+
WfsEndpoint,
|
|
12
|
+
WfsVersion,
|
|
13
13
|
} from '@camptocamp/ogc-client'
|
|
14
14
|
import {
|
|
15
15
|
BaseReader,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from '../../../../../../libs/util/shared/src'
|
|
27
27
|
import type { FeatureCollection } from 'geojson'
|
|
28
28
|
import { from, Observable, throwError } from 'rxjs'
|
|
29
|
-
import { catchError, map, switchMap
|
|
29
|
+
import { catchError, map, switchMap } from 'rxjs/operators'
|
|
30
30
|
import {
|
|
31
31
|
DatasetOnlineResource,
|
|
32
32
|
DatasetServiceDistribution,
|
|
@@ -347,14 +347,11 @@ export class DataService {
|
|
|
347
347
|
if (link.type === 'service' && link.accessServiceProtocol === 'wfs') {
|
|
348
348
|
const wfsUrlEndpoint = this.proxy.getProxiedUrl(link.url.toString())
|
|
349
349
|
return from(
|
|
350
|
-
openDataset(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
cacheActive
|
|
357
|
-
)
|
|
350
|
+
openDataset(wfsUrlEndpoint, {
|
|
351
|
+
typeHint: 'wfs',
|
|
352
|
+
wfsFeatureType: link.name,
|
|
353
|
+
enableCache: cacheActive,
|
|
354
|
+
})
|
|
358
355
|
)
|
|
359
356
|
} else if (link.type === 'download') {
|
|
360
357
|
const linkProxifiedUrl = this.proxy.getProxiedUrl(link.url.toString())
|
|
@@ -364,7 +361,10 @@ export class DataService {
|
|
|
364
361
|
? (format as SupportedType)
|
|
365
362
|
: undefined
|
|
366
363
|
return from(
|
|
367
|
-
openDataset(linkProxifiedUrl,
|
|
364
|
+
openDataset(linkProxifiedUrl, {
|
|
365
|
+
typeHint: supportedType,
|
|
366
|
+
enableCache: cacheActive,
|
|
367
|
+
})
|
|
368
368
|
).pipe()
|
|
369
369
|
} else if (
|
|
370
370
|
link.type === 'service' &&
|
|
@@ -374,7 +374,9 @@ export class DataService {
|
|
|
374
374
|
link.url.toString(),
|
|
375
375
|
'geojson'
|
|
376
376
|
)
|
|
377
|
-
return from(
|
|
377
|
+
return from(
|
|
378
|
+
openDataset(url, { typeHint: 'geojson', enableCache: cacheActive })
|
|
379
|
+
).pipe()
|
|
378
380
|
} else if (
|
|
379
381
|
link.type === 'service' &&
|
|
380
382
|
link.accessServiceProtocol === 'ogcFeatures'
|
|
@@ -395,12 +397,10 @@ export class DataService {
|
|
|
395
397
|
}
|
|
396
398
|
const urlWithoutLimit = new URL(geojsonUrl)
|
|
397
399
|
urlWithoutLimit.searchParams.delete('limit')
|
|
398
|
-
return openDataset(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
cacheActive
|
|
403
|
-
)
|
|
400
|
+
return openDataset(urlWithoutLimit.toString(), {
|
|
401
|
+
typeHint: 'geojson',
|
|
402
|
+
enableCache: cacheActive,
|
|
403
|
+
})
|
|
404
404
|
})
|
|
405
405
|
)
|
|
406
406
|
}
|
|
@@ -10,4 +10,3 @@ export * from './lib/style'
|
|
|
10
10
|
export * from './lib/layers-panel/layers-panel.component'
|
|
11
11
|
export * from './lib/add-layer-from-catalog/add-layer-from-catalog.component'
|
|
12
12
|
export * from './lib/add-layer-from-catalog/add-layer-record-preview/add-layer-record-preview.component'
|
|
13
|
-
export * from './lib/geocoding/geocoding.component'
|
|
@@ -2,18 +2,11 @@ import { NgModule } from '@angular/core'
|
|
|
2
2
|
import { StoreModule } from '@ngrx/store'
|
|
3
3
|
import * as fromMap from './+state/map.reducer'
|
|
4
4
|
import { MapFacade } from './+state/map.facade'
|
|
5
|
-
import { GEOCODING_PROVIDER, GeocodingProvider } from './geocoding.service'
|
|
6
5
|
|
|
7
6
|
@NgModule({
|
|
8
7
|
imports: [
|
|
9
8
|
StoreModule.forFeature(fromMap.MAP_FEATURE_KEY, fromMap.mapReducer),
|
|
10
9
|
],
|
|
11
|
-
providers: [
|
|
12
|
-
MapFacade,
|
|
13
|
-
{
|
|
14
|
-
provide: GEOCODING_PROVIDER,
|
|
15
|
-
useValue: ['geonames', { maxRows: 5 }] as GeocodingProvider,
|
|
16
|
-
},
|
|
17
|
-
],
|
|
10
|
+
providers: [MapFacade],
|
|
18
11
|
})
|
|
19
12
|
export class FeatureMapModule {}
|
|
@@ -18,10 +18,12 @@ export class NotificationsService {
|
|
|
18
18
|
error && console.error(error)
|
|
19
19
|
const id = Math.floor(Math.random() * 1000000)
|
|
20
20
|
this.notifications$.next([...this.notifications$.value, { ...content, id }])
|
|
21
|
-
if (typeof timeoutMs
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (typeof timeoutMs !== 'undefined') {
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
this.removeNotificationById(id)
|
|
24
|
+
}, timeoutMs)
|
|
25
|
+
}
|
|
26
|
+
return id
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
removeNotificationById(id: number) {
|
|
@@ -3,6 +3,7 @@ import { createAction, props } from '@ngrx/store'
|
|
|
3
3
|
import {
|
|
4
4
|
CatalogRecord,
|
|
5
5
|
DatasetFeatureCatalog,
|
|
6
|
+
LinkedRecord,
|
|
6
7
|
UserFeedback,
|
|
7
8
|
} from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
8
9
|
|
|
@@ -54,14 +55,9 @@ export const setRelated = createAction(
|
|
|
54
55
|
props<{ related: CatalogRecord[] }>()
|
|
55
56
|
)
|
|
56
57
|
|
|
57
|
-
export const
|
|
58
|
-
'[Metadata view] Set
|
|
59
|
-
props<{
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
export const setSourceOf = createAction(
|
|
63
|
-
'[Metadata view] Set has sources',
|
|
64
|
-
props<{ sourceOf: CatalogRecord[] }>()
|
|
58
|
+
export const setLinkedRecords = createAction(
|
|
59
|
+
'[Metadata view] Set associated records',
|
|
60
|
+
props<{ linkedRecords: LinkedRecord[] }>()
|
|
65
61
|
)
|
|
66
62
|
|
|
67
63
|
/*
|
|
@@ -70,25 +70,16 @@ export class MdViewEffects {
|
|
|
70
70
|
)
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
loadLinkedRecords$ = createEffect(() =>
|
|
74
74
|
this.actions$.pipe(
|
|
75
75
|
ofType(MdViewActions.loadFullMetadataSuccess),
|
|
76
|
-
switchMap(({ full }) => this.recordsRepository.
|
|
77
|
-
map((
|
|
78
|
-
return MdViewActions.
|
|
76
|
+
switchMap(({ full }) => this.recordsRepository.getLinkedRecords(full)),
|
|
77
|
+
map((linkedRecords) => {
|
|
78
|
+
return MdViewActions.setLinkedRecords({ linkedRecords })
|
|
79
79
|
}),
|
|
80
|
-
catchError(() =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
loadSourceOf$ = createEffect(() =>
|
|
85
|
-
this.actions$.pipe(
|
|
86
|
-
ofType(MdViewActions.loadFullMetadataSuccess),
|
|
87
|
-
switchMap(({ full }) => this.recordsRepository.getSourceOf(full)),
|
|
88
|
-
map((sourceOf) => {
|
|
89
|
-
return MdViewActions.setSourceOf({ sourceOf })
|
|
90
|
-
}),
|
|
91
|
-
catchError(() => of(MdViewActions.setSourceOf({ sourceOf: null })))
|
|
80
|
+
catchError(() =>
|
|
81
|
+
of(MdViewActions.setLinkedRecords({ linkedRecords: null }))
|
|
82
|
+
)
|
|
92
83
|
)
|
|
93
84
|
)
|
|
94
85
|
|
|
@@ -74,9 +74,7 @@ export class MdViewFacade {
|
|
|
74
74
|
|
|
75
75
|
related$ = this.store.pipe(select(MdViewSelectors.getRelated))
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
sourceOf$ = this.store.pipe(select(MdViewSelectors.getSourceOf))
|
|
77
|
+
linkedRecords$ = this.store.pipe(select(MdViewSelectors.getLinkedRecords))
|
|
80
78
|
|
|
81
79
|
chartConfig$ = this.store.pipe(select(MdViewSelectors.getChartConfig))
|
|
82
80
|
|