geonetwork-ui 2.8.0-dev.7ecc6ee1d → 2.8.0-dev.82b7031fa

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 (132) hide show
  1. package/esm2022/libs/api/metadata-converter/src/lib/common/resource-types.mjs +6 -1
  2. package/esm2022/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.mjs +6 -4
  3. package/esm2022/libs/api/repository/src/lib/gn4/elasticsearch/constant.mjs +2 -2
  4. package/esm2022/libs/feature/dataviz/src/index.mjs +1 -2
  5. package/esm2022/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.mjs +2 -2
  6. package/esm2022/libs/feature/dataviz/src/lib/service/data.service.mjs +9 -2
  7. package/esm2022/libs/feature/editor/src/lib/components/multilingual-panel/multilingual-panel.component.mjs +3 -3
  8. package/esm2022/libs/feature/map/src/lib/map-state-container/map-state-container.component.mjs +2 -2
  9. package/esm2022/libs/feature/record/src/index.mjs +2 -1
  10. package/esm2022/libs/feature/record/src/lib/data-view/data-view.component.mjs +1 -1
  11. package/esm2022/libs/feature/record/src/lib/gpf-api-dl/gpf-api-dl.component.mjs +4 -4
  12. package/esm2022/libs/feature/record/src/lib/gpf-api-dl-list-item/gpf-api-dl-list-item.component.mjs +1 -1
  13. package/esm2022/libs/feature/record/src/lib/map-view/map-view.component.mjs +3 -3
  14. package/esm2022/libs/feature/record/src/lib/stac-view/stac-view.component.mjs +230 -0
  15. package/esm2022/libs/feature/record/src/lib/stac-view/utils.mjs +26 -0
  16. package/esm2022/libs/feature/record/src/lib/state/mdview.effects.mjs +2 -2
  17. package/esm2022/libs/feature/search/src/lib/constants.mjs +2 -2
  18. package/esm2022/libs/feature/search/src/lib/utils/service/fields.mjs +36 -22
  19. package/esm2022/libs/feature/search/src/lib/utils/service/fields.service.mjs +2 -1
  20. package/esm2022/libs/ui/elements/src/index.mjs +2 -1
  21. package/esm2022/libs/ui/elements/src/lib/downloads-list/downloads-list.component.mjs +1 -1
  22. package/esm2022/libs/ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.mjs +18 -0
  23. package/esm2022/libs/ui/inputs/src/index.mjs +2 -1
  24. package/esm2022/libs/ui/inputs/src/lib/check-toggle/check-toggle.component.mjs +3 -3
  25. package/esm2022/libs/ui/inputs/src/lib/date-picker/date-picker.component.mjs +21 -7
  26. package/esm2022/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.mjs +3 -3
  27. package/esm2022/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.mjs +35 -0
  28. package/esm2022/libs/ui/layout/src/lib/paginable.interface.mjs +1 -1
  29. package/esm2022/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.mjs +13 -5
  30. package/esm2022/libs/ui/map/src/lib/components/map-container/map-container.component.mjs +86 -32
  31. package/esm2022/libs/ui/map/src/lib/components/spatial-extent/spatial-extent.component.mjs +1 -1
  32. package/esm2022/libs/ui/widgets/src/lib/loading-mask/loading-mask.component.mjs +3 -3
  33. package/esm2022/translations/de.json +7 -3
  34. package/esm2022/translations/en.json +7 -3
  35. package/esm2022/translations/es.json +7 -3
  36. package/esm2022/translations/fr.json +7 -3
  37. package/esm2022/translations/it.json +7 -3
  38. package/esm2022/translations/nl.json +7 -3
  39. package/esm2022/translations/pt.json +7 -3
  40. package/esm2022/translations/sk.json +7 -3
  41. package/fesm2022/{geonetwork-ui-date-locales-MYnkDJ5h.mjs → geonetwork-ui-date-locales-DhlIiWpT.mjs} +2 -2
  42. package/fesm2022/{geonetwork-ui-date-locales-MYnkDJ5h.mjs.map → geonetwork-ui-date-locales-DhlIiWpT.mjs.map} +1 -1
  43. package/fesm2022/geonetwork-ui.mjs +563 -197
  44. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  45. package/libs/api/metadata-converter/src/lib/common/resource-types.d.ts +3 -0
  46. package/libs/api/metadata-converter/src/lib/common/resource-types.d.ts.map +1 -1
  47. package/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.d.ts.map +1 -1
  48. package/libs/feature/dataviz/src/index.d.ts +0 -1
  49. package/libs/feature/dataviz/src/index.d.ts.map +1 -1
  50. package/libs/feature/dataviz/src/lib/service/data.service.d.ts +2 -1
  51. package/libs/feature/dataviz/src/lib/service/data.service.d.ts.map +1 -1
  52. package/libs/feature/record/src/index.d.ts +1 -0
  53. package/libs/feature/record/src/index.d.ts.map +1 -1
  54. package/libs/feature/record/src/lib/data-view/data-view.component.d.ts +5 -1
  55. package/libs/feature/record/src/lib/data-view/data-view.component.d.ts.map +1 -1
  56. package/libs/feature/record/src/lib/gpf-api-dl/gpf-api-dl.component.d.ts +12 -5
  57. package/libs/feature/record/src/lib/gpf-api-dl/gpf-api-dl.component.d.ts.map +1 -1
  58. package/libs/feature/record/src/lib/gpf-api-dl-list-item/gpf-api-dl-list-item.component.d.ts +3 -1
  59. package/libs/feature/record/src/lib/gpf-api-dl-list-item/gpf-api-dl-list-item.component.d.ts.map +1 -1
  60. package/libs/feature/record/src/lib/map-view/map-view.component.d.ts +5 -1
  61. package/libs/feature/record/src/lib/map-view/map-view.component.d.ts.map +1 -1
  62. package/libs/feature/record/src/lib/stac-view/stac-view.component.d.ts +53 -0
  63. package/libs/feature/record/src/lib/stac-view/stac-view.component.d.ts.map +1 -0
  64. package/libs/feature/record/src/lib/stac-view/utils.d.ts +7 -0
  65. package/libs/feature/record/src/lib/stac-view/utils.d.ts.map +1 -0
  66. package/libs/feature/search/src/lib/utils/service/fields.d.ts.map +1 -1
  67. package/libs/feature/search/src/lib/utils/service/fields.service.d.ts.map +1 -1
  68. package/libs/ui/elements/src/index.d.ts +1 -0
  69. package/libs/ui/elements/src/index.d.ts.map +1 -1
  70. package/libs/ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.d.ts +8 -0
  71. package/libs/ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.d.ts.map +1 -0
  72. package/libs/ui/inputs/src/index.d.ts +1 -0
  73. package/libs/ui/inputs/src/index.d.ts.map +1 -1
  74. package/libs/ui/inputs/src/lib/date-picker/date-picker.component.d.ts +5 -0
  75. package/libs/ui/inputs/src/lib/date-picker/date-picker.component.d.ts.map +1 -1
  76. package/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.d.ts +12 -0
  77. package/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.d.ts.map +1 -0
  78. package/libs/ui/layout/src/lib/paginable.interface.d.ts +3 -3
  79. package/libs/ui/layout/src/lib/paginable.interface.d.ts.map +1 -1
  80. package/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.d.ts +2 -1
  81. package/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.d.ts.map +1 -1
  82. package/libs/ui/map/src/lib/components/map-container/map-container.component.d.ts +24 -14
  83. package/libs/ui/map/src/lib/components/map-container/map-container.component.d.ts.map +1 -1
  84. package/package.json +5 -5
  85. package/src/libs/api/metadata-converter/src/lib/common/resource-types.ts +11 -0
  86. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +11 -3
  87. package/src/libs/api/repository/src/lib/gn4/elasticsearch/constant.ts +1 -1
  88. package/src/libs/feature/dataviz/src/index.ts +0 -1
  89. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +14 -0
  90. package/src/libs/feature/editor/src/lib/components/multilingual-panel/multilingual-panel.component.html +1 -0
  91. package/src/libs/feature/record/src/index.ts +1 -0
  92. package/src/libs/feature/record/src/lib/data-view/data-view.component.ts +5 -1
  93. package/src/libs/feature/record/src/lib/gpf-api-dl/gpf-api-dl.component.ts +10 -8
  94. package/src/libs/feature/record/src/lib/gpf-api-dl-list-item/gpf-api-dl-list-item.component.ts +1 -1
  95. package/src/libs/feature/record/src/lib/map-view/map-view.component.html +1 -2
  96. package/src/libs/feature/record/src/lib/map-view/map-view.component.ts +5 -2
  97. package/src/libs/feature/record/src/lib/stac-view/stac-view.component.css +8 -0
  98. package/src/libs/feature/record/src/lib/stac-view/stac-view.component.html +87 -0
  99. package/src/libs/feature/record/src/lib/stac-view/stac-view.component.ts +339 -0
  100. package/src/libs/feature/record/src/lib/stac-view/utils.ts +57 -0
  101. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +1 -1
  102. package/src/libs/feature/search/src/lib/constants.ts +1 -1
  103. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +1 -0
  104. package/src/libs/feature/search/src/lib/utils/service/fields.ts +37 -33
  105. package/src/libs/ui/elements/src/index.ts +1 -0
  106. package/src/libs/ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.html +13 -0
  107. package/src/libs/ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.ts +15 -0
  108. package/src/libs/ui/inputs/src/index.ts +1 -0
  109. package/src/libs/ui/inputs/src/lib/check-toggle/check-toggle.component.html +3 -3
  110. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +17 -1
  111. package/src/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.css +0 -0
  112. package/src/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.html +15 -0
  113. package/src/libs/ui/inputs/src/lib/date-range-inputs/date-range-inputs.component.ts +41 -0
  114. package/src/libs/ui/layout/src/lib/paginable.interface.ts +3 -3
  115. package/src/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.html +12 -6
  116. package/src/libs/ui/layout/src/lib/previous-next-buttons/previous-next-buttons.component.ts +4 -1
  117. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.html +16 -14
  118. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +144 -65
  119. package/translations/de.json +7 -3
  120. package/translations/en.json +7 -3
  121. package/translations/es.json +7 -3
  122. package/translations/fr.json +7 -3
  123. package/translations/it.json +7 -3
  124. package/translations/nl.json +7 -3
  125. package/translations/pt.json +7 -3
  126. package/translations/sk.json +7 -3
  127. package/esm2022/libs/feature/dataviz/src/lib/stac-view/stac-view.component.mjs +0 -51
  128. package/libs/feature/dataviz/src/lib/stac-view/stac-view.component.d.ts +0 -16
  129. package/libs/feature/dataviz/src/lib/stac-view/stac-view.component.d.ts.map +0 -1
  130. package/src/libs/feature/dataviz/src/lib/stac-view/stac-view.component.html +0 -40
  131. package/src/libs/feature/dataviz/src/lib/stac-view/stac-view.component.ts +0 -62
  132. /package/src/libs/{feature/dataviz/src/lib/stac-view/stac-view.component.css → ui/elements/src/lib/stac-items-result-grid/stac-items-result-grid.component.css} +0 -0
