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
|
@@ -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) {
|
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()),
|
|
@@ -40,7 +40,9 @@ export class MdViewEffects {
|
|
|
40
40
|
this.actions$.pipe(
|
|
41
41
|
ofType(MdViewActions.loadFullMetadataSuccess),
|
|
42
42
|
filter(({ full }) => full !== undefined),
|
|
43
|
-
switchMap(({ full }) =>
|
|
43
|
+
switchMap(({ full }) =>
|
|
44
|
+
this.recordsRepository.queryLinkedFeatureCatalog(full)
|
|
45
|
+
),
|
|
44
46
|
map((featureCatalog) =>
|
|
45
47
|
MdViewActions.loadFeatureCatalogSuccess({
|
|
46
48
|
datasetCatalog: featureCatalog,
|
|
@@ -162,7 +162,7 @@ export class MdViewFacade {
|
|
|
162
162
|
link.accessServiceProtocol === 'ogcFeatures'
|
|
163
163
|
) {
|
|
164
164
|
return from(
|
|
165
|
-
this.dataService.getItemsFromOgcApi(link.url.href, 1)
|
|
165
|
+
this.dataService.getItemsFromOgcApi(link.url.href, link.name, 1)
|
|
166
166
|
).pipe(
|
|
167
167
|
map((collectionRecords: OgcApiRecord[]) => {
|
|
168
168
|
return collectionRecords &&
|
|
@@ -22,7 +22,7 @@ export function flattenQueryParams(
|
|
|
22
22
|
const start = (flattened[key] as DateRange).start
|
|
23
23
|
const end = (flattened[key] as DateRange).end
|
|
24
24
|
flattened[key] = [
|
|
25
|
-
`${start ? formatDate(start) : ''}..${formatDate(end)
|
|
25
|
+
`${start ? formatDate(start) : ''}..${end ? formatDate(end) : ''}`,
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -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'
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
@if (fieldType === 'dateRange') {
|
|
2
2
|
<gn-ui-date-range-dropdown
|
|
3
3
|
[title]="title"
|
|
4
|
-
[
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
(endDateChange)="onEndDateChange($event)"
|
|
4
|
+
[dateRange]="(selectedDateRange$ | async) ?? {}"
|
|
5
|
+
(dateRangeChange)="onDateRangeChange($event)"
|
|
6
|
+
[attr.data-cy-field]="fieldName"
|
|
8
7
|
></gn-ui-date-range-dropdown>
|
|
8
|
+
} @else if (fieldType === 'spatialExtent') {
|
|
9
|
+
<gn-ui-spatial-extent-dropdown
|
|
10
|
+
[title]="title"
|
|
11
|
+
[initialBbox]="(selectedBoundingBox$ | async) ?? null"
|
|
12
|
+
[maxFileSizeMb]="spatialExtentMaxFileSize"
|
|
13
|
+
(bboxChange)="onBboxChange($event)"
|
|
14
|
+
(errorChange)="onSpatialExtentError($event)"
|
|
15
|
+
[attr.data-cy-field]="fieldName"
|
|
16
|
+
>
|
|
17
|
+
<gn-ui-location-search
|
|
18
|
+
[placeholder]="'search.filters.spatialExtent.searchLocation' | translate"
|
|
19
|
+
(bboxSelected)="onLocationSelected($event)"
|
|
20
|
+
data-cy="locationSearch"
|
|
21
|
+
></gn-ui-location-search>
|
|
22
|
+
</gn-ui-spatial-extent-dropdown>
|
|
9
23
|
} @else {
|
|
10
24
|
<gn-ui-dropdown-multiselect
|
|
11
25
|
class="w-full"
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ChangeDetectionStrategy,
|
|
3
3
|
Component,
|
|
4
|
+
InjectionToken,
|
|
4
5
|
Input,
|
|
5
6
|
OnInit,
|
|
7
|
+
ViewChild,
|
|
6
8
|
inject,
|
|
7
9
|
} from '@angular/core'
|
|
8
10
|
import {
|
|
9
11
|
Choice,
|
|
10
12
|
DateRangeDropdownComponent,
|
|
11
13
|
DropdownMultiselectComponent,
|
|
14
|
+
SpatialExtentDropdownComponent,
|
|
15
|
+
SpatialExtentDropdownError,
|
|
12
16
|
} from '../../../../../../libs/ui/inputs/src'
|
|
13
17
|
import { Observable, of, switchMap } from 'rxjs'
|
|
14
18
|
import { catchError, filter, map, startWith } from 'rxjs/operators'
|
|
@@ -22,6 +26,22 @@ import {
|
|
|
22
26
|
} from '../utils/service/fields'
|
|
23
27
|
import { DateRange } from '../../../../../../libs/api/repository/src'
|
|
24
28
|
import { CommonModule } from '@angular/common'
|
|
29
|
+
import { BoundingBox } from '../../../../../../libs/util/shared/src'
|
|
30
|
+
import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
|
|
31
|
+
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
|
|
32
|
+
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
|
|
33
|
+
import {
|
|
34
|
+
LocationBbox,
|
|
35
|
+
LocationSearchComponent,
|
|
36
|
+
} from '../location-search/location-search.component'
|
|
37
|
+
|
|
38
|
+
marker('search.filters.spatialExtent.searchLocation')
|
|
39
|
+
|
|
40
|
+
// in MB, null means no limit
|
|
41
|
+
export const SPATIAL_EXTENT_MAX_FILE_SIZE = new InjectionToken<number | null>(
|
|
42
|
+
'spatialExtentMaxFileSize',
|
|
43
|
+
{ factory: () => null }
|
|
44
|
+
)
|
|
25
45
|
|
|
26
46
|
@Component({
|
|
27
47
|
selector: 'gn-ui-filter-dropdown',
|
|
@@ -33,18 +53,27 @@ import { CommonModule } from '@angular/common'
|
|
|
33
53
|
CommonModule,
|
|
34
54
|
DateRangeDropdownComponent,
|
|
35
55
|
DropdownMultiselectComponent,
|
|
56
|
+
SpatialExtentDropdownComponent,
|
|
57
|
+
LocationSearchComponent,
|
|
58
|
+
TranslatePipe,
|
|
36
59
|
],
|
|
37
60
|
})
|
|
38
61
|
export class FilterDropdownComponent implements OnInit {
|
|
39
62
|
private searchFacade = inject(SearchFacade)
|
|
40
63
|
private searchService = inject(SearchService)
|
|
41
64
|
private fieldsService = inject(FieldsService)
|
|
65
|
+
private notificationsService = inject(NotificationsService)
|
|
66
|
+
private translateService = inject(TranslateService)
|
|
42
67
|
|
|
43
68
|
@Input() fieldName: string
|
|
44
69
|
@Input() title: string
|
|
45
70
|
|
|
71
|
+
@ViewChild(SpatialExtentDropdownComponent)
|
|
72
|
+
spatialExtentDropdown: SpatialExtentDropdownComponent
|
|
73
|
+
|
|
74
|
+
spatialExtentMaxFileSize = inject(SPATIAL_EXTENT_MAX_FILE_SIZE)
|
|
75
|
+
|
|
46
76
|
fieldType: FieldType
|
|
47
|
-
dateRange: DateRange
|
|
48
77
|
choices$: Observable<Choice[]>
|
|
49
78
|
selected$ = this.searchFacade.searchFilters$.pipe(
|
|
50
79
|
switchMap((filters) =>
|
|
@@ -57,15 +86,58 @@ export class FilterDropdownComponent implements OnInit {
|
|
|
57
86
|
) as Observable<FieldValue[]>
|
|
58
87
|
|
|
59
88
|
selectedDateRange$ = this.selected$.pipe(
|
|
60
|
-
map((
|
|
89
|
+
map((selected) => (Array.isArray(selected) ? {} : (selected as DateRange)))
|
|
61
90
|
) as Observable<DateRange>
|
|
62
91
|
|
|
92
|
+
selectedBoundingBox$ = this.selected$.pipe(
|
|
93
|
+
map((selected) =>
|
|
94
|
+
Array.isArray(selected) && selected.length > 0
|
|
95
|
+
? (selected as BoundingBox)
|
|
96
|
+
: null
|
|
97
|
+
)
|
|
98
|
+
) as Observable<BoundingBox | null>
|
|
99
|
+
|
|
63
100
|
onSelectedValues(values: unknown[]) {
|
|
64
101
|
this.fieldsService
|
|
65
102
|
.buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] })
|
|
66
103
|
.subscribe((filters) => this.searchService.updateFilters(filters))
|
|
67
104
|
}
|
|
68
105
|
|
|
106
|
+
private spatialExtentErrorNotificationId: number | null = null
|
|
107
|
+
|
|
108
|
+
onBboxChange(bbox: BoundingBox | null) {
|
|
109
|
+
this.fieldsService
|
|
110
|
+
.buildFiltersFromFieldValues({
|
|
111
|
+
[this.fieldName]: bbox,
|
|
112
|
+
})
|
|
113
|
+
.subscribe((filters) => this.searchService.updateFilters(filters))
|
|
114
|
+
this.clearSpatialExtentErrorNotification()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onLocationSelected({ bbox, label }: LocationBbox) {
|
|
118
|
+
this.spatialExtentDropdown.setLocationBbox(bbox, label)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onSpatialExtentError(error: SpatialExtentDropdownError) {
|
|
122
|
+
this.clearSpatialExtentErrorNotification()
|
|
123
|
+
this.spatialExtentErrorNotificationId =
|
|
124
|
+
this.notificationsService.showNotification({
|
|
125
|
+
type: 'error',
|
|
126
|
+
title: this.translateService.instant(
|
|
127
|
+
'search.filters.spatialExtent.error.title'
|
|
128
|
+
),
|
|
129
|
+
text: this.translateService.instant(error.key, error.params),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private clearSpatialExtentErrorNotification() {
|
|
134
|
+
if (this.spatialExtentErrorNotificationId === null) return
|
|
135
|
+
this.notificationsService.removeNotificationById(
|
|
136
|
+
this.spatialExtentErrorNotificationId
|
|
137
|
+
)
|
|
138
|
+
this.spatialExtentErrorNotificationId = null
|
|
139
|
+
}
|
|
140
|
+
|
|
69
141
|
ngOnInit() {
|
|
70
142
|
this.fieldType = this.fieldsService.getFieldType(this.fieldName)
|
|
71
143
|
this.choices$ = this.fieldsService.getAvailableValues(this.fieldName).pipe(
|
|
@@ -80,22 +152,11 @@ export class FilterDropdownComponent implements OnInit {
|
|
|
80
152
|
)
|
|
81
153
|
}
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!end) return
|
|
90
|
-
this.dateRange = { ...this.dateRange, end }
|
|
91
|
-
if (this.dateRange.start && this.dateRange.end) {
|
|
92
|
-
this.fieldsService
|
|
93
|
-
.buildFiltersFromFieldValues({
|
|
94
|
-
[this.fieldName]: this.dateRange,
|
|
95
|
-
})
|
|
96
|
-
.subscribe((filters) => {
|
|
97
|
-
return this.searchService.updateFilters(filters)
|
|
98
|
-
})
|
|
99
|
-
}
|
|
155
|
+
onDateRangeChange(dateRange: DateRange) {
|
|
156
|
+
this.fieldsService
|
|
157
|
+
.buildFiltersFromFieldValues({
|
|
158
|
+
[this.fieldName]: dateRange,
|
|
159
|
+
})
|
|
160
|
+
.subscribe((filters) => this.searchService.updateFilters(filters))
|
|
100
161
|
}
|
|
101
162
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Injectable, InjectionToken, inject } from '@angular/core'
|
|
2
|
+
import {
|
|
3
|
+
queryGeoadmin,
|
|
4
|
+
GeoadminOptions,
|
|
5
|
+
GeocodingResult,
|
|
6
|
+
queryGeonames,
|
|
7
|
+
GeonamesOptions,
|
|
8
|
+
GeoplateformeOptions,
|
|
9
|
+
queryGeoplateforme,
|
|
10
|
+
} from '@geospatial-sdk/geocoding'
|
|
11
|
+
import { from, Observable, throwError } from 'rxjs'
|
|
12
|
+
import { catchError } from 'rxjs/operators'
|
|
13
|
+
|
|
14
|
+
type GeoadminGeocodingProvider = ['geoadmin', GeoadminOptions]
|
|
15
|
+
type GeonamesGeocodingProvider = ['geonames', GeonamesOptions]
|
|
16
|
+
type GeoplateformeGeocodingProvider = ['geoplateforme', GeoplateformeOptions]
|
|
17
|
+
export type GeocodingProvider =
|
|
18
|
+
| GeoadminGeocodingProvider
|
|
19
|
+
| GeonamesGeocodingProvider
|
|
20
|
+
| GeoplateformeGeocodingProvider
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_GEOCODING_PROVIDER = [
|
|
23
|
+
'geonames',
|
|
24
|
+
{ maxRows: 5 },
|
|
25
|
+
] as GeocodingProvider
|
|
26
|
+
|
|
27
|
+
export const GEOCODING_PROVIDER = new InjectionToken<GeocodingProvider>(
|
|
28
|
+
'geocoding-provider',
|
|
29
|
+
{
|
|
30
|
+
providedIn: 'root',
|
|
31
|
+
factory: (): GeocodingProvider => DEFAULT_GEOCODING_PROVIDER,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// these options are arrays in the providers, but they can be configured
|
|
36
|
+
// with a single value for simplicity
|
|
37
|
+
const GEOCODING_PROVIDER_ARRAY_OPTIONS: {
|
|
38
|
+
geoadmin: (keyof GeoadminOptions)[]
|
|
39
|
+
geoplateforme: (keyof GeoplateformeOptions)[]
|
|
40
|
+
} = {
|
|
41
|
+
geoadmin: ['origins', 'features'],
|
|
42
|
+
geoplateforme: [
|
|
43
|
+
'index',
|
|
44
|
+
'category',
|
|
45
|
+
'type',
|
|
46
|
+
'postCode',
|
|
47
|
+
'cityCode',
|
|
48
|
+
'depCode',
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wrapArrayOptions<T extends object>(options: T, keys: (keyof T)[]): T {
|
|
53
|
+
const wrapped = { ...options }
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
if (typeof wrapped[key] === 'string') {
|
|
56
|
+
wrapped[key] = [wrapped[key]] as T[keyof T]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return wrapped
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Injectable({
|
|
63
|
+
providedIn: 'root',
|
|
64
|
+
})
|
|
65
|
+
export class GeocodingService {
|
|
66
|
+
private provider = inject<GeocodingProvider>(GEOCODING_PROVIDER)
|
|
67
|
+
|
|
68
|
+
query(text: string): Observable<GeocodingResult[]> {
|
|
69
|
+
let queryObservable: Observable<GeocodingResult[]>
|
|
70
|
+
switch (this.provider[0]) {
|
|
71
|
+
case 'geoadmin':
|
|
72
|
+
queryObservable = from(
|
|
73
|
+
queryGeoadmin(
|
|
74
|
+
text,
|
|
75
|
+
wrapArrayOptions(
|
|
76
|
+
this.provider[1] as GeoadminOptions,
|
|
77
|
+
GEOCODING_PROVIDER_ARRAY_OPTIONS.geoadmin
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
break
|
|
82
|
+
case 'geonames':
|
|
83
|
+
queryObservable = from(
|
|
84
|
+
queryGeonames(text, this.provider[1] as GeonamesOptions)
|
|
85
|
+
)
|
|
86
|
+
break
|
|
87
|
+
case 'geoplateforme':
|
|
88
|
+
queryObservable = from(
|
|
89
|
+
queryGeoplateforme(text, {
|
|
90
|
+
...wrapArrayOptions(
|
|
91
|
+
this.provider[1] as GeoplateformeOptions,
|
|
92
|
+
GEOCODING_PROVIDER_ARRAY_OPTIONS.geoplateforme
|
|
93
|
+
),
|
|
94
|
+
returnTrueGeometry: true,
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
break
|
|
98
|
+
default:
|
|
99
|
+
return throwError(
|
|
100
|
+
() => new Error(`Unsupported geocoding provider: ${this.provider[0]}`)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return queryObservable.pipe(catchError((error) => throwError(error)))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<gn-ui-autocomplete
|
|
2
|
+
[action]="searchAction"
|
|
3
|
+
[displayWithFn]="displayWithFn"
|
|
4
|
+
[placeholder]="placeholder"
|
|
5
|
+
[displayWithTemplate]="defaultItemTpl"
|
|
6
|
+
[clearOnSelection]="true"
|
|
7
|
+
(itemSelected)="handleItemSelected($event)"
|
|
8
|
+
>
|
|
9
|
+
</gn-ui-autocomplete>
|
|
10
|
+
|
|
11
|
+
<ng-template #defaultItemTpl let-result>
|
|
12
|
+
<div class="py-2">
|
|
13
|
+
@if (getSecondaryLabel(result)) {
|
|
14
|
+
<div class="gn-ui-card-detail">{{ getSecondaryLabel(result) }}</div>
|
|
15
|
+
}
|
|
16
|
+
<div class="gn-ui-card-title">{{ getDisplayLabel(result) }}</div>
|
|
17
|
+
</div>
|
|
18
|
+
</ng-template>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
inject,
|
|
6
|
+
InjectionToken,
|
|
7
|
+
Input,
|
|
8
|
+
Output,
|
|
9
|
+
} from '@angular/core'
|
|
10
|
+
import { Observable } from 'rxjs'
|
|
11
|
+
import pointer from 'jsonpointer'
|
|
12
|
+
import { GeocodingResult } from '@geospatial-sdk/geocoding'
|
|
13
|
+
import { Geometry } from 'geojson'
|
|
14
|
+
import {
|
|
15
|
+
AutocompleteComponent,
|
|
16
|
+
AutocompleteItem,
|
|
17
|
+
} from '../../../../../../libs/ui/inputs/src'
|
|
18
|
+
import { BoundingBox, getGeometryBoundingBox } from '../../../../../../libs/util/shared/src'
|
|
19
|
+
import { GeocodingService } from '../geocoding/geocoding.service'
|
|
20
|
+
|
|
21
|
+
// JSON Pointers resolved against a geocoding result to build the labels shown in the dropdown
|
|
22
|
+
export interface GeocodingProviderLabels {
|
|
23
|
+
main?: string
|
|
24
|
+
secondary?: string
|
|
25
|
+
tertiary?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LocationBbox {
|
|
29
|
+
bbox: BoundingBox
|
|
30
|
+
label: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const GEOCODING_PROVIDER_LABELS =
|
|
34
|
+
new InjectionToken<GeocodingProviderLabels>('geocodingProviderLabels', {
|
|
35
|
+
factory: () => ({}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
@Component({
|
|
39
|
+
selector: 'gn-ui-location-search',
|
|
40
|
+
templateUrl: './location-search.component.html',
|
|
41
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
42
|
+
standalone: true,
|
|
43
|
+
imports: [AutocompleteComponent],
|
|
44
|
+
})
|
|
45
|
+
export class LocationSearchComponent {
|
|
46
|
+
private geocodingService = inject(GeocodingService)
|
|
47
|
+
private labels = inject(GEOCODING_PROVIDER_LABELS)
|
|
48
|
+
|
|
49
|
+
@Input() placeholder = ''
|
|
50
|
+
@Output() resultSelected = new EventEmitter<GeocodingResult>()
|
|
51
|
+
@Output() bboxSelected = new EventEmitter<LocationBbox>()
|
|
52
|
+
|
|
53
|
+
displayWithFn = (item: AutocompleteItem) => (item as GeocodingResult).label
|
|
54
|
+
|
|
55
|
+
searchAction = (text: string): Observable<AutocompleteItem[]> =>
|
|
56
|
+
this.geocodingService.query(text)
|
|
57
|
+
|
|
58
|
+
getSecondaryLabel(result: GeocodingResult): string | undefined {
|
|
59
|
+
return this.resolveLabel(result, this.labels.secondary)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getMainLabel(result: GeocodingResult): string {
|
|
63
|
+
return this.resolveLabel(result, this.labels.main) ?? result.label
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getTertiaryLabel(result: GeocodingResult): string | undefined {
|
|
67
|
+
return this.resolveLabel(result, this.labels.tertiary)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getDisplayLabel(result: GeocodingResult): string {
|
|
71
|
+
const tertiary = this.getTertiaryLabel(result)
|
|
72
|
+
return this.getMainLabel(result) + (tertiary ? ', ' + tertiary : '')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private resolveLabel(
|
|
76
|
+
result: GeocodingResult,
|
|
77
|
+
path?: string
|
|
78
|
+
): string | undefined {
|
|
79
|
+
if (!path) return undefined
|
|
80
|
+
let value: unknown
|
|
81
|
+
try {
|
|
82
|
+
value = pointer.get(result, path)
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
const values = Array.isArray(value) ? value : [value]
|
|
87
|
+
const label = values
|
|
88
|
+
.filter((v) => typeof v === 'string' || typeof v === 'number')
|
|
89
|
+
.join(', ')
|
|
90
|
+
return label || undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// geoplateforme keeps a simplified geometry in geom and puts the real one,
|
|
94
|
+
// as a JSON string, in properties.truegeometry when returnTrueGeometry is set
|
|
95
|
+
private getGeometry(result: GeocodingResult): Geometry | null {
|
|
96
|
+
const trueGeometry = result.properties?.['truegeometry']
|
|
97
|
+
if (typeof trueGeometry === 'string') {
|
|
98
|
+
return JSON.parse(trueGeometry)
|
|
99
|
+
}
|
|
100
|
+
return (trueGeometry as Geometry) ?? result.geom
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleItemSelected(item: AutocompleteItem) {
|
|
104
|
+
const result = item as GeocodingResult
|
|
105
|
+
this.resultSelected.emit(result)
|
|
106
|
+
const geometry = this.getGeometry(result)
|
|
107
|
+
if (geometry) {
|
|
108
|
+
this.bboxSelected.emit({
|
|
109
|
+
bbox: getGeometryBoundingBox(geometry),
|
|
110
|
+
label: this.getDisplayLabel(result),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component, Input, OnInit, inject } from '@angular/core'
|
|
2
|
-
import { CommonModule
|
|
2
|
+
import { CommonModule } from '@angular/common'
|
|
3
3
|
import {
|
|
4
4
|
catchError,
|
|
5
5
|
firstValueFrom,
|
|
@@ -15,11 +15,15 @@ import { FieldType, FieldValue } from '../utils/service/fields'
|
|
|
15
15
|
import { SearchFacade } from '../state/search.facade'
|
|
16
16
|
import { SearchService } from '../utils/service/search.service'
|
|
17
17
|
import { FieldsService } from '../utils/service/fields.service'
|
|
18
|
-
import { formatUserInfo } from '../../../../../../libs/util/shared/src'
|
|
18
|
+
import { DateService, formatUserInfo } from '../../../../../../libs/util/shared/src'
|
|
19
19
|
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')
|
|
25
|
+
|
|
26
|
+
const OPEN_BOUND = '…'
|
|
23
27
|
|
|
24
28
|
interface DisplayedValue {
|
|
25
29
|
label: string
|
|
@@ -32,13 +36,12 @@ interface DisplayedValue {
|
|
|
32
36
|
imports: [CommonModule, BadgeComponent],
|
|
33
37
|
templateUrl: './search-filters-summary-item.component.html',
|
|
34
38
|
styleUrls: ['./search-filters-summary-item.component.css'],
|
|
35
|
-
providers: [DatePipe],
|
|
36
39
|
})
|
|
37
40
|
export class SearchFiltersSummaryItemComponent implements OnInit {
|
|
38
41
|
private searchFacade = inject(SearchFacade)
|
|
39
42
|
private searchService = inject(SearchService)
|
|
40
43
|
private fieldsService = inject(FieldsService)
|
|
41
|
-
private
|
|
44
|
+
private dateService = inject(DateService)
|
|
42
45
|
private translate = inject(TranslateService)
|
|
43
46
|
|
|
44
47
|
@Input() fieldName: string
|
|
@@ -82,12 +85,10 @@ export class SearchFiltersSummaryItemComponent implements OnInit {
|
|
|
82
85
|
getReadableValues(fieldValues: FieldValue[] | DateRange[]): DisplayedValue[] {
|
|
83
86
|
return fieldValues.map((value) => {
|
|
84
87
|
if (this.fieldType === 'dateRange') {
|
|
88
|
+
const { start, end } = value as DateRange
|
|
85
89
|
return {
|
|
86
90
|
value,
|
|
87
|
-
label: `${this.
|
|
88
|
-
value.start,
|
|
89
|
-
'dd.MM.yyyy'
|
|
90
|
-
)} - ${this.datePipe.transform(value.end, 'dd.MM.yyyy')}`,
|
|
91
|
+
label: `${this.formatBound(start)} - ${this.formatBound(end)}`,
|
|
91
92
|
}
|
|
92
93
|
} else if (this.fieldName === 'user') {
|
|
93
94
|
return { value, label: formatUserInfo(value) }
|
|
@@ -97,6 +98,16 @@ export class SearchFiltersSummaryItemComponent implements OnInit {
|
|
|
97
98
|
})
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
private formatBound(date: Date | undefined): string {
|
|
102
|
+
return date
|
|
103
|
+
? this.dateService.formatDate(date, {
|
|
104
|
+
day: '2-digit',
|
|
105
|
+
month: '2-digit',
|
|
106
|
+
year: 'numeric',
|
|
107
|
+
})
|
|
108
|
+
: OPEN_BOUND
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
async removeFilterValue(fieldValue: FieldValue | DateRange) {
|
|
101
112
|
const currentFieldValues: DisplayedValue[] = await firstValueFrom(
|
|
102
113
|
this.fieldValues$
|
|
@@ -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,
|