geonetwork-ui 2.10.0-dev.db7545cdc → 2.10.0-dev.dc20f8516

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 (32) hide show
  1. package/fesm2022/geonetwork-ui.mjs +369 -269
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +60 -16
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +0 -5
  7. package/src/libs/feature/editor/src/lib/components/record-form/form-field/field-focus.directive.ts +4 -1
  8. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-constraints-shortcuts/form-field-constraints-shortcuts.component.ts +6 -7
  9. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field-topics/form-field-topics.component.ts +2 -2
  10. package/src/libs/feature/editor/src/lib/components/record-form/form-field/form-field.component.css +0 -34
  11. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.css +35 -0
  12. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.html +5 -1
  13. package/src/libs/feature/editor/src/lib/components/record-form/record-form.component.ts +35 -19
  14. package/src/libs/feature/editor/src/lib/fields.config.ts +60 -47
  15. package/src/libs/feature/map/src/lib/utils/map-utils.service.ts +33 -0
  16. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +9 -1
  17. package/src/libs/ui/elements/src/lib/contact-details/contact-details.component.html +1 -1
  18. package/src/libs/ui/elements/src/lib/contact-pill/contact-pill.component.html +7 -3
  19. package/src/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +1 -1
  20. package/src/libs/ui/inputs/src/lib/button/button.component.ts +4 -0
  21. package/src/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.ts +4 -54
  22. package/src/libs/ui/map/src/lib/map-utils.ts +48 -0
  23. package/src/libs/util/shared/src/lib/utils/geojson.ts +58 -1
  24. package/tailwind.base.css +6 -0
  25. package/translations/de.json +19 -19
  26. package/translations/en.json +19 -19
  27. package/translations/es.json +19 -19
  28. package/translations/fr.json +19 -19
  29. package/translations/it.json +19 -19
  30. package/translations/nl.json +19 -19
  31. package/translations/pt.json +19 -19
  32. package/translations/sk.json +19 -19
@@ -9,7 +9,7 @@ import {
9
9
  } from '@angular/core'
10
10
  import { EditorFacade } from '../../+state/editor.facade'
11
11
  import { EditorFieldValue } from '../../models'
12
- import { FormFieldComponent } from './form-field'
12
+ import { FieldFocusDirective, FormFieldComponent } from './form-field'
13
13
  import { TranslateDirective } from '@ngx-translate/core'
14
14
  import {
15
15
  EditorFieldWithValue,
@@ -25,7 +25,12 @@ import { switchMap } from 'rxjs/operators'
25
25
  styleUrls: ['./record-form.component.css'],
26
26
  changeDetection: ChangeDetectionStrategy.OnPush,
27
27
  standalone: true,
28
- imports: [CommonModule, FormFieldComponent, TranslateDirective],
28
+ imports: [
29
+ CommonModule,
30
+ FormFieldComponent,
31
+ FieldFocusDirective,
32
+ TranslateDirective,
33
+ ],
29
34
  })
