geonetwork-ui 2.11.0-dev.f5ee66b24 → 2.11.0-dev.fb48cfeee

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.
Files changed (74) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1570 -1196
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +153 -71
  4. package/index.d.ts.map +1 -1
  5. package/package.json +6 -5
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +2 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/mel.records.dataset+featurecatalog.ts +395 -0
  8. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +35 -8
  9. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +19 -0
  10. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +17 -6
  11. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +110 -0
  12. package/src/libs/api/metadata-converter/src/lib/iso19115-3/write-parts.ts +131 -0
  13. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +9 -0
  14. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +2 -1
  15. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +29 -3
  16. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +83 -35
  17. package/src/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts +11 -0
  18. package/src/libs/common/domain/src/lib/model/record/flags.model.ts +1 -0
  19. package/src/libs/common/domain/src/lib/model/record/index.ts +1 -0
  20. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +14 -18
  21. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  22. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +4 -1
  23. package/src/libs/common/domain/src/lib/platform.service.interface.ts +1 -0
  24. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +20 -1
  25. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
  26. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +32 -19
  27. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints/form-field-constraints.component.ts +3 -5
  28. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-contacts/form-field-contacts.component.ts +2 -2
  29. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.css +0 -0
  30. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.html +18 -0
  31. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-record-flags/form-field-record-flag-component.ts +86 -0
  32. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.html +100 -88
  33. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.ts +11 -4
  34. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +18 -4
  35. package/src/libs/feature/editor/src/lib/expressions.ts +67 -0
  36. package/src/libs/feature/editor/src/lib/fields.config.ts +13 -0
  37. package/src/libs/feature/editor/src/lib/models/editor-config.model.ts +10 -4
  38. package/src/libs/feature/map/src/index.ts +0 -1
  39. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  40. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +3 -1
  41. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -1
  42. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +7 -1
  43. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +26 -4
  44. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +52 -8
  45. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +11 -1
  46. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +77 -3
  47. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +1 -0
  48. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  49. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +56 -4
  50. package/src/libs/feature/search/src/lib/utils/service/fields.ts +33 -9
  51. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +3 -3
  52. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  53. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  54. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +2 -2
  55. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +3 -3
  56. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  57. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +8 -2
  58. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +34 -5
  59. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  60. package/src/libs/util/app-config/src/lib/app-config.ts +34 -0
  61. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  62. package/src/libs/util/app-config/src/lib/model.ts +24 -1
  63. package/src/libs/util/shared/src/lib/utils/geojson.ts +16 -0
  64. package/translations/de.json +12 -1
  65. package/translations/en.json +14 -3
  66. package/translations/es.json +12 -1
  67. package/translations/fr.json +19 -8
  68. package/translations/it.json +12 -1
  69. package/translations/nl.json +12 -1
  70. package/translations/pt.json +12 -1
  71. package/translations/sk.json +12 -1
  72. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  73. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  74. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