@@ -7,7 +7,6 @@ export const ES_SOURCE_SUMMARY = [
7
7
  'resourceAbstractObject',
8
8
  'overview',
9
9
  'logo',
10
- 'codelist_status_text',
11
10
  'link',
12
11
  'linkProtocol',
13
12
  'contactForResource*.organisation*',
@@ -16,6 +15,7 @@ export const ES_SOURCE_SUMMARY = [
16
15
  'userSavedCount',
17
16
  'cl_topic',
18
17
  'cl_maintenanceAndUpdateFrequency',
18
+ 'cl_presentationForm',
19
19
  'MD_LegalConstraints*Object',
20
20
  'qualityScore',
21
21
  'allKeywords',
@@ -3,4 +3,3 @@ export * from './lib/chart-view/chart-view.component'
3
3
  export * from './lib/figure/figure-container/figure-container.component'
4
4
  export * from './lib/geo-table-view/geo-table-view.component'
5
5
  export * from './lib/table-view/table-view.component'
6
- export * from './lib/stac-view/stac-view.component'
@@ -7,6 +7,9 @@ import {
7
7
  WfsEndpoint,
8
8
  WfsVersion,
9
9
  TmsEndpoint,
10
+ StacEndpoint,
11
+ GetCollectionItemsOptions,
12
+ StacItemsDocument,
10
13
  } from '@camptocamp/ogc-client'
11
14
  import {
12
15
  BaseReader,
@@ -244,6 +247,17 @@ export class DataService {
244
247
  })
245
248
  }
246
249
 
250
+ async getItemsFromStacApi(
251
+ url: string,
252
+ options: GetCollectionItemsOptions
253
+ ): Promise<StacItemsDocument> {
254
+ return await StacEndpoint.getItemsFromUrl(url, options)
255
+ .then((response) => response)
256
+ .catch(() => {
257
+ throw new Error(`ogc.unreachable.unknown`)
258
+ })
259
+ }
260
+
247
261
  async getGeodataLinksFromTms(
248
262
  tmsLink: DatasetServiceDistribution,
249
263
  keepOriginalLink = false
@@ -40,6 +40,7 @@
40
40
  <div
41
41
  class="flex flex-col gap-2 w-full px-2"
42
42
  data-test="langAvailable"
43
+ [attr.data-test-lang]="lang"
43
44
  *ngFor="let lang of languages"
44
45
  >
45
46
  <gn-ui-button
@@ -8,3 +8,4 @@ export * from './lib/external-viewer-button/external-viewer-button.component'
8
8
  export * from './lib/gpf-api-dl/gpf-api-dl.component'
9
9
  export * from './lib/map-view/map-view.component'
10
10
  export * from './lib/record-meta/record-meta.component'
11
+ export * from './lib/stac-view/stac-view.component'
@@ -56,7 +56,11 @@ export class DataViewComponent {
56
56
  this.linkSelected.emit(this.selectedLink$.value)
57
57
  }
58
58
  }
59
- @Input() set datavizConfig(value: any) {
59
+ @Input() set datavizConfig(value: {
60
+ view?: string
61
+ source?: DatasetOnlineResource
62
+ chartConfig?: DatavizChartConfigModel
63
+ }) {
60
64
  if ((value && value.view === 'table') || value.view === 'chart') {
61
65
  this._selectedView = value.view
62
66
  }
@@ -45,8 +45,8 @@ export interface TermBucket {
45
45
  }
46
46
 
47
47
  export interface Field {
48
- entry: Array<any>
49
- link: any
48
+ entry: Array<unknown>
49
+ link: Record<string, unknown>
50
50
  }
51
51
 
52
52
  @Component({
@@ -75,7 +75,7 @@ export class GpfApiDlComponent implements OnInit {
75
75
  page$ = new BehaviorSubject(1)
76
76
  url =
77
77
  'https://data.geopf.fr/telechargement/capabilities?outputFormat=application/json'
78
- choices: any
78
+ choices: { zone: TermBucket[]; format: TermBucket[]; category: TermBucket[] }
79
79
  bucketPromisesZone: Choice[]
80
80
  bucketPromisesFormat: Choice[]
81
81
  bucketPromisesCrs: Choice[]
@@ -144,8 +144,10 @@ export class GpfApiDlComponent implements OnInit {
144
144
  })
145
145
  )
146
146
 
147
- getFilteredProduct$(url): Observable<any> {
148
- return this.http.get(url)
147
+ getFilteredProduct$(
148
+ url
149
+ ): Observable<{ entry: unknown[]; totalentries: number }> {
150
+ return this.http.get<{ entry: unknown[]; totalentries: number }>(url)
149
151
  }
150
152
 
151
153
  getLinkFormat(produit): string {
@@ -224,7 +226,7 @@ export class GpfApiDlComponent implements OnInit {
224
226
 
225
227
  const tempZone = this.choices.zone.map((bucket) => ({
226
228
  value: bucket.term,
227
- label: bucket.label,
229
+ label: String(bucket.label),
228
230
  }))
229
231
  tempZone.sort((a, b) => (a.label > b.label ? 1 : -1))
230
232
  tempZone.unshift({ value: 'null', label: 'ZONE' })
@@ -233,7 +235,7 @@ export class GpfApiDlComponent implements OnInit {
233
235
 
234
236
  const tempFormat = this.choices.format.map((bucket) => ({
235
237
  value: bucket.term,
236
- label: bucket.label,
238
+ label: String(bucket.label),
237
239
  }))
238
240
  tempFormat.sort((a, b) => (a.label > b.label ? 1 : -1))
239
241
  tempFormat.unshift({ value: 'null', label: 'FORMAT' })
@@ -242,7 +244,7 @@ export class GpfApiDlComponent implements OnInit {
242
244
 
243
245
  const tempCrs = this.choices.category.map((bucket) => ({
244
246
  value: bucket.term,
245
- label: bucket.label,
247
+ label: String(bucket.label),
246
248
  }))
247
249
  tempCrs.sort((a, b) => (a.label > b.label ? 1 : -1))
248
250
  tempCrs.unshift({ value: 'null', label: 'CRS' })
@@ -28,7 +28,7 @@ export class GpfApiDlListItemComponent implements OnInit {
28
28
  @Input() isFromWfs: boolean
29
29
 
30
30
  constructor(protected http: HttpClient) {}
31
- liste$: Observable<any>
31
+ liste$: Observable<{ entry: unknown[] }>
32
32
 
33
33
  ngOnInit(): void {
34
34
  this.liste$ = this.http
@@ -112,9 +112,8 @@
112
112
  type="outline"
113
113
  (buttonClick)="toggleLegend()"
114
114
  extraClass="absolute top-[1em] right-[1em] rounded p-1 text-xs bg-white"
115
- translate
116
115
  >
117
- map.legend.title
116
+ {{ 'map.legend.title' | translate }}
118
117
  </gn-ui-button>
119
118
 
120
119
  <gn-ui-loading-mask
@@ -27,7 +27,6 @@ import {
27
27
  map,
28
28
  shareReplay,
29
29
  switchMap,
30
- take,
31
30
  tap,
32
31
  } from 'rxjs/operators'
33
32
  import { MdViewFacade } from '../state/mdview.facade'
@@ -118,7 +117,11 @@ export class MapViewComponent implements AfterViewInit {
118
117
  @Input() set selectedView(value: string) {
119
118
  this.selectedView$.next(value)
120
119
  }
121
- @Input() set datavizConfig(value: any) {
120
+ @Input() set datavizConfig(value: {
121
+ view?: string
122
+ styleTMSIndex?: number
123
+ source?: DatasetOnlineResource
124
+ }) {
122
125
  if (value && value.view === 'map') {
123
126
  this.selectedView$.next(value.view)
124
127
  if (value.styleTMSIndex) {
@@ -0,0 +1,8 @@
1
+ :host {
2
+ --gn-ui-button-padding: 7px 8px;
3
+ }
4
+
5
+ /* Ensure OpenLayers controls are above the custom toggle */
6
+ ::ng-deep .ol-overlaycontainer-stopevent {
7
+ z-index: 11 !important;
8
+ }
@@ -0,0 +1,87 @@
1
+ <div class="mt-6 bg-white border border-gray-300 overflow-hidden rounded-lg">
2
+ <div
3
+ class="w-full h-[700px] md:h-[366px] flex md:flex-row flex-col border-b border-gray-300"
4
+ >
5
+ @let filterState = filterState$ | async;
6
+
7
+ <div class="w-full md:w-5/12 h-[366px] flex flex-col">
8
+ <gn-ui-date-range-inputs
9
+ [temporalExtent]="filterState.temporalExtent"
10
+ (temporalExtentChange)="onTemporalExtentChange($event)"
11
+ ></gn-ui-date-range-inputs>
12
+
13
+ <div class="mt-auto mb-8 mx-8" *ngIf="isFilterModified$ | async">
14
+ <gn-ui-button
15
+ id="reset-filters-button"
16
+ type="light"
17
+ (buttonClick)="onResetFilters()"
18
+ >
19
+ <span translate>stac.filter.reset</span>
20
+ <ng-icon name="matDeleteOutline" class="ml-2"></ng-icon>
21
+ </gn-ui-button>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="w-full md:w-7/12 h-[334px] md:h-[366px] flex flex-col">
26
+ <gn-ui-map-container
27
+ #mapContainer
28
+ [context]="mapContext$ | async"
29
+ (extentChange)="onSpatialExtentChange($event)"
30
+ (resolvedExtentChange)="onResolvedMapExtentChange($event)"
31
+ class="w-full h-full"
32
+ >
33
+ <div
34
+ class="bg-white rounded-xl shadow-xl absolute left-1/2 transform -translate-x-1/2 bottom-0 mb-2.5 px-4 py-2 z-10 max-w-[90%] w-max"
35
+ >
36
+ <gn-ui-check-toggle
37
+ [value]="filterState.isSpatialExtentFilterEnabled"
38
+ (toggled)="onSpatialFilterToggle($event)"
39
+ [label]="'stac.filter.enable' | translate"
40
+ [color]="'primary'"
41
+ ></gn-ui-check-toggle>
42
+ </div>
43
+ </gn-ui-map-container>
44
+ </div>
45
+ </div>
46
+ <div
47
+ class="relative mx-5 my-[30px] min-h-[274px] flex items-center justify-center flex-col"
48
+ >
49
+ <!-- Keep grid outside ngIf to keep items$ observable alive -->
50
+ <gn-ui-stac-items-result-grid
51
+ [items]="items$ | async"
52
+ ></gn-ui-stac-items-result-grid>
53
+ <div
54
+ *ngIf="(items$ | async)?.length > 0; else noResultsOrError"
55
+ class="mt-[20px] mx-auto flex justify-center"
56
+ >
57
+ <gn-ui-previous-next-buttons
58
+ [listComponent]="this"
59
+ [displayLabels]="true"
60
+ ></gn-ui-previous-next-buttons>
61
+ </div>
62
+ <ng-template #noResultsOrError>
63
+ <gn-ui-popup-alert
64
+ *ngIf="error$ | async as error"
65
+ type="warning"
66
+ icon="matErrorOutlineOutline"
67
+ class="absolute left-0 top-0 w-full block"
68
+ >
69
+ <span translate>{{ error }}</span>
70
+ </gn-ui-popup-alert>
71
+ <div
72
+ *ngIf="(error$ | async) === null"
73
+ class="flex items-center justify-center flex-col h-full gap-[10px]"
74
+ >
75
+ <h2 class="text-center text-xl" translate>stac.results.noResults</h2>
76
+ <gn-ui-button
77
+ id="no-results-button"
78
+ type="secondary"
79
+ (buttonClick)="onResetFilters()"
80
+ >
81
+ <span translate>stac.filter.reset</span>
82
+ <ng-icon name="matDeleteOutline" class="ml-2"></ng-icon>
83
+ </gn-ui-button>
84
+ </div>
85
+ </ng-template>
86
+ </div>
87
+ </div>
@@ -0,0 +1,339 @@
1
+ import { CommonModule } from '@angular/common'
2
+ import {
3
+ AfterViewInit,
4
+ ChangeDetectionStrategy,
5
+ Component,
6
+ OnInit,
7
+ ViewChild,
8
+ } from '@angular/core'
9
+ import {
10
+ DatasetRecord,
11
+ DatasetTemporalExtent,
12
+ } from '../../../../../../libs/common/domain/src/lib/model/record'
13
+ import {
14
+ ButtonComponent,
15
+ DateRangeInputsComponent,
16
+ CheckToggleComponent,
17
+ } from '../../../../../../libs/ui/inputs/src'
18
+ import {
19
+ MapContainerComponent,
20
+ prioritizePageScroll,
21
+ } from '../../../../../../libs/ui/map/src'
22
+ import { Extent, MapContext } from '@geospatial-sdk/core/dist/model'
23
+ import { StacItemsResultGridComponent } from '../../../../../../libs/ui/elements/src'
24
+ import { NgIconComponent, provideIcons } from '@ng-icons/core'
25
+ import { matDeleteOutline } from '@ng-icons/material-icons/outline'
26
+ import {
27
+ TranslateDirective,
28
+ TranslatePipe,
29
+ TranslateService,
30
+ } from '@ngx-translate/core'
31
+ import { DataService } from '../../../../../../libs/feature/dataviz/src'
32
+ import {
33
+ BehaviorSubject,
34
+ catchError,
35
+ debounceTime,
36
+ distinctUntilChanged,
37
+ from,
38
+ map,
39
+ Observable,
40
+ of,
41
+ shareReplay,
42
+ switchMap,
43
+ take,
44
+ tap,
45
+ } from 'rxjs'
46
+ import { GetCollectionItemsOptions, StacItem } from '@camptocamp/ogc-client'
47
+ import { MdViewFacade } from '../state'
48
+ import {
49
+ areSpatialExtentsEqual,
50
+ areTemporalExtentsEqual,
51
+ areFilterStatesEqual,
52
+ } from './utils'
53
+ import { MapUtilsService } from '../../../../../../libs/feature/map/src'
54
+ import { PreviousNextButtonsComponent } from '../../../../../../libs/ui/layout/src'
55
+ import { FetchError } from '../../../../../../libs/util/data-fetcher/src'
56
+ import { PopupAlertComponent } from '../../../../../../libs/ui/widgets/src'
57
+
58
+ const STAC_ITEMS_PER_PAGE = 12
59
+ const DEBOUNCE_TIME_MS = 500
60
+
61
+ export interface StacFilterState {
62
+ temporalExtent: DatasetTemporalExtent | null
63
+ spatialExtent: Extent | null
64
+ isSpatialExtentFilterEnabled: boolean
65
+ pageUrl: string | null
66
+ }
67
+
68
+ @Component({
69
+ selector: 'gn-ui-stac-view',
70
+ templateUrl: './stac-view.component.html',
71
+ styleUrls: ['./stac-view.component.css'],
72
+ changeDetection: ChangeDetectionStrategy.OnPush,
73
+ standalone: true,
74
+ imports: [
75
+ CommonModule,
76
+ NgIconComponent,
77
+ TranslateDirective,
78
+ TranslatePipe,
79
+ StacItemsResultGridComponent,
80
+ DateRangeInputsComponent,
81
+ MapContainerComponent,
82
+ CheckToggleComponent,
83
+ PreviousNextButtonsComponent,
84
+ PopupAlertComponent,
85
+ ButtonComponent,
86
+ ],
87
+ viewProviders: [provideIcons({ matDeleteOutline })],
88
+ })
89
+ export class StacViewComponent implements OnInit, AfterViewInit {
90
+ @ViewChild('mapContainer') mapContainer: MapContainerComponent
91
+
92
+ initialTemporalExtent: DatasetTemporalExtent | null = null
93
+ initialSpatialExtent: Extent | null = null
94
+ resolvedInitialSpatialExtent: Extent | null = null
95
+ initialPageUrl: string
96
+ previousPageUrl: string
97
+ nextPageUrl: string
98
+
99
+ error$ = new BehaviorSubject<string | null>(null)
100
+ mapContext$ = new BehaviorSubject<MapContext>({
101
+ layers: [],
102
+ view: null,
103
+ })
104
+ filterState$ = new BehaviorSubject<StacFilterState>({
105
+ temporalExtent: null,
106
+ spatialExtent: null,
107
+ isSpatialExtentFilterEnabled: true,
108
+ pageUrl: null,
109
+ })
110
+
111
+ isFilterModified$ = this.filterState$.pipe(
112
+ map((filterState) => {
113
+ const isTemporalModified = !areTemporalExtentsEqual(
114
+ filterState.temporalExtent,
115
+ this.initialTemporalExtent
116
+ )
117
+
118
+ const isSpatialModified =
119
+ this.resolvedInitialSpatialExtent &&
120
+ filterState.spatialExtent !== null &&
121
+ filterState.isSpatialExtentFilterEnabled &&
122
+ !areSpatialExtentsEqual(
123
+ filterState.spatialExtent,
124
+ this.resolvedInitialSpatialExtent
125
+ )
126
+
127
+ return isTemporalModified || isSpatialModified
128
+ }),
129
+ shareReplay({ bufferSize: 1, refCount: false })
130
+ )
131
+
132
+ items$: Observable<StacItem[]> = this.filterState$.pipe(
133
+ debounceTime(DEBOUNCE_TIME_MS),
134
+ distinctUntilChanged((prev, curr) => areFilterStatesEqual(prev, curr)),
135
+ switchMap((filterState) => {
136
+ if (filterState.pageUrl === null) {
137
+ return of([])
138
+ }
139
+
140
+ this.error$.next(null)
141
+ return from(
142
+ this.dataService.getItemsFromStacApi(
143
+ filterState.pageUrl,
144
+ this.buildRequestOptions(filterState)
145
+ )
146
+ ).pipe(
147
+ tap((stacDocument) => {
148
+ this.previousPageUrl =
149
+ stacDocument.links.find((link) => link.rel === 'previous')?.href ||
150
+ null
151
+ this.nextPageUrl =
152
+ stacDocument.links.find((link) => link.rel === 'next')?.href || null
153
+ }),
154
+ map((stacDocument) => stacDocument.features),
155
+ catchError((err) => {
156
+ this.handleError(err)
157
+ return of([])
158
+ })
159
+ )
160
+ }),
161
+ shareReplay({ bufferSize: 1, refCount: false })
162
+ )
163
+
164
+ constructor(
165
+ private dataService: DataService,
166
+ private metadataViewFacade: MdViewFacade,
167
+ private mapUtils: MapUtilsService,
168
+ private translateService: TranslateService
169
+ ) {}
170
+
171
+ ngOnInit() {
172
+ this.metadataViewFacade.metadata$
173
+ .pipe(
174
+ take(1),
175
+ map((metadata) => {
176
+ const temporalExtents =
177
+ metadata?.kind === 'dataset'
178
+ ? (metadata as DatasetRecord).temporalExtents
179
+ : []
180
+
181
+ const temporalExtent =
182
+ temporalExtents.length > 0
183
+ ? temporalExtents[0]
184
+ : ({
185
+ start: null,
186
+ end: null,
187
+ } as DatasetTemporalExtent)
188
+
189
+ const spatialExtent = this.mapUtils.getRecordExtent(metadata)
190
+ return { temporalExtent, spatialExtent }
191
+ })
192
+ )
193
+ .subscribe(({ temporalExtent, spatialExtent }) => {
194
+ this.initialTemporalExtent = temporalExtent
195
+ this.initialSpatialExtent = spatialExtent
196
+
197
+ this.filterState$.next({
198
+ ...this.filterState$.value,
199
+ temporalExtent: this.initialTemporalExtent,
200
+ })
201
+
202
+ this.mapContext$.next({
203
+ ...this.mapContext$.value,
204
+ view: {
205
+ extent: spatialExtent,
206
+ },
207
+ })
208
+ })
209
+
210
+ this.metadataViewFacade.stacLinks$
211
+ .pipe(
212
+ take(1),
213
+ map((links) => (links && links.length > 0 ? links[0] : null))
214
+ )
215
+ .subscribe((link) => {
216
+ if (link) {
217
+ this.initialPageUrl = link.url.href
218
+ this.filterState$.next({
219
+ ...this.filterState$.value,
220
+ pageUrl: link.url.href,
221
+ })
222
+ }
223
+ })
224
+ }
225
+
226
+ async ngAfterViewInit() {
227
+ const map = await this.mapContainer.openlayersMap
228
+ prioritizePageScroll(map.getInteractions())
229
+ }
230
+
231
+ onTemporalExtentChange(extent: DatasetTemporalExtent | null) {
232
+ this.filterState$.next({
233
+ ...this.filterState$.value,
234
+ temporalExtent: extent,
235
+ pageUrl: this.initialPageUrl,
236
+ })
237
+ }
238
+
239
+ onSpatialExtentChange(extent: Extent) {
240
+ this.filterState$.next({
241
+ ...this.filterState$.value,
242
+ spatialExtent: extent,
243
+ pageUrl: this.initialPageUrl,
244
+ })
245
+ }
246
+
247
+ onResolvedMapExtentChange(extent: Extent) {
248
+ this.resolvedInitialSpatialExtent = extent
249
+ }
250
+
251
+ onSpatialFilterToggle(enabled: boolean) {
252
+ this.filterState$.next({
253
+ ...this.filterState$.value,
254
+ isSpatialExtentFilterEnabled: enabled,
255
+ pageUrl: this.initialPageUrl,
256
+ })
257
+ }
258
+
259
+ onResetFilters() {
260
+ this.mapContext$.next({
261
+ ...this.mapContext$.value,
262
+ view: {
263
+ extent: this.initialSpatialExtent,
264
+ },
265
+ })
266
+
267
+ this.filterState$.next({
268
+ temporalExtent: this.initialTemporalExtent,
269
+ spatialExtent: this.resolvedInitialSpatialExtent,
270
+ isSpatialExtentFilterEnabled: true,
271
+ pageUrl: this.initialPageUrl,
272
+ })
273
+ }
274
+
275
+ private buildRequestOptions(
276
+ filterState: StacFilterState
277
+ ): GetCollectionItemsOptions {
278
+ const options: GetCollectionItemsOptions = {
279
+ limit: STAC_ITEMS_PER_PAGE,
280
+ }
281
+
282
+ if (
283
+ filterState.temporalExtent &&
284
+ (filterState.temporalExtent.start || filterState.temporalExtent.end)
285
+ ) {
286
+ options.datetime = {
287
+ ...(filterState.temporalExtent.start && {
288
+ start: filterState.temporalExtent.start,
289
+ }),
290
+ ...(filterState.temporalExtent.end && {
291
+ end: filterState.temporalExtent.end,
292
+ }),
293
+ }
294
+ }
295
+
296
+ if (filterState.isSpatialExtentFilterEnabled && filterState.spatialExtent) {
297
+ options.bbox = filterState.spatialExtent
298
+ }
299
+
300
+ return options
301
+ }
302
+
303
+ handleError(error: FetchError | Error | string) {
304
+ if (error instanceof FetchError) {
305
+ this.error$.next(
306
+ this.translateService.instant(`dataset.error.${error.type}`, {
307
+ info: error.info,
308
+ })
309
+ )
310
+ console.warn(error.message)
311
+ } else if (error instanceof Error) {
312
+ this.error$.next(this.translateService.instant(error.message))
313
+ console.warn(error.stack || error)
314
+ } else {
315
+ this.error$.next(this.translateService.instant(error))
316
+ console.warn(error)
317
+ }
318
+ }
319
+
320
+ // Paginable API
321
+ get isFirstPage() {
322
+ return this.previousPageUrl == null
323
+ }
324
+ get isLastPage() {
325
+ return this.nextPageUrl == null
326
+ }
327
+ goToNextPage() {
328
+ this.filterState$.next({
329
+ ...this.filterState$.value,
330
+ pageUrl: this.nextPageUrl,
331
+ })
332
+ }
333
+ goToPrevPage() {
334
+ this.filterState$.next({
335
+ ...this.filterState$.value,
336
+ pageUrl: this.previousPageUrl,
337
+ })
338
+ }
339
+ }
@@ -0,0 +1,57 @@
1
+ import { DatasetTemporalExtent } from '../../../../../../libs/common/domain/src/lib/model/record'
2
+ import { Extent } from '@geospatial-sdk/core/dist/model'
3
+ import { StacFilterState } from './stac-view.component'
4
+
5
+ export function areTemporalExtentsEqual(
6
+ previous: DatasetTemporalExtent | null,
7
+ current: DatasetTemporalExtent | null
8
+ ): boolean {
9
+ const previousStartTime = previous?.start?.getTime() ?? null
10
+ const previousEndTime = previous?.end?.getTime() ?? null
11
+
12
+ const currentStartTime = current?.start?.getTime() ?? null
13
+ const currentEndTime = current?.end?.getTime() ?? null
14
+
15
+ return (
16
+ previousStartTime === currentStartTime && previousEndTime === currentEndTime
17
+ )
18
+ }
19
+
20
+ export function areSpatialExtentsEqual(
21
+ previous: Extent | null,
22
+ current: Extent | null
23
+ ): boolean {
24
+ return (
25
+ previous?.[0] === current?.[0] &&
26
+ previous?.[1] === current?.[1] &&
27
+ previous?.[2] === current?.[2] &&
28
+ previous?.[3] === current?.[3]
29
+ )
30
+ }
31
+
32
+ export function areFilterStatesEqual(
33
+ previous: StacFilterState,
34
+ current: StacFilterState
35
+ ): boolean {
36
+ const sameTemporalExtents = areTemporalExtentsEqual(
37
+ previous.temporalExtent,
38
+ current.temporalExtent
39
+ )
40
+
41
+ const sameSpatialExtents =
42
+ !current.isSpatialExtentFilterEnabled ||
43
+ areSpatialExtentsEqual(previous.spatialExtent, current.spatialExtent)
44
+
45
+ const sameIsSpatialExtentFilterEnabled =
46
+ previous.isSpatialExtentFilterEnabled ===
47
+ current.isSpatialExtentFilterEnabled
48
+
49
+ const samePageUrl = previous.pageUrl === current.pageUrl
50
+
51
+ return (
52
+ sameTemporalExtents &&
53
+ sameSpatialExtents &&
54
+ sameIsSpatialExtentFilterEnabled &&
55
+ samePageUrl
56
+ )
57
+ }
@@ -68,7 +68,7 @@ export class MdViewEffects {
68
68
  map((related) => {
69
69
  return MdViewActions.setRelated({ related })
70
70
  }),
71
- catchError((error) => of(MdViewActions.setRelated({ related: null })))
71
+ catchError(() => of(MdViewActions.setRelated({ related: null })))
72
72
  )
73
73
  )
74
74