geonetwork-ui 2.11.0-dev.aed5b2e5b → 2.11.0-dev.bc1a6d4e2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonetwork-ui",
3
- "version": "2.11.0-dev.aed5b2e5b",
3
+ "version": "2.11.0-dev.bc1a6d4e2",
4
4
  "engines": {
5
5
  "node": ">=24"
6
6
  },
@@ -28,6 +28,7 @@
28
28
  "@angular/core": "19.x || 20.x || 21.x",
29
29
  "@angular/forms": "19.x || 20.x || 21.x",
30
30
  "@angular/material": "19.x || 20.x || 21.x",
31
+ "@angular/material-date-fns-adapter": "19.x || 20.x",
31
32
  "@angular/platform-browser": "19.x || 20.x || 21.x",
32
33
  "@angular/platform-browser-dynamic": "19.x || 20.x || 21.x",
33
34
  "@angular/router": "19.x || 20.x || 21.x",
@@ -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 { getGeometryBoundingBox } from '../../../../../../../libs/util/shared/src'
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
 
@@ -264,8 +269,18 @@ export class ElasticsearchService {
264
269
  return this.metadataLang === 'current'
265
270
  }
266
271
 
267
- private filtersToQuery(
272
+ private findSpatialFilterExtent(
268
273
  filters: FieldFilters | FiltersAggregationParams | string
274
+ ): BoundingBox | undefined {
275
+ if (typeof filters === 'string') {
276
+ return undefined
277
+ }
278
+ return Object.values(filters).find(isBoundingBox)
279
+ }
280
+
281
+ private filtersToQuery(
282
+ filters: FieldFilters | FiltersAggregationParams | string,
283
+ spatialFilterExtent = this.findSpatialFilterExtent(filters)
269
284
  ): FilterQuery {
270
285
  const addQuote = (key: string) => (/^\/.+\/$/.test(key) ? key : `"${key}"`)
271
286
  const makeQuery = (filter: FieldFilter): string => {
@@ -286,7 +301,9 @@ export class ElasticsearchService {
286
301
  ? filters
287
302
  : Object.keys(filters)
288
303
  .filter((fieldname) => fieldname !== 'gn-ui-crossFieldFilter')
304
+ .filter((fieldname) => !isBoundingBox(filters[fieldname]))
289
305
  .filter((fieldname) => !isDateRange(filters[fieldname]))
306
+ .filter((fieldname) => !Array.isArray(filters[fieldname]))
290
307
  .filter(
291
308
  (fieldname) =>
292
309
  filters[fieldname] &&
@@ -330,6 +347,21 @@ export class ElasticsearchService {
330
347
  },
331
348
  },
332
349
  },
350
+ spatialFilterExtent && {
351
+ geo_shape: {
352
+ geom: {
353
+ shape: {
354
+ type: 'envelope',
355
+ // spatialFilterExtent is [minX, minY, maxX, maxY]; envelope coordinates are [top-left, bottom-right]
356
+ coordinates: [
357
+ [spatialFilterExtent[0], spatialFilterExtent[3]],
358
+ [spatialFilterExtent[2], spatialFilterExtent[1]],
359
+ ],
360
+ },
361
+ relation: 'intersects',
362
+ },
363
+ },
364
+ },
333
365
  ].filter(Boolean)
334
366
  return queryParts.length > 0 ? (queryParts as FilterQuery) : undefined
335
367
  }
@@ -368,7 +400,12 @@ export class ElasticsearchService {
368
400
  },
369
401
  })
370
402
  }
371
- const queryFilters = this.filtersToQuery(fieldSearchFilters)
403
+ // a spatial extent filter takes precedence over the preference geometry for boosting
404
+ const spatialFilterExtent = this.findSpatialFilterExtent(fieldSearchFilters)
405
+ const queryFilters = this.filtersToQuery(
406
+ fieldSearchFilters,
407
+ spatialFilterExtent
408
+ )
372
409
  if (queryFilters) {
373
410
  filter.push(...queryFilters)
374
411
  }
