geonetwork-ui 2.11.0-dev.aed5b2e5b → 2.11.0-dev.b75ae786a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/geonetwork-ui.mjs +3219 -3235
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +113 -83
- package/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +120 -34
- package/src/libs/common/domain/src/lib/model/search/filter.model.ts +24 -2
- package/src/libs/feature/map/src/index.ts +0 -2
- package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
- package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
- package/src/libs/feature/search/src/index.ts +2 -0
- package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +3 -0
- package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +21 -4
- package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +16 -7
- 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 +2 -0
- package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
- package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +14 -2
- package/src/libs/feature/search/src/lib/utils/service/fields.ts +49 -5
- package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
- 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/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 +5 -11
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +34 -4
- 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 +2 -0
- package/src/libs/util/app-config/src/lib/app-config.ts +20 -0
- package/src/libs/util/app-config/src/lib/fixtures.ts +16 -1
- package/src/libs/util/app-config/src/lib/model.ts +14 -1
- package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
- package/src/libs/util/shared/src/lib/utils/geojson.ts +24 -0
- package/translations/de.json +10 -4
- package/translations/en.json +11 -5
- package/translations/es.json +10 -4
- package/translations/fr.json +11 -5
- package/translations/it.json +10 -4
- package/translations/nl.json +10 -4
- package/translations/pt.json +10 -4
- package/translations/sk.json +10 -4
- package/src/libs/feature/map/src/lib/geocoding/geocoding.component.css +0 -0
- 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/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
|
@@ -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 {
|
|
@@ -264,8 +284,68 @@ export class ElasticsearchService {
|
|
|
264
284
|
return this.metadataLang === 'current'
|
|
265
285
|
}
|
|
266
286
|
|
|
267
|
-
private
|
|
287
|
+
private findSpatialFilterExtent(
|
|
268
288
|
filters: FieldFilters | FiltersAggregationParams | string
|
|
289
|
+
): BoundingBox | undefined {
|
|
290
|
+
if (typeof filters === 'string') {
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
return Object.values(filters).find(isBoundingBox)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// builds a query matching records whose dates intersect the filter range
|
|
297
|
+
private buildDateRangeQuery(searchField: string, dateRange: DateRange) {
|
|
298
|
+
// fields ending with 'DateRange' maps as ES range type
|
|
299
|
+
if (searchField.endsWith('DateRange')) {
|
|
300
|
+
return {
|
|
301
|
+
range: {
|
|
302
|
+
[searchField]: {
|
|
303
|
+
...(dateRange.start && { gte: formatDate(dateRange.start) }),
|
|
304
|
+
...(dateRange.end && { lte: formatDate(dateRange.end) }),
|
|
305
|
+
format: 'yyyy-MM-dd',
|
|
306
|
+
relation: 'intersects',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const minField = `${searchField}Min`
|
|
312
|
+
const maxField = `${searchField}Max`
|
|
313
|
+
if (minField in this.runtimeFields && maxField in this.runtimeFields) {
|
|
314
|
+
// min/max runtime fields form an interval per record; match if it intersects the filter range
|
|
315
|
+
const filter = [
|
|
316
|
+
dateRange.start && {
|
|
317
|
+
range: {
|
|
318
|
+
[maxField]: {
|
|
319
|
+
gte: formatDate(dateRange.start),
|
|
320
|
+
format: 'yyyy-MM-dd',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
dateRange.end && {
|
|
325
|
+
range: {
|
|
326
|
+
[minField]: {
|
|
327
|
+
lte: formatDate(dateRange.end),
|
|
328
|
+
format: 'yyyy-MM-dd',
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
].filter(Boolean)
|
|
333
|
+
return { bool: { filter } }
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
range: {
|
|
337
|
+
[searchField]: {
|
|
338
|
+
...(dateRange.start && { gte: formatDate(dateRange.start) }),
|
|
339
|
+
...(dateRange.end && { lte: formatDate(dateRange.end) }),
|
|
340
|
+
format: 'yyyy-MM-dd',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private filtersToQuery(
|
|
347
|
+
filters: FieldFilters | FiltersAggregationParams | string,
|
|
348
|
+
spatialFilterExtent = this.findSpatialFilterExtent(filters)
|
|
269
349
|
): FilterQuery {
|
|
270
350
|
const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
|
|
271
351
|
const makeQuery = (filter: FieldFilter): string => {
|
|
@@ -286,7 +366,9 @@ export class ElasticsearchService {
|
|
|
286
366
|
? filters
|
|
287
367
|
: Object.keys(filters)
|
|
288
368
|
.filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
|
|
369
|
+
.filter((fieldname) => !isBoundingBox(filters[fieldname]))
|
|
289
370
|
.filter((fieldname) => !isDateRange(filters[fieldname]))
|
|
371
|
+
.filter((fieldname) => !Array.isArray(filters[fieldname]))
|
|
290
372
|
.filter(
|
|
291
373
|
(fieldname) =>
|
|
292
374
|
filters[fieldname] &&
|
|
@@ -299,37 +381,33 @@ export class ElasticsearchService {
|
|
|
299
381
|
if (filters['gn-ui-crossFieldFilter']) {
|
|
300
382
|
queryString = `${queryString} AND (${filters['gn-ui-crossFieldFilter']})`
|
|
301
383
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
searchField,
|
|
307
|
-
dateRange,
|
|
308
|
-
} as {
|
|
309
|
-
searchField: string
|
|
310
|
-
dateRange: DateRange
|
|
311
|
-
}
|
|
312
|
-
})[0]
|
|
384
|
+
const queryRanges = Object.entries(filters).filter(([, value]) =>
|
|
385
|
+
isDateRange(value)
|
|
386
|
+
) as [string, DateRange][]
|
|
313
387
|
const queryParts = [
|
|
314
388
|
queryString && {
|
|
315
389
|
query_string: {
|
|
316
390
|
query: queryString,
|
|
317
391
|
},
|
|
318
392
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
393
|
+
...queryRanges.map(([searchField, dateRange]) =>
|
|
394
|
+
this.buildDateRangeQuery(searchField, dateRange)
|
|
395
|
+
),
|
|
396
|
+
spatialFilterExtent && {
|
|
397
|
+
geo_shape: {
|
|
398
|
+
geom: {
|
|
399
|
+
shape: {
|
|
400
|
+
type: 'envelope',
|
|
401
|
+
// spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
|
|
402
|
+
coordinates: [
|
|
403
|
+
[spatialFilterExtent[0], spatialFilterExtent[3]],
|
|
404
|
+
[spatialFilterExtent[2], spatialFilterExtent[1]],
|
|
405
|
+
],
|
|
330
406
|
},
|
|
407
|
+
relation: 'intersects',
|
|
331
408
|
},
|
|
332
409
|
},
|
|
410
|
+
},
|
|
333
411
|
].filter(Boolean)
|
|
334
412
|
return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
|
|
335
413
|
}
|
|
@@ -368,7 +446,12 @@ export class ElasticsearchService {
|
|
|
368
446
|
},
|
|
369
447
|
})
|
|
370
448
|
}
|
|
371
|
-
|
|
449
|
+
// a spatial extent filter takes precedence over the preference geometry for boosting
|
|
450
|
+
const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
|
|
451
|
+
const queryFilters = this.filtersToQuery(
|
|
452
|
+
fieldSearchFilters,
|
|
453
|
+
spatialFilterExtent
|
|
454
|
+
)
|
|
372
455
|
if (queryFilters) {
|
|
373
456
|
filter.push(...queryFilters)
|
|
374
457
|
}
|
|
@@ -379,7 +462,10 @@ export class ElasticsearchService {
|
|
|
379
462
|
},
|
|
380
463
|
})
|
|
381
464
|
}
|
|
382
|
-
|
|
465
|
+
const boostGeometry = spatialFilterExtent
|
|
466
|
+
? bboxToPolygon(spatialFilterExtent)
|
|
467
|
+
: geometry
|
|
468
|
+
if (boostGeometry) {
|
|
383
469
|
// boosts applied using the filter geometry:
|
|
384
470
|
// * records completely within the geometry receive a boost of 5
|
|
385
471
|
// * records intersecting the geometry receive a boost of 2
|
|
@@ -389,7 +475,7 @@ export class ElasticsearchService {
|
|
|
389
475
|
{
|
|
390
476
|
geo_shape: {
|
|
391
477
|
geom: {
|
|
392
|
-
shape:
|
|
478
|
+
shape: boostGeometry,
|
|
393
479
|
relation: 'within',
|
|
394
480
|
},
|
|
395
481
|
boost: 5.0,
|
|
@@ -398,7 +484,7 @@ export class ElasticsearchService {
|
|
|
398
484
|
{
|
|
399
485
|
geo_shape: {
|
|
400
486
|
geom: {
|
|
401
|
-
shape:
|
|
487
|
+
shape: boostGeometry,
|
|
402
488
|
relation: 'intersects',
|
|
403
489
|
},
|
|
404
490
|
boost: 2.0,
|
|
@@ -409,7 +495,7 @@ export class ElasticsearchService {
|
|
|
409
495
|
// this will boost the results variably depending on their distance from the given geometry
|
|
410
496
|
// note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
|
|
411
497
|
// combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
|
|
412
|
-
const bbox = getGeometryBoundingBox(
|
|
498
|
+
const bbox = getGeometryBoundingBox(boostGeometry)
|
|
413
499
|
const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
|
|
414
500
|
const northToCenter = new LineString([
|
|
415
501
|
[center[0], bbox[3]],
|
|
@@ -6,17 +6,39 @@ export type FieldFilterByRange = {
|
|
|
6
6
|
start?: Date
|
|
7
7
|
end?: Date
|
|
8
8
|
}
|
|
9
|
+
// matches util/shared's BoundingBox; duplicated here to avoid a circular
|
|
10
|
+
// dependency (util/shared already depends on common/domain)
|
|
11
|
+
export type FieldFilterByBoundingBox = [number, number, number, number]
|
|
9
12
|
|
|
10
13
|
export type FieldFilter =
|
|
11
14
|
| FieldFilterByExpression
|
|
12
15
|
| FieldFilterByValues
|
|
13
16
|
| FieldFilterByRange
|
|
17
|
+
| FieldFilterByBoundingBox
|
|
14
18
|
export type FieldFilters = Record<FieldName, FieldFilter>
|
|
15
19
|
|
|
16
20
|
export type QueryString = {
|
|
17
21
|
query_string: string
|
|
18
22
|
}
|
|
19
23
|
export type QueryRange = {
|
|
20
|
-
range: Record<
|
|
24
|
+
range: Record<
|
|
25
|
+
string,
|
|
26
|
+
{ format: string; gte?: string; lte?: string; relation?: string }
|
|
27
|
+
>
|
|
21
28
|
}
|
|
22
|
-
export type
|
|
29
|
+
export type QueryGeoShape = {
|
|
30
|
+
geo_shape: {
|
|
31
|
+
geom: {
|
|
32
|
+
shape: { type: string; coordinates: number[][] }
|
|
33
|
+
relation: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export type QueryBool = {
|
|
38
|
+
bool: {
|
|
39
|
+
filter: QueryRange[]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export type FilterQuery = Array<
|
|
43
|
+
QueryString | QueryRange | QueryGeoShape | QueryBool
|
|
44
|
+
>
|
|
@@ -6,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 {}
|
package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts
CHANGED
|
@@ -33,7 +33,6 @@ import { RecordsRepositoryInterface } from '../../../../../../libs/common/domain
|
|
|
33
33
|
import { NOT_KNOWN_CONSTRAINT } from '../../../../../../libs/feature/editor/src'
|
|
34
34
|
import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
|
|
35
35
|
import { ButtonComponent, TextInputComponent } from '../../../../../../libs/ui/inputs/src'
|
|
36
|
-
import { getOptionalEditorConfig } from '../../../../../../libs/util/app-config/src'
|
|
37
36
|
import { NgIcon, provideIcons, provideNgIconsConfig } from '@ng-icons/core'
|
|
38
37
|
import { iconoirAppWindow, iconoirPlusCircle } from '@ng-icons/iconoir'
|
|
39
38
|
import { matCloseOutline } from '@ng-icons/material-icons/outline'
|
|
@@ -48,6 +47,10 @@ marker('notify.reuse.form.error.title')
|
|
|
48
47
|
marker('notify.reuse.form.error.body')
|
|
49
48
|
|
|
50
49
|
export const REUSE_FORM_URL = new InjectionToken<string>('reuseFormUrl')
|
|
50
|
+
// language assigned to the created reuse record; falls back to the current UI language
|
|
51
|
+
export const NEW_RECORD_DEFAULT_LANGUAGE = new InjectionToken<string>(
|
|
52
|
+
'newRecordDefaultLanguage'
|
|
53
|
+
)
|
|
51
54
|
|
|
52
55
|
@Component({
|
|
53
56
|
selector: 'gn-ui-notify-reuse-form',
|
|
@@ -86,6 +89,9 @@ export class NotifyReuseFormComponent implements OnDestroy {
|
|
|
86
89
|
private readonly notificationsService = inject(NotificationsService)
|
|
87
90
|
private readonly platformServiceInterface = inject(PlatformServiceInterface)
|
|
88
91
|
reuseFormUrl = inject(REUSE_FORM_URL, { optional: true })
|
|
92
|
+
private newRecordDefaultLanguage = inject(NEW_RECORD_DEFAULT_LANGUAGE, {
|
|
93
|
+
optional: true,
|
|
94
|
+
})
|
|
89
95
|
private locationStrategy = inject(LocationStrategy)
|
|
90
96
|
|
|
91
97
|
me = toSignal(this.platformServiceInterface.getMe())
|
|
@@ -158,8 +164,7 @@ export class NotifyReuseFormComponent implements OnDestroy {
|
|
|
158
164
|
submit() {
|
|
159
165
|
if (!this.isFormValid()) return
|
|
160
166
|
const defaultLang =
|
|
161
|
-
|
|
162
|
-
this.translate.currentLang
|
|
167
|
+
this.newRecordDefaultLanguage ?? this.translate.currentLang
|
|
163
168
|
const onlineResource: OnlineLinkResource = {
|
|
164
169
|
type: 'link',
|
|
165
170
|
url: new URL(this.url()),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './lib/feature-search.module'
|
|
2
|
+
export * from './lib/geocoding/geocoding.service'
|
|
3
|
+
export * from './lib/location-search/location-search.component'
|
|
2
4
|
export * from './lib/filter-geometry.token'
|
|
3
5
|
export * from './lib/record-url.token'
|
|
4
6
|
export * from './lib/state/actions'
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
[title]="title"
|
|
4
4
|
[dateRange]="(selectedDateRange$ | async) ?? {}"
|
|
5
5
|
(dateRangeChange)="onDateRangeChange($event)"
|
|
6
|
+
[attr.data-cy-field]="fieldName"
|
|
6
7
|
></gn-ui-date-range-dropdown>
|
|
7
8
|
} @else if (fieldType === 'spatialExtent') {
|
|
8
9
|
<gn-ui-spatial-extent-dropdown
|
|
9
10
|
[title]="title"
|
|
11
|
+
[initialBbox]="(selectedBoundingBox$ | async) ?? null"
|
|
10
12
|
[maxFileSizeMb]="spatialExtentMaxFileSize"
|
|
11
13
|
(bboxChange)="onBboxChange($event)"
|
|
12
14
|
(errorChange)="onSpatialExtentError($event)"
|
|
15
|
+
[attr.data-cy-field]="fieldName"
|
|
13
16
|
></gn-ui-spatial-extent-dropdown>
|
|
14
17
|
} @else {
|
|
15
18
|
<gn-ui-dropdown-multiselect
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChangeDetectionStrategy,
|
|
3
3
|
Component,
|
|
4
|
+
InjectionToken,
|
|
4
5
|
Input,
|
|
5
6
|
OnInit,
|
|
6
7
|
inject,
|
|
@@ -27,7 +28,12 @@ import { CommonModule } from '@angular/common'
|
|
|
27
28
|
import { BoundingBox } from '../../../../../../libs/util/shared/src'
|
|
28
29
|
import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
|
|
29
30
|
import { TranslateService } from '@ngx-translate/core'
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
// in MB, null means no limit
|
|
33
|
+
export const SPATIAL_EXTENT_MAX_FILE_SIZE = new InjectionToken<number | null>(
|
|
34
|
+
'spatialExtentMaxFileSize',
|
|
35
|
+
{ factory: () => null }
|
|
36
|
+
)
|
|
31
37
|
|
|
32
38
|
@Component({
|
|
33
39
|
selector: 'gn-ui-filter-dropdown',
|
|
@@ -52,8 +58,7 @@ export class FilterDropdownComponent implements OnInit {
|
|
|
52
58
|
@Input() fieldName: string
|
|
53
59
|
@Input() title: string
|
|
54
60
|
|
|
55
|
-
spatialExtentMaxFileSize =
|
|
56
|
-
getOptionalSearchConfig()?.SPATIAL_EXTENT_MAX_FILE_SIZE
|
|
61
|
+
spatialExtentMaxFileSize = inject(SPATIAL_EXTENT_MAX_FILE_SIZE)
|
|
57
62
|
|
|
58
63
|
fieldType: FieldType
|
|
59
64
|
choices$: Observable<Choice[]>
|
|
@@ -71,6 +76,14 @@ export class FilterDropdownComponent implements OnInit {
|
|
|
71
76
|
map((selected) => (Array.isArray(selected) ? {} : (selected as DateRange)))
|
|
72
77
|
) as Observable<DateRange>
|
|
73
78
|
|
|
79
|
+
selectedBoundingBox$ = this.selected$.pipe(
|
|
80
|
+
map((selected) =>
|
|
81
|
+
Array.isArray(selected) && selected.length > 0
|
|
82
|
+
? (selected as BoundingBox)
|
|
83
|
+
: null
|
|
84
|
+
)
|
|
85
|
+
) as Observable<BoundingBox | null>
|
|
86
|
+
|
|
74
87
|
onSelectedValues(values: unknown[]) {
|
|
75
88
|
this.fieldsService
|
|
76
89
|
.buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] })
|
|
@@ -80,7 +93,11 @@ export class FilterDropdownComponent implements OnInit {
|
|
|
80
93
|
private spatialExtentErrorNotificationId: number | null = null
|
|
81
94
|
|
|
82
95
|
onBboxChange(bbox: BoundingBox | null) {
|
|
83
|
-
|
|
96
|
+
this.fieldsService
|
|
97
|
+
.buildFiltersFromFieldValues({
|
|
98
|
+
[this.fieldName]: bbox,
|
|
99
|
+
})
|
|
100
|
+
.subscribe((filters) => this.searchService.updateFilters(filters))
|
|
84
101
|
this.clearSpatialExtentErrorNotification()
|
|
85
102
|
}
|
|
86
103
|
|
|
@@ -5,22 +5,31 @@ import {
|
|
|
5
5
|
GeocodingResult,
|
|
6
6
|
queryGeonames,
|
|
7
7
|
GeonamesOptions,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
GeoplateformeOptions,
|
|
9
|
+
queryGeoplateforme,
|
|
10
10
|
} from '@geospatial-sdk/geocoding'
|
|
11
11
|
import { from, Observable, throwError } from 'rxjs'
|
|
12
12
|
import { catchError } from 'rxjs/operators'
|
|
13
13
|
|
|
14
14
|
type GeoadminGeocodingProvider = ['geoadmin', GeoadminOptions]
|
|
15
15
|
type GeonamesGeocodingProvider = ['geonames', GeonamesOptions]
|
|
16
|
-
type
|
|
16
|
+
type GeoplateformeGeocodingProvider = ['geoplateforme', GeoplateformeOptions]
|
|
17
17
|
export type GeocodingProvider =
|
|
18
18
|
| GeoadminGeocodingProvider
|
|
19
19
|
| GeonamesGeocodingProvider
|
|
20
|
-
|
|
|
20
|
+
| GeoplateformeGeocodingProvider
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_GEOCODING_PROVIDER = [
|
|
23
|
+
'geonames',
|
|
24
|
+
{ maxRows: 5 },
|
|
25
|
+
] as GeocodingProvider
|
|
21
26
|
|
|
22
27
|
export const GEOCODING_PROVIDER = new InjectionToken<GeocodingProvider>(
|
|
23
|
-
'geocoding-provider'
|
|
28
|
+
'geocoding-provider',
|
|
29
|
+
{
|
|
30
|
+
providedIn: 'root',
|
|
31
|
+
factory: (): GeocodingProvider => DEFAULT_GEOCODING_PROVIDER,
|
|
32
|
+
}
|
|
24
33
|
)
|
|
25
34
|
|
|
26
35
|
@Injectable({
|
|
@@ -42,9 +51,9 @@ export class GeocodingService {
|
|
|
42
51
|
queryGeonames(text, this.provider[1] as GeonamesOptions)
|
|
43
52
|
)
|
|
44
53
|
break
|
|
45
|
-
case '
|
|
54
|
+
case 'geoplateforme':
|
|
46
55
|
queryObservable = from(
|
|
47
|
-
|
|
56
|
+
queryGeoplateforme(text, this.provider[1] as GeoplateformeOptions)
|
|
48
57
|
)
|
|
49
58
|
break
|
|
50
59
|
default:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
inject,
|
|
6
|
+
Input,
|
|
7
|
+
Output,
|
|
8
|
+
TemplateRef,
|
|
9
|
+
} from '@angular/core'
|
|
10
|
+
import { Observable } from 'rxjs'
|
|
11
|
+
import { GeocodingResult } from '@geospatial-sdk/geocoding'
|
|
12
|
+
import {
|
|
13
|
+
AutocompleteComponent,
|
|
14
|
+
AutocompleteItem,
|
|
15
|
+
} from '../../../../../../libs/ui/inputs/src'
|
|
16
|
+
import { GeocodingService } from '../geocoding/geocoding.service'
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'gn-ui-location-search',
|
|
20
|
+
templateUrl: './location-search.component.html',
|
|
21
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
22
|
+
standalone: true,
|
|
23
|
+
imports: [AutocompleteComponent],
|
|
24
|
+
})
|
|
25
|
+
export class LocationSearchComponent {
|
|
26
|
+
private geocodingService = inject(GeocodingService)
|
|
27
|
+
|
|
28
|
+
@Input() placeholder = ''
|
|
29
|
+
@Input() displayWithTemplate?: TemplateRef<{ $implicit: GeocodingResult }>
|
|
30
|
+
@Output() resultSelected = new EventEmitter<GeocodingResult>()
|
|
31
|
+
|
|
32
|
+
displayWithFn = (item: AutocompleteItem) => (item as GeocodingResult).label
|
|
33
|
+
|
|
34
|
+
searchAction = (text: string): Observable<AutocompleteItem[]> =>
|
|
35
|
+
this.geocodingService.query(text)
|
|
36
|
+
|
|
37
|
+
handleItemSelected(item: AutocompleteItem) {
|
|
38
|
+
this.resultSelected.emit(item as GeocodingResult)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -20,6 +20,8 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker'
|
|
|
20
20
|
|
|
21
21
|
marker('search.filters.summaryLabel.user')
|
|
22
22
|
marker('search.filters.summaryLabel.changeDate')
|
|
23
|
+
marker('search.filters.summaryLabel.resourceCreationRevisionDate')
|
|
24
|
+
marker('search.filters.summaryLabel.temporalExtent')
|
|
23
25
|
|
|
24
26
|
const OPEN_BOUND = '…'
|
|
25
27
|
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
SortByField,
|
|
9
9
|
} from '../../../../../../libs/common/domain/src/lib/model/search'
|
|
10
10
|
import { DEFAULT_PAGE_SIZE, FIELDS_SUMMARY } from '../constants'
|
|
11
|
-
import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/src'
|
|
12
11
|
import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
|
|
13
12
|
|
|
14
13
|
export const SEARCH_FEATURE_KEY = 'searchState'
|
|
@@ -56,9 +55,7 @@ export const initSearch = (): SearchStateSearch => {
|
|
|
56
55
|
},
|
|
57
56
|
params: {
|
|
58
57
|
filters: {},
|
|
59
|
-
pageSize:
|
|
60
|
-
? getOptionalSearchConfig().LIMIT
|
|
61
|
-
: DEFAULT_PAGE_SIZE,
|
|
58
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
62
59
|
currentPage: 0,
|
|
63
60
|
favoritesOnly: false,
|
|
64
61
|
useSpatialFilter: true,
|
|
@@ -2,6 +2,7 @@ import { Injectable, Injector, inject } from '@angular/core'
|
|
|
2
2
|
import {
|
|
3
3
|
AbstractSearchField,
|
|
4
4
|
AvailableServicesField,
|
|
5
|
+
BoundingBoxSearchField,
|
|
5
6
|
DateRangeSearchField,
|
|
6
7
|
FieldValue,
|
|
7
8
|
FullTextSearchField,
|
|
@@ -10,12 +11,12 @@ import {
|
|
|
10
11
|
MultilingualSearchField,
|
|
11
12
|
OrganizationSearchField,
|
|
12
13
|
OwnerSearchField,
|
|
14
|
+
ResourceCreationRevisionDateSearchField,
|
|
13
15
|
ResourceTypeLegacyField,
|
|
14
16
|
SimpleSearchField,
|
|
15
17
|
TranslatedSearchField,
|
|
16
18
|
RecordKindField,
|
|
17
19
|
UserSearchField,
|
|
18
|
-
SpatialExtentSearchField,
|
|
19
20
|
} from './fields'
|
|
20
21
|
import { forkJoin, Observable, of } from 'rxjs'
|
|
21
22
|
import { map } from 'rxjs/operators'
|
|
@@ -43,6 +44,8 @@ marker('search.filters.producerOrg')
|
|
|
43
44
|
marker('search.filters.publisherOrg')
|
|
44
45
|
marker('search.filters.user')
|
|
45
46
|
marker('search.filters.changeDate')
|
|
47
|
+
marker('search.filters.resourceCreationRevisionDate')
|
|
48
|
+
marker('search.filters.temporalExtent')
|
|
46
49
|
marker('search.filters.spatialExtent')
|
|
47
50
|
@Injectable({
|
|
48
51
|
providedIn: 'root',
|
|
@@ -95,8 +98,17 @@ export class FieldsService {
|
|
|
95
98
|
),
|
|
96
99
|
user: new UserSearchField(this.injector),
|
|
97
100
|
changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'),
|
|
101
|
+
resourceCreationRevisionDate: new ResourceCreationRevisionDateSearchField(
|
|
102
|
+
this.injector,
|
|
103
|
+
'desc'
|
|
104
|
+
),
|
|
105
|
+
temporalExtent: new DateRangeSearchField(
|
|
106
|
+
'resourceTemporalExtentDateRange',
|
|
107
|
+
this.injector,
|
|
108
|
+
'desc'
|
|
109
|
+
),
|
|
98
110
|
availableServices: new AvailableServicesField(this.injector),
|
|
99
|
-
spatialExtent: new
|
|
111
|
+
spatialExtent: new BoundingBoxSearchField('spatialExtent', this.injector),
|
|
100
112
|
} as Record<string, AbstractSearchField>
|
|
101
113
|
|
|
102
114
|
get supportedFields() {
|