@@ -12,13 +12,13 @@ import {
12
12
  import { MatTooltipModule } from '@angular/material/tooltip'
13
13
  import {
14
14
  AssociatedRecord,
15
- CatalogRecordKeys,
16
15
  Constraint,
17
16
  DatasetTemporalExtent,
18
17
  GraphicOverview,
19
18
  Individual,
20
19
  Keyword,
21
20
  OnlineResource,
21
+ RecordFlag,
22
22
  RecordKind,
23
23
  UpdateFrequency,
24
24
  } from '../../../../../../../../libs/common/domain/src/lib/model/record'
@@ -36,9 +36,10 @@ import {
36
36
  FormFieldTemporalExtentsComponent,
37
37
  } from '.'
38
38
  import {
39
- FieldModelSpecifier,
40
39
  FormFieldComponentName,
41
40
  FormFieldConfig,
41
+ FormFieldModel,
42
+ FormFieldModelSpecifier,
42
43
  } from '../../../models'
43
44
  import { FormFieldAssociatedRecordsComponent } from './form-field-associated-records/form-field-associated-records.component'
44
45
  import { FormFieldContactsForResourceComponent } from './form-field-contacts-for-resource/form-field-contacts-for-resource.component'
@@ -57,6 +58,7 @@ import { FormFieldConstraintsComponent } from './form-field-constraints/form-fie
57
58
  import { TextFieldModule } from '@angular/cdk/text-field'
58
59
  import { FormFieldSpatialToggleComponent } from './form-field-spatial-toggle/form-field-spatial-toggle.component'
59
60
  import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.component'
61
+ import { FormFieldRecordFlagComponent } from './form-field-record-flags/form-field-record-flag-component'
60
62
 
61
63
  @Component({
62
64
  selector: 'gn-ui-form-field',
@@ -90,6 +92,7 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
90
92
  FormFieldTopicsComponent,
91
93
  TextFieldModule,
92
94
  NgIconComponent,
95
+ FormFieldRecordFlagComponent,
93
96
  ],
94
97
  providers: [provideIcons({ matEditOutline, matHelpOutline })],
95
98
  hostDirectives: [FieldFocusDirective],
@@ -97,8 +100,8 @@ import { FormFieldTopicsComponent } from './form-field-topics/form-field-topics.
97
100
  export class FormFieldComponent {
98
101
  @Input() uniqueIdentifier: string
99
102
  @Input() recordKind: RecordKind
100
- @Input() model: CatalogRecordKeys
101
- @Input() modelSpecifier: FieldModelSpecifier
103
+ @Input() model: FormFieldModel
104
+ @Input() modelSpecifier: FormFieldModelSpecifier
102
105
  @Input() componentName: FormFieldComponentName
103
106
 
104
107
  @Input() config: FormFieldConfig
@@ -126,6 +129,7 @@ export class FormFieldComponent {
126
129
  this.model === 'legalConstraints' ||
127
130
  this.model === 'securityConstraints' ||
128
131
  this.model === 'otherConstraints' ||
132
+ this.model === 'flags' ||
129
133
  this.componentName === 'form-field-constraints-shortcuts'
130
134
  )
131
135
  }
@@ -172,6 +176,9 @@ export class FormFieldComponent {
172
176
  }>
173
177
  return identifiers?.[0]?.code || ''
174
178
  }
179
+ get recordFlag() {
180
+ return this.modelSpecifier?.replace(/^flags:/, '') as RecordFlag
181
+ }
175
182
 
176
183
  handleResourceIdentifierChange(code: string) {
177
184
  const identifiers = this.value as Array<{
@@ -11,7 +11,7 @@ import {
11
11
  viewChildren,
12
12
  } from '@angular/core'
13
13
  import { EditorFacade } from '../../+state/editor.facade'
14
- import { EditorFieldValue } from '../../models'
14
+ import { EditorFieldValue, FormFieldModel } from '../../models'
15
15
  import { FieldFocusDirective, FormFieldComponent } from './form-field'
16
16
  import { TranslateDirective } from '@ngx-translate/core'
17
17
  import {
@@ -24,6 +24,7 @@ import { switchMap } from 'rxjs/operators'
24
24
  import { toSignal } from '@angular/core/rxjs-interop'
25
25
  import { evaluate, isExpression } from '../../expressions'
26
26
  import { NotificationsService } from '../../../../../../../libs/feature/notifications/src'
27
+ import { PlatformServiceInterface } from '../../../../../../../libs/common/domain/src/lib/platform.service.interface'
27
28
 
28
29
  @Component({
29
30
  selector: 'gn-ui-record-form',
@@ -41,6 +42,7 @@ import { NotificationsService } from '../../../../../../../libs/feature/notifica
41
42
  export class RecordFormComponent implements OnInit, OnDestroy {
42
43
  facade = inject(EditorFacade)
43
44
  notifications = inject(NotificationsService)
45
+ platform = inject(PlatformServiceInterface)
44
46
  subscription = new Subscription()
45
47
 
46
48
  recordUniqueIdentifier$ = this.facade.record$.pipe(
@@ -90,8 +92,8 @@ export class RecordFormComponent implements OnInit, OnDestroy {
90
92
  this.subscription.unsubscribe()
91
93
  }
92
94
 
93
- handleFieldValueChange(model: CatalogRecordKeys, newValue: EditorFieldValue) {
94
- if (!model) {
95
+ handleFieldValueChange(model: FormFieldModel, newValue: EditorFieldValue) {
96
+ if (!model || model === 'flags') {
95
97
  return
96
98
  }
97
99
  this.facade.updateRecordField(model, newValue)
@@ -122,6 +124,8 @@ export class RecordFormComponent implements OnInit, OnDestroy {
122
124
  }
123
125
 
124
126
  private recordSignal = toSignal(this.facade.record$, { requireSync: true })
127
+ private userSignal = toSignal(this.platform.getMe())
128
+ private isAdminSignal = toSignal(this.platform.isAdministrator())
125
129
  private expressionCache = new Map<string, Signal<EditorFieldValue>>()
126
130
 
127
131
  // only compiles the expression into an evaluator the first time;
@@ -149,7 +153,17 @@ ${errors.map((err) => `> ${err}`).join('\n')}`)
149
153
  }
150
154
  this.expressionCache.set(
151
155
  expression,
152
- computed(() => evaluator({ globals: { record: this.recordSignal() } }))
156
+ computed(() =>
157
+ evaluator({
158
+ globals: {
159
+ record: this.recordSignal(),
160
+ user: {
161
+ ...this.userSignal(),
162
+ isAdmin: this.isAdminSignal(),
163
+ },
164
+ },
165
+ })
166
+ )
153
167
  )
154
168
  return this.expressionCache.get(expression)
155
169
  }
@@ -17,10 +17,14 @@ const TOKEN_OPERATOR_LOGICAL = ['==', '!=']
17
17
  const TOKEN_STRING_LITERAL = ["'", '"']
18
18
  const TOKEN_GROUP_START = '('
19
19
  const TOKEN_GROUP_END = ')'
20
+ const TOKEN_OPERATOR_BOOLEAN = ['&&', '||']
21
+ const TOKEN_OPERATOR_NOT = '!'
20
22
 
21
23
  // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence
22
24
  const OPERATORS_BY_PRECEDENCE: string[][] = [
23
25
  [TOKEN_OPERATOR_TERNARY_FIRST, TOKEN_OPERATOR_TERNARY_SECOND],
26
+ TOKEN_OPERATOR_BOOLEAN,
27
+ [TOKEN_OPERATOR_NOT],
24
28
  TOKEN_OPERATOR_LOGICAL,
25
29
  ]
26
30
  const ALL_OPERATORS = OPERATORS_BY_PRECEDENCE.flat()
@@ -161,6 +165,18 @@ function evaluateInternal(
161
165
  continue
162
166
  }
163
167
 
168
+ // BOOLEAN LITERAL
169
+ const isBooleanLiteral =
170
+ readNext(expression, pos, 4) === 'true' ||
171
+ readNext(expression, pos, 5) === 'false'
172
+ if (isBooleanLiteral) {
173
+ const literal = readNext(expression, pos, 4) === 'true' ? 'true' : 'false'
174
+ pos += literal.length
175
+ const booleanLiteral = literal === 'true'
176
+ currentEvaluator = () => booleanLiteral
177
+ continue
178
+ }
179
+
164
180
  // SYMBOL (NAME)
165
181
  const isSymbol = /[a-zA-Z]/.test(readNext(expression, pos))
166
182
  if (isSymbol) {
@@ -291,6 +307,57 @@ function evaluateInternal(
291
307
  continue
292
308
  }
293
309
 
310
+ // BOOLEAN OPERATORS
311
+ const isBooleanOperator = TOKEN_OPERATOR_BOOLEAN.some(
312
+ (token) => readNext(expression, pos, token.length) === token
313
+ )
314
+ if (isBooleanOperator) {
315
+ const matchingOperator = TOKEN_OPERATOR_BOOLEAN.find(
316
+ (token) => readNext(expression, pos, token.length) === token
317
+ )
318
+ const isAnd = matchingOperator === '&&'
319
+ pos += matchingOperator.length
320
+ const leftEvaluator = currentEvaluator
321
+ const rightExpression = readUntilNextOperator(
322
+ expression,
323
+ pos,
324
+ matchingOperator
325
+ )
326
+ pos += rightExpression.length
327
+ const rightEvaluator = evaluateInternal(
328
+ rightExpression,
329
+ currentContext
330
+ ).evaluator
331
+
332
+ currentEvaluator = (context) => {
333
+ return isAnd
334
+ ? Boolean(leftEvaluator(context) && rightEvaluator(context))
335
+ : Boolean(leftEvaluator(context) || rightEvaluator(context))
336
+ }
337
+ continue
338
+ }
339
+
340
+ // PREFIX OPERATORS
341
+ const isNotOperator = readNext(expression, pos) === TOKEN_OPERATOR_NOT
342
+ if (isNotOperator) {
343
+ pos++
344
+ const operandExpression = readUntilNextOperator(
345
+ expression,
346
+ pos,
347
+ TOKEN_OPERATOR_NOT
348
+ )
349
+ pos += operandExpression.length
350
+ const operandEvaluator = evaluateInternal(
351
+ operandExpression,
352
+ currentContext
353
+ ).evaluator
354
+
355
+ currentEvaluator = (context) => {
356
+ return !operandEvaluator(context)
357
+ }
358
+ continue
359
+ }
360
+
294
361
  // TERNARY OPERATOR
295
362
  const isTernary = readNext(expression, pos) === TOKEN_OPERATOR_TERNARY_FIRST
296
363
  if (isTernary) {
@@ -37,6 +37,19 @@ export const DEFAULT_CONFIGURATION: EditorConfig = {
37
37
  labelKey: marker('editor.record.form.field.title'),
38
38
  },
39
39
  },
40
+ {
41
+ model: 'flags',
42
+ modelSpecifier: 'flags:IS_REFERENCE_DATASET',
43
+ formFieldConfig: {
44
+ labelKey: marker(
45
+ 'editor.record.form.field.flags.IS_REFERENCE_DATASET'
46
+ ),
47
+ hintKey: marker(
48
+ 'editor.record.form.field.hint.flags.IS_REFERENCE_DATASET'
49
+ ),
50
+ },
51
+ hidden: '${record.kind != "dataset" || !user.isAdmin}',
52
+ },
40
53
  {
41
54
  model: 'abstract',
42
55
  formFieldConfig: {
@@ -1,4 +1,7 @@
1
- import { CatalogRecordKeys } from '../../../../../../libs/common/domain/src/lib/model/record'
1
+ import {
2
+ CatalogRecordKeys,
3
+ RecordFlag,
4
+ } from '../../../../../../libs/common/domain/src/lib/model/record'
2
5
 
3
6
  // Expressions should be enclosed in `${}` to be recognized as such
4
7
  // eg. ${dateNow()}
@@ -15,6 +18,8 @@ export interface FormFieldConfig {
15
18
  invalidHintKey?: string
16
19
  }
17
20
 
21
+ export type FormFieldModel = CatalogRecordKeys | 'flags'
22
+
18
23
  // Specifiers let us use specific components
19
24
  // This is used for instance to target only certain online resources in a field
20
25
  type OnlineLinkResourceSpecifier = `onlineResourceType:link`
@@ -25,11 +30,12 @@ type OnlineSingleLinkResourceSpecifier = `onlineResourceType:singleLink`
25
30
  // When set on the `contacts` field, contacts are rendered as editable detail
26
31
  // fields (ContactDetailsFormComponent) instead of cards (ContactCardComponent)
27
32
  type EditableContactDetailsSpecifier = `contact:editableDetails`
28
- export type FieldModelSpecifier =
33
+ export type FormFieldModelSpecifier =
29
34
  | OnlineLinkResourceSpecifier
30
35
  | DatasetDistributionsSpecifier
31
36
  | OnlineSingleLinkResourceSpecifier
32
37
  | EditableContactDetailsSpecifier
38
+ | `flags:${RecordFlag}`
33
39
 
34
40
  export type FormFieldComponentName =
35
41
  | 'form-field-constraints-shortcuts'
@@ -37,8 +43,8 @@ export type FormFieldComponentName =
37
43
 
38
44
  export interface EditorFieldIdentification {
39
45
  // name of the target field in the record; will not change the record directly if not defined
40
- model?: CatalogRecordKeys
41
- modelSpecifier?: FieldModelSpecifier
46
+ model?: FormFieldModel
47
+ modelSpecifier?: FormFieldModelSpecifier
42
48
 
43
49
  // if no model is given, a component can be shown instead
44
50
  componentName?: FormFieldComponentName
@@ -6,7 +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'
@@ -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
- getOptionalEditorConfig()?.NEW_RECORD_DEFAULT_LANGUAGE ??
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 }) => this.recordsRepository.getFeatureCatalog(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 &&
@@ -13,7 +13,13 @@
13
13
  (bboxChange)="onBboxChange($event)"
14
14
  (errorChange)="onSpatialExtentError($event)"
15
15
  [attr.data-cy-field]="fieldName"
16
- ></gn-ui-spatial-extent-dropdown>
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>
17
23
  } @else {
18
24
  <gn-ui-dropdown-multiselect
19
25
  class="w-full"
@@ -1,8 +1,10 @@
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 {
@@ -26,8 +28,20 @@ import { DateRange } from '../../../../../../libs/api/repository/src'
26
28
  import { CommonModule } from '@angular/common'
27
29
  import { BoundingBox } from '../../../../../../libs/util/shared/src'
28
30
  import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
29
- import { TranslateService } from '@ngx-translate/core'
30
- import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/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
+ )
31
45
 
32
46
  @Component({
33
47
  selector: 'gn-ui-filter-dropdown',
@@ -40,6 +54,8 @@ import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/
40
54
  DateRangeDropdownComponent,
41
55
  DropdownMultiselectComponent,
42
56
  SpatialExtentDropdownComponent,
57
+ LocationSearchComponent,
58
+ TranslatePipe,
43
59
  ],
44
60
  })
45
61
  export class FilterDropdownComponent implements OnInit {
@@ -52,8 +68,10 @@ export class FilterDropdownComponent implements OnInit {
52
68
  @Input() fieldName: string
53
69
  @Input() title: string
54
70
 
55
- spatialExtentMaxFileSize =
56
- getOptionalSearchConfig()?.SPATIAL_EXTENT_MAX_FILE_SIZE
71
+ @ViewChild(SpatialExtentDropdownComponent)
72
+ spatialExtentDropdown: SpatialExtentDropdownComponent
73
+
74
+ spatialExtentMaxFileSize = inject(SPATIAL_EXTENT_MAX_FILE_SIZE)
57
75
 
58
76
  fieldType: FieldType
59
77
  choices$: Observable<Choice[]>
@@ -96,6 +114,10 @@ export class FilterDropdownComponent implements OnInit {
96
114
  this.clearSpatialExtentErrorNotification()
97
115
  }
98
116
 
117
+ onLocationSelected({ bbox, label }: LocationBbox) {
118
+ this.spatialExtentDropdown.setLocationBbox(bbox, label)
119
+ }
120
+
99
121
  onSpatialExtentError(error: SpatialExtentDropdownError) {
100
122
  this.clearSpatialExtentErrorNotification()
101
123
  this.spatialExtentErrorNotificationId =
@@ -5,28 +5,60 @@ import {
5
5
  GeocodingResult,
6
6
  queryGeonames,
7
7
  GeonamesOptions,
8
- DataGouvFrOptions,
9
- queryDataGouvFr,
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 DataGouvFrGeocodingProvider = ['data-gouv-fr', DataGouvFrOptions]
16
+ type GeoplateformeGeocodingProvider = ['geoplateforme', GeoplateformeOptions]
17
17
  export type GeocodingProvider =
18
18
  | GeoadminGeocodingProvider
19
19
  | GeonamesGeocodingProvider
20
- | DataGouvFrGeocodingProvider
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
28
  'geocoding-provider',
24
29
  {
25
30
  providedIn: 'root',
26
- factory: () => ['geonames', { maxRows: 5 }] as GeocodingProvider,
31
+ factory: (): GeocodingProvider => DEFAULT_GEOCODING_PROVIDER,
27
32
  }
28
33
  )
29
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
+
30
62
  @Injectable({
31
63
  providedIn: 'root',
32
64
  })
@@ -38,7 +70,13 @@ export class GeocodingService {
38
70
  switch (this.provider[0]) {
39
71
  case 'geoadmin':
40
72
  queryObservable = from(
41
- queryGeoadmin(text, this.provider[1] as GeoadminOptions)
73
+ queryGeoadmin(
74
+ text,
75
+ wrapArrayOptions(
76
+ this.provider[1] as GeoadminOptions,
77
+ GEOCODING_PROVIDER_ARRAY_OPTIONS.geoadmin
78
+ )
79
+ )
42
80
  )
43
81
  break
44
82
  case 'geonames':
@@ -46,9 +84,15 @@ export class GeocodingService {
46
84
  queryGeonames(text, this.provider[1] as GeonamesOptions)
47
85
  )
48
86
  break
49
- case 'data-gouv-fr':
87
+ case 'geoplateforme':
50
88
  queryObservable = from(
51
- queryDataGouvFr(text, this.provider[1] as DataGouvFrOptions)
89
+ queryGeoplateforme(text, {
90
+ ...wrapArrayOptions(
91
+ this.provider[1] as GeoplateformeOptions,
92
+ GEOCODING_PROVIDER_ARRAY_OPTIONS.geoplateforme
93
+ ),
94
+ returnTrueGeometry: true,
95
+ })
52
96
  )
53
97
  break
54
98
  default:
@@ -2,7 +2,17 @@
2
2
  [action]="searchAction"
3
3
  [displayWithFn]="displayWithFn"
4
4
  [placeholder]="placeholder"
5
- [displayWithTemplate]="displayWithTemplate"
5
+ [displayWithTemplate]="defaultItemTpl"
6
+ [clearOnSelection]="true"
6
7
  (itemSelected)="handleItemSelected($event)"
7
8
  >
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>
@@ -3,18 +3,38 @@ import {
3
3
  Component,
4
4
  EventEmitter,
5
5
  inject,
6
+ InjectionToken,
6
7
  Input,
7
8
  Output,
8
- TemplateRef,
9
9
  } from '@angular/core'
10
10
  import { Observable } from 'rxjs'
11
+ import pointer from 'jsonpointer'
11
12
  import { GeocodingResult } from '@geospatial-sdk/geocoding'
13
+ import { Geometry } from 'geojson'
12
14
  import {
13
15
  AutocompleteComponent,
14
16
  AutocompleteItem,
15
17
  } from '../../../../../../libs/ui/inputs/src'
18
+ import { BoundingBox, getGeometryBoundingBox } from '../../../../../../libs/util/shared/src'
16
19
  import { GeocodingService } from '../geocoding/geocoding.service'
17
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
+
18
38
  @Component({
19
39
  selector: 'gn-ui-location-search',
20
40
  templateUrl: './location-search.component.html',
@@ -24,17 +44,71 @@ import { GeocodingService } from '../geocoding/geocoding.service'
24
44
  })
25
45
  export class LocationSearchComponent {
26
46
  private geocodingService = inject(GeocodingService)
47
+ private labels = inject(GEOCODING_PROVIDER_LABELS)
27
48
 
28
49
  @Input() placeholder = ''
29
- @Input() displayWithTemplate?: TemplateRef<{ $implicit: GeocodingResult }>
30
50
  @Output() resultSelected = new EventEmitter<GeocodingResult>()
51
+ @Output() bboxSelected = new EventEmitter<LocationBbox>()
31
52
 
32
53
  displayWithFn = (item: AutocompleteItem) => (item as GeocodingResult).label
33
54
 
34
55
  searchAction = (text: string): Observable<AutocompleteItem[]> =>
35
56
  this.geocodingService.query(text)
36
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
+
37
103
  handleItemSelected(item: AutocompleteItem) {
38
- this.resultSelected.emit(item as GeocodingResult)
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
+ }
39
113
  }
40
114
  }