@@ -379,7 +416,10 @@ export class ElasticsearchService {
379
416
  },
380
417
  })
381
418
  }
382
- if (geometry) {
419
+ const boostGeometry = spatialFilterExtent
420
+ ? bboxToPolygon(spatialFilterExtent)
421
+ : geometry
422
+ if (boostGeometry) {
383
423
  // boosts applied using the filter geometry:
384
424
  // * records completely within the geometry receive a boost of 5
385
425
  // * records intersecting the geometry receive a boost of 2
@@ -389,7 +429,7 @@ export class ElasticsearchService {
389
429
  {
390
430
  geo_shape: {
391
431
  geom: {
392
- shape: geometry,
432
+ shape: boostGeometry,
393
433
  relation: 'within',
394
434
  },
395
435
  boost: 5.0,
@@ -398,7 +438,7 @@ export class ElasticsearchService {
398
438
  {
399
439
  geo_shape: {
400
440
  geom: {
401
- shape: geometry,
441
+ shape: boostGeometry,
402
442
  relation: 'intersects',
403
443
  },
404
444
  boost: 2.0,
@@ -409,7 +449,7 @@ export class ElasticsearchService {
409
449
  // this will boost the results variably depending on their distance from the given geometry
410
450
  // note: this takes into account the `location` field of a record; this is generally the center of all spatial extents
411
451
  // combined, and thus the actual size/coverage of the record spatial extent isn't relevant here
412
- const bbox = getGeometryBoundingBox(geometry)
452
+ const bbox = getGeometryBoundingBox(boostGeometry)
413
453
  const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
414
454
  const northToCenter = new LineString([
415
455
  [center[0], bbox[3]],
@@ -6,11 +6,15 @@ export type FieldFilterByRange = {
6
6
  start?: Date
7
7
  end?: Date
8
8
  }
9
+ // matches util/shared's BoundingBox; duplicated here to avoid a circular
10
+ // dependency (util/shared already depends on common/domain)
11
+ export type FieldFilterByBoundingBox = [number, number, number, number]
9
12
 
10
13
  export type FieldFilter =
11
14
  | FieldFilterByExpression
12
15
  | FieldFilterByValues
13
16
  | FieldFilterByRange
17
+ | FieldFilterByBoundingBox
14
18
  export type FieldFilters = Record<FieldName, FieldFilter>
15
19
 
16
20
  export type QueryString = {
@@ -19,4 +23,12 @@ export type QueryString = {
19
23
  export type QueryRange = {
20
24
  range: Record<string, { format: string; gte: string; lte: string }>
21
25
  }
22
- export type FilterQuery = Array<QueryString | QueryRange>
26
+ export type QueryGeoShape = {
27
+ geo_shape: {
28
+ geom: {
29
+ shape: { type: string; coordinates: number[][] }
30
+ relation: string
31
+ }
32
+ }
33
+ }
34
+ export type FilterQuery = Array<QueryString | QueryRange | QueryGeoShape>
@@ -7,9 +7,11 @@
7
7
  } @else if (fieldType === 'spatialExtent') {
8
8
  <gn-ui-spatial-extent-dropdown
9
9
  [title]="title"
10
+ [initialBbox]="(selectedBoundingBox$ | async) ?? null"
10
11
  [maxFileSizeMb]="spatialExtentMaxFileSize"
11
12
  (bboxChange)="onBboxChange($event)"
12
13
  (errorChange)="onSpatialExtentError($event)"
14
+ [attr.data-cy-field]="fieldName"
13
15
  ></gn-ui-spatial-extent-dropdown>
14
16
  } @else {
15
17
  <gn-ui-dropdown-multiselect
@@ -71,6 +71,14 @@ export class FilterDropdownComponent implements OnInit {
71
71
  map((selected) => (Array.isArray(selected) ? {} : (selected as DateRange)))
72
72
  ) as Observable<DateRange>
73
73
 
74
+ selectedBoundingBox$ = this.selected$.pipe(
75
+ map((selected) =>
76
+ Array.isArray(selected) && selected.length > 0
77
+ ? (selected as BoundingBox)
78
+ : null
79
+ )
80
+ ) as Observable<BoundingBox | null>
81
+
74
82
  onSelectedValues(values: unknown[]) {
75
83
  this.fieldsService
76
84
  .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] })
@@ -80,7 +88,11 @@ export class FilterDropdownComponent implements OnInit {
80
88
  private spatialExtentErrorNotificationId: number | null = null
81
89
 
82
90
  onBboxChange(bbox: BoundingBox | null) {
83
- console.log(bbox)
91
+ this.fieldsService
92
+ .buildFiltersFromFieldValues({
93
+ [this.fieldName]: bbox,
94
+ })
95
+ .subscribe((filters) => this.searchService.updateFilters(filters))
84
96
  this.clearSpatialExtentErrorNotification()
85
97
  }
86
98
 
@@ -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,
@@ -15,7 +16,6 @@ import {
15
16
  TranslatedSearchField,
16
17
  RecordKindField,
17
18
  UserSearchField,
18
- SpatialExtentSearchField,
19
19
  } from './fields'
20
20
  import { forkJoin, Observable, of } from 'rxjs'
21
21
  import { map } from 'rxjs/operators'
@@ -96,7 +96,7 @@ export class FieldsService {
96
96
  user: new UserSearchField(this.injector),
97
97
  changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'),
98
98
  availableServices: new AvailableServicesField(this.injector),
99
- spatialExtent: new SpatialExtentSearchField(this.injector),
99
+ spatialExtent: new BoundingBoxSearchField('spatialExtent', this.injector),
100
100
  } as Record<string, AbstractSearchField>
101
101
 
102
102
  get supportedFields() {
@@ -20,7 +20,11 @@ import {
20
20
  isDateRange,
21
21
  METADATA_LANGUAGE,
22
22
  } from '../../../../../../../libs/api/repository/src'
23
- import { formatUserInfo } from '../../../../../../../libs/util/shared/src'
23
+ import {
24
+ BoundingBox,
25
+ formatUserInfo,
26
+ isBoundingBox,
27
+ } from '../../../../../../../libs/util/shared/src'
24
28
  import { PossibleResourceTypes } from '../../../../../../../libs/api/metadata-converter/src'
25
29
 
26
30
  export type FieldType = 'values' | 'dateRange' | 'spatialExtent'
@@ -431,16 +435,22 @@ export class DateRangeSearchField extends SimpleSearchField {
431
435
  }
432
436
  }
433
437
 
434
- export class SpatialExtentSearchField extends SimpleSearchField {
435
- constructor(injector: Injector) {
436
- super('spatialExtent', injector, 'asc')
437
- }
438
-
438
+ export class BoundingBoxSearchField extends SimpleSearchField {
439
439
  getAvailableValues(): Observable<FieldAvailableValue[]> {
440
- // TODO: return an array of spatial extents to show which ones are available in the dropdown
441
440
  return of([])
442
441
  }
443
442
 
443
+ getFiltersForValues(values: FieldValue[]): Observable<FieldFilters> {
444
+ return of({
445
+ [this.esFieldName]: values.map(Number) as BoundingBox,
446
+ })
447
+ }
448
+
449
+ getValuesForFilter(filters: FieldFilters): Observable<FieldValue[]> {
450
+ const filter = filters[this.esFieldName]
451
+ return of(isBoundingBox(filter) ? filter : [])
452
+ }
453
+
444
454
  getType(): FieldType {
445
455
  return 'spatialExtent'
446
456
  }
@@ -79,11 +79,7 @@
79
79
  @if (hasSelection) {
80
80
  <gn-ui-button
81
81
  [type]="'primary-light'"
82
- [title]="
83
- ('search.filters.spatialExtent.bboxDelete' | translate) +
84
- ' ' +
85
- fileName
86
- "
82
+ [title]="selectionDeleteLabelKey | translate: selectionLabelParams"
87
83
  (buttonClick)="removeSelection($event)"
88
84
  extraClass="group gap-x-2 w-full h-[28px] px-[8px] py-[4px] mb-[12px] bg-primary-lighter text-white"
89
85
  data-test="spatial-extent-selected-item"
@@ -94,12 +90,10 @@
94
90
  size="15px"
95
91
  ></ng-icon>
96
92
  <span class="group-hover:hidden truncate text-sm m-auto">
97
- {{ 'search.filters.spatialExtent.bboxPrefix' | translate }}
98
- {{ fileName }}
93
+ {{ selectionLabelKey | translate: selectionLabelParams }}
99
94
  </span>
100
95
  <span class="hidden group-hover:block truncate text-sm m-auto">
101
- {{ 'search.filters.spatialExtent.bboxDelete' | translate }}
102
- {{ fileName }}
96
+ {{ selectionDeleteLabelKey | translate: selectionLabelParams }}
103
97
  </span>
104
98
  <ng-icon
105
99
  name="iconoirCheckCircle"
@@ -46,13 +46,20 @@ import {
46
46
  marker('search.filters.spatialExtent.import')
47
47
  marker('search.filters.spatialExtent.helpText')
48
48
  marker('search.filters.spatialExtent.error.title')
49
- marker('search.filters.spatialExtent.bboxPrefix')
49
+
50
+ const LABEL_FROM_FILE = marker('search.filters.spatialExtent.bboxFromFile')
51
+ const LABEL_FROM_FILE_DELETE = marker(
52
+ 'search.filters.spatialExtent.bboxFromFileDelete'
53
+ )
54
+ const LABEL_INITIAL = marker('search.filters.spatialExtent.bboxInitial')
55
+ const LABEL_INITIAL_DELETE = marker(
56
+ 'search.filters.spatialExtent.bboxInitialDelete'
57
+ )
50
58
 
51
59
  export interface SpatialExtentDropdownError {
52
60
  key: string
53
61
  params?: Record<string, string | number>
54
62
  }
55
- marker('search.filters.spatialExtent.bboxDelete')
56
63
 
57
64
  @Component({
58
65
  selector: 'gn-ui-spatial-extent-dropdown',
@@ -85,6 +92,11 @@ export class SpatialExtentDropdownComponent {
85
92
 
86
93
  @Input() title: string
87
94
  @Input() maxFileSizeMb: number | null = null
95
+ @Input() set initialBbox(value: BoundingBox | null) {
96
+ if (!this.bbox && value) {
97
+ this.bbox = value
98
+ }
99
+ }
88
100
 
89
101
  @Output() bboxChange = new EventEmitter<BoundingBox | null>()
90
102
  @Output() errorChange = new EventEmitter<SpatialExtentDropdownError>()
@@ -123,6 +135,18 @@ export class SpatialExtentDropdownComponent {
123
135
  return !!this.bbox
124
136
  }
125
137
 
138
+ get selectionLabelKey() {
139
+ return this.fileName ? LABEL_FROM_FILE : LABEL_INITIAL
140
+ }
141
+
142
+ get selectionDeleteLabelKey() {
143
+ return this.fileName ? LABEL_FROM_FILE_DELETE : LABEL_INITIAL_DELETE
144
+ }
145
+
146
+ get selectionLabelParams() {
147
+ return { fileName: this.fileName }
148
+ }
149
+
126
150
  openOverlay() {
127
151
  this.overlayMinWidth =
128
152
  this.overlayOrigin.elementRef.nativeElement.getBoundingClientRect()
@@ -58,6 +58,8 @@ import { transformExtent } from 'ol/proj.js'
58
58
  export const DEFAULT_BASEMAP_LAYER: MapContextLayerMapLibreStyle = {
59
59
  type: 'maplibre-style',
60
60
  styleUrl: `https://basemaps.cartocdn.com/gl/positron-gl-style/style.json`,
61
+ clickable: false,
62
+ hoverable: false,
61
63
  }
62
64
 
63
65
  const DEFAULT_VIEW: MapContextView = {
@@ -38,6 +38,14 @@ export function getGeometryFromGeoJSON(
38
38
  // FIXME: this type should be more generic across the project
39
39
  export type BoundingBox = [number, number, number, number]
40
40
 
41
+ export function isBoundingBox(value: unknown): value is BoundingBox {
42
+ return (
43
+ Array.isArray(value) &&
44
+ value.length === 4 &&
45
+ value.every((item) => typeof item === 'number')
46
+ )
47
+ }
48
+
41
49
  export function getGeometryBoundingBox(geometry: Geometry): BoundingBox {
42
50
  // use the bounding box if specified in the GeoJSON object
43
51
  if (geometry.bbox) {
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "Repräsentationstyp",
693
693
  "search.filters.resourceType": "Ressourcentyp",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "Representation type",
693
693
  "search.filters.resourceType": "Resource type",
694
694
  "search.filters.spatialExtent": "Spatial extent",
695
- "search.filters.spatialExtent.bboxDelete": "Delete",
696
- "search.filters.spatialExtent.bboxPrefix": "bbox from",
695
+ "search.filters.spatialExtent.bboxFromFile": "extent from {fileName}",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "Delete {fileName}",
697
+ "search.filters.spatialExtent.bboxInitial": "Pre-selected extent",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "Delete selection",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "The file exceeds the maximum allowed size ({maxSize} MB)",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "The file could not be read as a valid GeoJSON",
699
701
  "search.filters.spatialExtent.error.noGeometry": "No geometry could be found in the file",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "",
693
693
  "search.filters.resourceType": "",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "Type de représentation",
693
693
  "search.filters.resourceType": "Type de ressource",
694
694
  "search.filters.spatialExtent": "Étendue spatiale",
695
- "search.filters.spatialExtent.bboxDelete": "Supprimer",
696
- "search.filters.spatialExtent.bboxPrefix": "bbox de",
695
+ "search.filters.spatialExtent.bboxFromFile": "étendue de {fileName}",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "Supprimer {fileName}",
697
+ "search.filters.spatialExtent.bboxInitial": "étendue présélectionnée",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "Supprimer la sélection",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "Le fichier dépasse la taille maximale autorisée ({maxSize} Mo)",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "Le fichier n'a pas pu être lu comme un GeoJSON valide",
699
701
  "search.filters.spatialExtent.error.noGeometry": "Aucune géométrie n'a pu être trouvée dans le fichier",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "Tipo di rappresentazione",
693
693
  "search.filters.resourceType": "Tipo di risorsa",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "",
693
693
  "search.filters.resourceType": "",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "",
693
693
  "search.filters.resourceType": "",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",
@@ -692,8 +692,10 @@
692
692
  "search.filters.representationType": "Typ reprezentácie",
693
693
  "search.filters.resourceType": "Typ zdroja",
694
694
  "search.filters.spatialExtent": "",
695
- "search.filters.spatialExtent.bboxDelete": "",
696
- "search.filters.spatialExtent.bboxPrefix": "",
695
+ "search.filters.spatialExtent.bboxFromFile": "",
696
+ "search.filters.spatialExtent.bboxFromFileDelete": "",
697
+ "search.filters.spatialExtent.bboxInitial": "",
698
+ "search.filters.spatialExtent.bboxInitialDelete": "",
697
699
  "search.filters.spatialExtent.error.fileTooLarge": "",
698
700
  "search.filters.spatialExtent.error.invalidFormat": "",
699
701
  "search.filters.spatialExtent.error.noGeometry": "",