30
35
  export class RecordFormComponent implements OnInit, OnDestroy {
31
36
  facade = inject(EditorFacade)
@@ -36,17 +41,22 @@ export class RecordFormComponent implements OnInit, OnDestroy {
36
41
  )
37
42
 
38
43
  focusFieldWithPage$ = this.facade.focusedField$.pipe(
39
- switchMap(
40
- async (field) => [field, await this.getPageIndexForField(field)] as const
41
- )
44
+ switchMap(async (field) => {
45
+ const location = await this.getFieldLocation(field)
46
+ return [field, location?.page ?? null, location?.section ?? -1] as const
47
+ })
42
48
  )
43
49
 
44
50
  formFields = viewChildren(FormFieldComponent)
51
+ sectionFocusDirectives = viewChildren<FieldFocusDirective>('sectionFocus')
45
52
 
46
- focusField(model: CatalogRecordKeys) {
47
- const fields = this.formFields()
48
- const field = fields.find((f) => f.model === model)
49
- field?.fieldFocus.focusField()
53
+ focusField(model: CatalogRecordKeys, sectionIndex: number) {
54
+ const field = this.formFields().find((f) => f.model === model)
55
+ if (field) {
56
+ field.fieldFocus.focusField()
57
+ return
58
+ }
59
+ this.sectionFocusDirectives()[sectionIndex]?.focusField(false)
50
60
  }
51
61
 
52
62
  ngOnInit() {
@@ -55,15 +65,15 @@ export class RecordFormComponent implements OnInit, OnDestroy {
55
65
  .pipe(
56
66
  withLatestFrom(
57
67
  this.facade.currentPage$,
58
- ([field, fieldPage], currentPage) =>
59
- [field, fieldPage, currentPage] as const
68
+ ([field, fieldPage, fieldSection], currentPage) =>
69
+ [field, fieldPage, fieldSection, currentPage] as const
60
70
  )
61
71
  )
62
- .subscribe(([field, fieldPage, currentPage]) => {
72
+ .subscribe(([field, fieldPage, fieldSection, currentPage]) => {
63
73
  if (fieldPage !== null && fieldPage !== currentPage) {
64
74
  this.facade.setCurrentPage(fieldPage)
65
75
  }
66
- setTimeout(() => this.focusField(field))
76
+ setTimeout(() => this.focusField(field, fieldSection))
67
77
  })
68
78
  )
69
79
  }
@@ -87,13 +97,19 @@ export class RecordFormComponent implements OnInit, OnDestroy {
87
97
  return section.labelKey
88
98
  }
89
99
 
90
- async getPageIndexForField(model: CatalogRecordKeys): Promise<number | null> {
100
+ async getFieldLocation(
101
+ model: CatalogRecordKeys
102
+ ): Promise<{ page: number; section: number } | null> {
91
103
  const config = await firstValueFrom(this.facade.editorConfig$)
92
- const pageIndex = config.pages.findIndex((page) =>
93
- page.sections.some((section) =>
94
- section.fields.some((field) => field.model === model)
95
- )
104
+ const page = config.pages.findIndex((p) =>
105
+ p.sections.some((s) => s.fields.some((f) => f.model === model))
96
106
  )
97
- return pageIndex >= 0 ? pageIndex : null
107
+ if (page < 0) {
108
+ return null
109
+ }
110
+ const section = config.pages[page].sections
111
+ .filter((s) => !s.hidden)
112
+ .findIndex((s) => s.fields.some((f) => f.model === model))
113
+ return { page, section }
98
114
  }
99
115
  }
@@ -4,10 +4,12 @@ import {
4
4
  EditorField,
5
5
  EditorSection,
6
6
  } from './models/editor-config.model'
7
- import {
8
- INSPIRE_topic,
9
- Keyword,
10
- } from '../../../../../libs/common/domain/src/lib/model/record'
7
+ import { Keyword } from '../../../../../libs/common/domain/src/lib/model/record'
8
+
9
+ export interface ISOTopic {
10
+ value: string
11
+ label: string
12
+ }
11
13
 
12
14
  /**
13
15
  * This file contains the configuration of the fields that will be displayed in the editor.
@@ -359,75 +361,86 @@ export const SPATIAL_SCOPES: Keyword[] = [
359
361
  ]
360
362
 
361
363
  /************************************************************
362
- *************** INSPIRE TOPICS **************
364
+ *************** ISO TOPICS **************
363
365
  ************************************************************
364
366
  */
365
367
 
366
- export const INSPIRE_TOPICS: INSPIRE_topic[] = [
367
- { value: 'biota', label: 'editor.record.form.topics.inspire.biota' },
368
+ // label keys mirror the ISO MD_TopicCategoryCode value for consistency
369
+ // TODO: correctly handle code lists; for instance, this code list is specific to ISO 19139
370
+ export const ISO_TOPICS: ISOTopic[] = [
371
+ { value: 'biota', label: marker('editor.record.form.topics.iso.biota') },
368
372
  {
369
373
  value: 'boundaries',
370
- label: 'editor.record.form.topics.inspire.boundaries',
374
+ label: marker('editor.record.form.topics.iso.boundaries'),
371
375
  },
372
376
  {
373
377
  value: 'climatologyMeteorologyAtmosphere',
374
- label: 'editor.record.form.topics.inspire.climatology',
378
+ label: marker(
379
+ 'editor.record.form.topics.iso.climatologyMeteorologyAtmosphere'
380
+ ),
381
+ },
382
+ {
383
+ value: 'economy',
384
+ label: marker('editor.record.form.topics.iso.economy'),
385
+ },
386
+ {
387
+ value: 'elevation',
388
+ label: marker('editor.record.form.topics.iso.elevation'),
375
389
  },
376
- { value: 'economy', label: 'editor.record.form.topics.inspire.economy' },
377
- { value: 'elevation', label: 'editor.record.form.topics.inspire.elevation' },
378
390
  {
379
391
  value: 'environment',
380
- label: 'editor.record.form.topics.inspire.environnement',
392
+ label: marker('editor.record.form.topics.iso.environment'),
393
+ },
394
+ {
395
+ value: 'farming',
396
+ label: marker('editor.record.form.topics.iso.farming'),
397
+ },
398
+ {
399
+ value: 'geoscientificInformation',
400
+ label: marker('editor.record.form.topics.iso.geoscientificInformation'),
381
401
  },
382
- { value: 'farming', label: 'editor.record.form.topics.inspire.farming' },
383
402
  {
384
- value: 'geoscientific information',
385
- label: 'editor.record.form.topics.inspire.geoscientific',
403
+ value: 'health',
404
+ label: marker('editor.record.form.topics.iso.health'),
386
405
  },
387
- { value: 'health', label: 'editor.record.form.topics.inspire.health' },
388
406
  {
389
407
  value: 'imageryBaseMapsEarthCover',
390
- label: 'editor.record.form.topics.inspire.imagery',
408
+ label: marker('editor.record.form.topics.iso.imageryBaseMapsEarthCover'),
409
+ },
410
+ {
411
+ value: 'inlandWaters',
412
+ label: marker('editor.record.form.topics.iso.inlandWaters'),
391
413
  },
392
- { value: 'inlandWaters', label: 'editor.record.form.topics.inspire.waters' },
393
414
  {
394
415
  value: 'intelligenceMilitary',
395
- label: 'editor.record.form.topics.inspire.intelligence',
416
+ label: marker('editor.record.form.topics.iso.intelligenceMilitary'),
417
+ },
418
+ {
419
+ value: 'location',
420
+ label: marker('editor.record.form.topics.iso.location'),
421
+ },
422
+ {
423
+ value: 'oceans',
424
+ label: marker('editor.record.form.topics.iso.oceans'),
396
425
  },
397
- { value: 'Location', label: 'editor.record.form.topics.inspire.location' },
398
- { value: 'Oceans', label: 'editor.record.form.topics.inspire.oceans' },
399
426
  {
400
427
  value: 'planningCadastre',
401
- label: 'editor.record.form.topics.inspire.planning',
428
+ label: marker('editor.record.form.topics.iso.planningCadastre'),
402
429
  },
403
- { value: 'Society', label: 'editor.record.form.topics.inspire.society' },
404
- { value: 'Structure', label: 'editor.record.form.topics.inspire.structure' },
405
430
  {
406
- value: 'Transportation',
407
- label: 'editor.record.form.topics.inspire.transportation',
431
+ value: 'society',
432
+ label: marker('editor.record.form.topics.iso.society'),
433
+ },
434
+ {
435
+ value: 'structure',
436
+ label: marker('editor.record.form.topics.iso.structure'),
437
+ },
438
+ {
439
+ value: 'transportation',
440
+ label: marker('editor.record.form.topics.iso.transportation'),
408
441
  },
409
442
  {
410
443
  value: 'utilitiesCommunication',
411
- label: 'editor.record.form.topics.inspire.utilities',
444
+ label: marker('editor.record.form.topics.iso.utilitiesCommunication'),
412
445
  },
413
446
  ]
414
-
415
- marker('editor.record.form.topics.inspire.biota')
416
- marker('editor.record.form.topics.inspire.boundaries')
417
- marker('editor.record.form.topics.inspire.climatology')
418
- marker('editor.record.form.topics.inspire.economy')
419
- marker('editor.record.form.topics.inspire.elevation')
420
- marker('editor.record.form.topics.inspire.environnement')
421
- marker('editor.record.form.topics.inspire.farming')
422
- marker('editor.record.form.topics.inspire.geoscientific')
423
- marker('editor.record.form.topics.inspire.health')
424
- marker('editor.record.form.topics.inspire.imagery')
425
- marker('editor.record.form.topics.inspire.intelligence')
426
- marker('editor.record.form.topics.inspire.location')
427
- marker('editor.record.form.topics.inspire.oceans')
428
- marker('editor.record.form.topics.inspire.planning')
429
- marker('editor.record.form.topics.inspire.society')
430
- marker('editor.record.form.topics.inspire.structure')
431
- marker('editor.record.form.topics.inspire.transportation')
432
- marker('editor.record.form.topics.inspire.utilities')
433
- marker('editor.record.form.topics.inspire.waters')
@@ -2,6 +2,23 @@ import { Injectable } from '@angular/core'
2
2
  import { extend } from 'ol/extent.js'
3
3
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
4
4
  import { BoundingBox, getGeometryBoundingBox } from '../../../../../../libs/util/shared/src'
5
+ import { MapContextLayer } from '@geospatial-sdk/core'
6
+ import {
7
+ createSpatialExtentLayer,
8
+ SpatialExtentLayerStyle,
9
+ } from '../../../../../../libs/ui/map/src'
10
+
11
+ /**
12
+ * Style of the extent overlay drawn on top of the previewed data: a dashed
13
+ * black outline over a very light fill, so the extent stays readable without
14
+ * hiding the data underneath.
15
+ */
16
+ const RECORD_EXTENT_OVERLAY_STYLE: SpatialExtentLayerStyle = {
17
+ 'stroke-color': 'rgba(0, 0, 0, 0.6)',
18
+ 'stroke-width': 2,
19
+ 'stroke-line-dash': [8, 6],
20
+ 'fill-color': 'rgba(0, 0, 0, 0.03)',
21
+ }
5
22
 
6
23
  @Injectable({
7
24
  providedIn: 'root',
@@ -26,4 +43,20 @@ export class MapUtilsService {
26
43
  [Infinity, Infinity, -Infinity, -Infinity]
27
44
  )
28
45
  }
46
+
47
+ /**
48
+ * Builds a non-interactive overlay layer drawing the spatial extent(s)
49
+ * declared in the record's metadata (bounding boxes and/or geometries).
50
+ * Returns null when the record has no usable spatial extent.
51
+ *
52
+ * This is purely for display and is independent from the map's initial view,
53
+ * which is derived separately (see {@link getRecordExtent}).
54
+ */
55
+ getRecordExtentLayer(record: Partial<CatalogRecord>): MapContextLayer | null {
56
+ return createSpatialExtentLayer(record.spatialExtents ?? [], {
57
+ label: 'Spatial extent',
58
+ clickable: false,
59
+ style: RECORD_EXTENT_OVERLAY_STYLE,
60
+ })
61
+ }
29
62
  }
@@ -431,11 +431,19 @@ export class MapViewComponent implements AfterViewInit {
431
431
  }),
432
432
  withLatestFrom(this.mdViewFacade.metadata$),
433
433
  map(([context, metadata]) => {
434
- if (context.view) return context
434
+ // overlay the record's declared spatial extent on top of the data layers
435
+ const extentLayer = this.mapUtils.getRecordExtentLayer(metadata)
436
+ const layers = extentLayer
437
+ ? [...context.layers, extentLayer]
438
+ : context.layers
439
+ // the view (initial zoom) is derived from the data layer or, as a
440
+ // fallback, from the record extent — independently from the overlay above
441
+ if (context.view) return { ...context, layers }
435
442
  const extent = this.mapUtils.getRecordExtent(metadata)
436
443
  const view = extent ? { extent } : null
437
444
  return {
438
445
  ...context,
446
+ layers,
439
447
  view,
440
448
  }
441
449
  }),
@@ -1,5 +1,5 @@
1
1
  <div
2
- class="bg-gray-50 rounded border border-gray-200 shadow-md p-4 flex flex-col gap-3 w-full"
2
+ class="bg-gray-50 text-black rounded border border-gray-200 shadow-md p-4 flex flex-col gap-3 w-full"
3
3
  data-test="contact-details"
4
4
  >
5
5
  @if (displayName) {
@@ -1,5 +1,5 @@
1
1
  <gn-ui-button
2
- [type]="overlayOpen ? 'primary' : 'primary-light'"
2
+ [type]="overlayOpen ? 'gray-light' : 'primary-light'"
3
3
  extraClass="group w-full min-h-12 gap-3 justify-between py-2 pl-5 pr-4 rounded"
4
4
  data-test="contact-pill"
5
5
  (buttonClick)="toggleOverlay()"
@@ -7,12 +7,16 @@
7
7
  #overlayOrigin="cdkOverlayOrigin"
8
8
  >
9
9
  <span
10
- class="font-title font-medium text-base leading-tight truncate group-hover:text-white"
10
+ class="font-title font-medium text-base leading-tight truncate"
11
+ [class]="!overlayOpen ? 'text-primary-black group-hover:text-white' : ''"
11
12
  [title]="displayName"
12
13
  >{{ displayName }}</span
13
14
  >
14
15
  <div
15
- class="gn-ui-card-icon items-center justify-center w-10 h-8 group-hover:border-white group-hover:text-white"
16
+ class="gn-ui-card-icon items-center justify-center w-10 h-8"
17
+ [class]="
18
+ !overlayOpen ? 'group-hover:border-white group-hover:text-white' : ''
19
+ "
16
20
  >
17
21
  @if (overlayOpen) {
18
22
  <ng-icon class="!w-6 !h-6 !text-[24px]" name="matClose"></ng-icon>
@@ -113,7 +113,7 @@
113
113
  >
114
114
  <div class="flex flex-col gap-1 pt-3 pb-4">
115
115
  @for (group of contactGroups; track group.role) {
116
- <div class="flex flex-col gap-1 rounded bg-gray-50 py-4 px-2">
116
+ <div class="flex flex-col gap-1 rounded py-4 px-2">
117
117
  <p class="text-xs font-normal text-black">
118
118
  {{ group.roleLabel | translate }}
119
119
  </p>
@@ -25,6 +25,7 @@ export class ButtonComponent {
25
25
  | 'outline'
26
26
  | 'light'
27
27
  | 'gray'
28
+ | 'gray-light'
28
29
  | 'black'
29
30
  | 'primary-light'
30
31
  ) {
@@ -45,6 +46,9 @@ export class ButtonComponent {
45
46
  case 'gray':
46
47
  this.btnClass = 'gn-ui-btn-gray'
47
48
  break
49
+ case 'gray-light':
50
+ this.btnClass = 'gn-ui-btn-gray-light'
51
+ break
48
52
  case 'black':
49
53
  this.btnClass = 'gn-ui-btn-black'
50
54
  break
@@ -1,18 +1,11 @@
1
1
  import { ChangeDetectorRef, Component, inject, Input } from '@angular/core'
2
2
  import { CommonModule } from '@angular/common'
3
- import { Geometry } from 'geojson'
4
- import { GeoJSONFeatureCollection } from 'ol/format/GeoJSON.js'
5
- import GeoJSON from 'ol/format/GeoJSON.js'
6
- import { Polygon } from 'ol/geom.js'
7
- import {
8
- createViewFromLayer,
9
- MapContext,
10
- MapContextLayer,
11
- } from '@geospatial-sdk/core'
3
+ import { createViewFromLayer, MapContext } from '@geospatial-sdk/core'
12
4
  import { MapContainerComponent } from '../map-container/map-container.component'
13
5
  import { BehaviorSubject, from, Observable, of } from 'rxjs'
14
6
  import { map, switchMap, tap } from 'rxjs/operators'
15
7
  import { DatasetSpatialExtent } from '../../../../../../../libs/common/domain/src/lib/model/record'
8
+ import { createSpatialExtentLayer } from '../../map-utils'
16
9
 
17
10
  @Component({
18
11
  selector: 'gn-ui-spatial-extent',
@@ -30,39 +23,10 @@ export class SpatialExtentComponent {
30
23
  spatialExtents$ = new BehaviorSubject<DatasetSpatialExtent[]>([])
31
24
  mapContext$: Observable<MapContext> = this.spatialExtents$.pipe(
32
25
  switchMap((extents) => {
33
- if (extents.length === 0) {
26
+ const layer = createSpatialExtentLayer(extents)
27
+ if (!layer) {
34
28
  return of(null)
35
29
  }
36
- const featureCollection: GeoJSONFeatureCollection = {
37
- type: 'FeatureCollection',
38
- features: [],
39
- }
40
- extents.forEach((extent) => {
41
- if (extent.geometry) {
42
- featureCollection.features.push({
43
- type: 'Feature',
44
- properties: {},
45
- geometry: extent.geometry,
46
- })
47
- } else if (extent.bbox?.length >= 0) {
48
- featureCollection.features.push({
49
- type: 'Feature',
50
- properties: {},
51
- geometry: this.bboxCoordsToGeometry(extent.bbox),
52
- })
53
- }
54
- })
55
-
56
- const layer: MapContextLayer = {
57
- type: 'geojson',
58
- data: featureCollection,
59
- label: 'Spatial extents',
60
- style: {
61
- 'stroke-color': 'black',
62
- 'stroke-width': 2,
63
- 'fill-color': 'rgba(153, 153, 153, 0.3)',
64
- },
65
- }
66
30
  return from(createViewFromLayer(layer)).pipe(
67
31
  map((view) => ({ view, layers: [layer] }) as MapContext),
68
32
  tap(() => this._cdr.markForCheck())
@@ -71,18 +35,4 @@ export class SpatialExtentComponent {
71
35
  )
72
36
 
73
37
  error = ''
74
-
75
- bboxCoordsToGeometry(bbox: [number, number, number, number]): Geometry {
76
- const geometry = new Polygon([
77
- [
78
- [bbox[0], bbox[1]],
79
- [bbox[0], bbox[3]],
80
- [bbox[2], bbox[3]],
81
- [bbox[2], bbox[1]],
82
- [bbox[0], bbox[1]],
83
- ],
84
- ])
85
-
86
- return new GeoJSON().writeGeometryObject(geometry)
87
- }
88
38
  }
@@ -12,6 +12,54 @@ import {
12
12
  MouseWheelZoom,
13
13
  } from 'ol/interaction.js'
14
14
  import MapBrowserEvent from 'ol/MapBrowserEvent.js'
15
+ import type { DatasetSpatialExtent } from '../../../../../libs/common/domain/src/lib/model/record'
16
+ import type {
17
+ MapContextLayer,
18
+ MapContextLayerGeojson,
19
+ } from '@geospatial-sdk/core'
20
+ import { spatialExtentsToFeatureCollection } from '../../../../../libs/util/shared/src'
21
+
22
+ export type SpatialExtentLayerStyle = NonNullable<
23
+ MapContextLayerGeojson['style']
24
+ >
25
+
26
+ /**
27
+ * Default style for a spatial-extent layer: a solid black outline over a
28
+ * translucent grey fill.
29
+ */
30
+ export const DEFAULT_SPATIAL_EXTENT_STYLE: SpatialExtentLayerStyle = {
31
+ 'stroke-color': 'black',
32
+ 'stroke-width': 2,
33
+ 'fill-color': 'rgba(153, 153, 153, 0.3)',
34
+ }
35
+
36
+ /**
37
+ * Builds a GeoJSON map layer drawing the given spatial extents: each extent is
38
+ * rendered from its own geometry or, when only a bounding box is available,
39
+ * from a polygon derived from that box.
40
+ *
41
+ * @returns the layer, or `null` when none of the extents can be represented.
42
+ */
43
+ export function createSpatialExtentLayer(
44
+ extents: DatasetSpatialExtent[],
45
+ overrides?: {
46
+ label?: string
47
+ clickable?: boolean
48
+ style?: SpatialExtentLayerStyle
49
+ }
50
+ ): MapContextLayer | null {
51
+ const data = spatialExtentsToFeatureCollection(extents)
52
+ if (data.features.length === 0) {
53
+ return null
54
+ }
55
+ return {
56
+ type: 'geojson',
57
+ data,
58
+ label: 'Spatial extents',
59
+ style: DEFAULT_SPATIAL_EXTENT_STYLE,
60
+ ...overrides,
61
+ }
62
+ }
15
63
 
16
64
  export function prioritizePageScroll(interactions: Collection<Interaction>) {
17
65
  interactions.clear()
@@ -1,4 +1,11 @@
1
- import type { Feature, FeatureCollection, Geometry, Position } from 'geojson'
1
+ import type {
2
+ Feature,
3
+ FeatureCollection,
4
+ Geometry,
5
+ Polygon,
6
+ Position,
7
+ } from 'geojson'
8
+ import type { DatasetSpatialExtent } from '../../../../../../libs/common/domain/src/lib/model/record'
2
9
 
3
10
  /**
4
11
  * @returns The geometry if available, otherwise null.
@@ -94,3 +101,53 @@ export function getGeometryBoundingBox(geometry: Geometry): BoundingBox {
94
101
  return coordinatesReducer(emptyExtent, geometry.coordinates)
95
102
  }
96
103
  }
104
+
105
+ /**
106
+ * Builds the closed GeoJSON Polygon matching the given bounding box.
107
+ */
108
+ export function bboxToPolygon(bbox: BoundingBox): Polygon {
109
+ const [minX, minY, maxX, maxY] = bbox
110
+ return {
111
+ type: 'Polygon',
112
+ coordinates: [
113
+ [
114
+ [minX, minY],
115
+ [minX, maxY],
116
+ [maxX, maxY],
117
+ [maxX, minY],
118
+ [minX, minY],
119
+ ],
120
+ ],
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Returns the geometry carried by a spatial extent, falling back to the polygon
126
+ * derived from its bounding box. Returns null when the extent has neither.
127
+ */
128
+ export function spatialExtentToGeometry(
129
+ extent: DatasetSpatialExtent
130
+ ): Geometry | null {
131
+ if (extent.geometry) return extent.geometry
132
+ if (extent.bbox) return bboxToPolygon(extent.bbox)
133
+ return null
134
+ }
135
+
136
+ /**
137
+ * Converts a list of dataset spatial extents into a GeoJSON FeatureCollection,
138
+ * e.g. to be used as the data source of a map layer.
139
+ */
140
+ export function spatialExtentsToFeatureCollection(
141
+ extents: DatasetSpatialExtent[]
142
+ ): FeatureCollection {
143
+ return {
144
+ type: 'FeatureCollection',
145
+ features: extents.reduce<Feature[]>((features, extent) => {
146
+ const geometry = spatialExtentToGeometry(extent)
147
+ if (geometry) {
148
+ features.push({ type: 'Feature', properties: {}, geometry })
149
+ }
150
+ return features
151
+ }, []),
152
+ }
153
+ }
package/tailwind.base.css CHANGED
@@ -119,6 +119,12 @@
119
119
  border-gray-100 focus:ring-4 focus:ring-gray-50;
120
120
  }
121
121
 
122
+ .gn-ui-btn-gray-light {
123
+ @apply gn-ui-btn text-black
124
+ bg-gray-50 hover:bg-gray-100 focus:bg-gray-100 active:bg-gray-200
125
+ border-gray-50 focus:ring-4 focus:ring-gray-100;
126
+ }
127
+
122
128
  .gn-ui-btn-black {
123
129
  @apply gn-ui-btn text-white
124
130
  bg-black hover:bg-gray-950 focus:bg-gray-950 active:bg-gray-900
@@ -243,25 +243,25 @@
243
243
  "editor.record.form.temporalExtents.addRange": "Zeitraum",
244
244
  "editor.record.form.temporalExtents.date": "Datum",
245
245
  "editor.record.form.temporalExtents.range": "Datumsbereich",
246
- "editor.record.form.topics.inspire.biota": "Biologie",
247
- "editor.record.form.topics.inspire.boundaries": "Grenzen",
248
- "editor.record.form.topics.inspire.climatology": "Klimatologie/Meteorologie/Atmosphäre",
249
- "editor.record.form.topics.inspire.economy": "Wirtschaft",
250
- "editor.record.form.topics.inspire.elevation": "Höhenangaben",
251
- "editor.record.form.topics.inspire.environnement": "Umwelt",
252
- "editor.record.form.topics.inspire.farming": "Landwirtschaft",
253
- "editor.record.form.topics.inspire.geoscientific": "Geowissenschaften",
254
- "editor.record.form.topics.inspire.health": "Gesundheitswesen",
255
- "editor.record.form.topics.inspire.imagery": "Bilddaten/Basiskarten/Landbedeckung",
256
- "editor.record.form.topics.inspire.intelligence": "Aufklärung/Militär",
257
- "editor.record.form.topics.inspire.location": "Ortsangaben",
258
- "editor.record.form.topics.inspire.oceans": "Meere",
259
- "editor.record.form.topics.inspire.planning": "Planungsunterlagen/Kataster",
260
- "editor.record.form.topics.inspire.society": "Gesellschaft",
261
- "editor.record.form.topics.inspire.structure": "Bauwerke",
262
- "editor.record.form.topics.inspire.transportation": "Verkehrswesen",
263
- "editor.record.form.topics.inspire.utilities": "Ver- und Entsorgung/Nachrichtenwesen",
264
- "editor.record.form.topics.inspire.waters": "Binnengewässer",
246
+ "editor.record.form.topics.iso.biota": "Biologie",
247
+ "editor.record.form.topics.iso.boundaries": "Grenzen",
248
+ "editor.record.form.topics.iso.climatologyMeteorologyAtmosphere": "Klimatologie/Meteorologie/Atmosphäre",
249
+ "editor.record.form.topics.iso.economy": "Wirtschaft",
250
+ "editor.record.form.topics.iso.elevation": "Höhenangaben",
251
+ "editor.record.form.topics.iso.environment": "Umwelt",
252
+ "editor.record.form.topics.iso.farming": "Landwirtschaft",
253
+ "editor.record.form.topics.iso.geoscientificInformation": "Geowissenschaften",
254
+ "editor.record.form.topics.iso.health": "Gesundheitswesen",
255
+ "editor.record.form.topics.iso.imageryBaseMapsEarthCover": "Bilddaten/Basiskarten/Landbedeckung",
256
+ "editor.record.form.topics.iso.intelligenceMilitary": "Aufklärung/Militär",
257
+ "editor.record.form.topics.iso.location": "Ortsangaben",
258
+ "editor.record.form.topics.iso.oceans": "Meere",
259
+ "editor.record.form.topics.iso.planningCadastre": "Planungsunterlagen/Kataster",
260
+ "editor.record.form.topics.iso.society": "Gesellschaft",
261
+ "editor.record.form.topics.iso.structure": "Bauwerke",
262
+ "editor.record.form.topics.iso.transportation": "Verkehrswesen",
263
+ "editor.record.form.topics.iso.utilitiesCommunication": "Ver- und Entsorgung/Nachrichtenwesen",
264
+ "editor.record.form.topics.iso.inlandWaters": "Binnengewässer",
265
265
  "editor.record.form.topics.placeholder": "Ein oder mehrere Themen auswählen",
266
266
  "editor.record.form.updateFrequency.planned": "Dieses Dataset wird regelmäßig aktualisiert.",
267
267
  "editor.record.importFromExternalFile.failure.body": "Beim Importieren des Datensatzes ist ein Fehler aufgetreten:",