• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geonetwork / geonetwork-ui / 14995892876

13 May 2025 11:52AM UTC coverage: 84.001% (-0.5%) from 84.526%
14995892876

Pull #1238

github

web-flow
Merge 7087ecfbc into 492bf4497
Pull Request #1238: [Datahub]: UI fixes and improvements on Service capabilities

1682 of 2280 branches covered (73.77%)

Branch coverage included in aggregate %.

1 of 5 new or added lines in 1 file covered. (20.0%)

11 existing lines in 2 files now uncovered.

5364 of 6108 relevant lines covered (87.82%)

11.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.59
/libs/feature/record/src/lib/map-view/map-view.component.ts
1
import {
1✔
2
  AfterViewInit,
3
  ChangeDetectionStrategy,
4
  ChangeDetectorRef,
5
  Component,
6
  Input,
7
  ViewChild,
8
} from '@angular/core'
9
import { MapUtilsService } from '@geonetwork-ui/feature/map'
1✔
10
import { getLinkLabel } from '@geonetwork-ui/util/shared'
1✔
11
import {
1✔
12
  BehaviorSubject,
13
  combineLatest,
14
  from,
15
  Observable,
16
  of,
17
  startWith,
18
  throwError,
19
  withLatestFrom,
20
} from 'rxjs'
21
import {
1✔
22
  catchError,
23
  distinctUntilChanged,
24
  finalize,
25
  map,
26
  shareReplay,
27
  switchMap,
28
  tap,
29
} from 'rxjs/operators'
30
import { MdViewFacade } from '../state/mdview.facade'
1✔
31
import { DataService } from '@geonetwork-ui/feature/dataviz'
1✔
32
import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record'
33
import {
1✔
34
  createViewFromLayer,
35
  MapContext,
36
  MapContextLayer,
37
  SourceLoadErrorEvent,
38
} from '@geospatial-sdk/core'
39
import {
1✔
40
  FeatureDetailComponent,
41
  MapContainerComponent,
42
  MapLegendComponent,
43
  prioritizePageScroll,
44
} from '@geonetwork-ui/ui/map'
45
import { Feature } from 'geojson'
46
import { NgIconComponent, provideIcons } from '@ng-icons/core'
1✔
47
import { matClose } from '@ng-icons/material-icons/baseline'
1✔
48
import { CommonModule } from '@angular/common'
1✔
49
import {
1✔
50
  ButtonComponent,
51
  DropdownSelectorComponent,
52
} from '@geonetwork-ui/ui/inputs'
53
import { TranslateModule, TranslateService } from '@ngx-translate/core'
1✔
54
import { ExternalViewerButtonComponent } from '../external-viewer-button/external-viewer-button.component'
1✔
55
import {
1✔
56
  LoadingMaskComponent,
57
  PopupAlertComponent,
58
} from '@geonetwork-ui/ui/widgets'
59
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
1✔
60
import { FetchError } from '@geonetwork-ui/data-fetcher'
1✔
61

62
marker('map.dropdown.placeholder')
1✔
63
marker('wfs.feature.limit')
1✔
64
marker('dataset.error.restrictedAccess')
1✔
65

66
@Component({
67
  selector: 'gn-ui-map-view',
68
  templateUrl: './map-view.component.html',
69
  styleUrls: ['./map-view.component.css'],
70
  changeDetection: ChangeDetectionStrategy.OnPush,
71
  standalone: true,
72
  imports: [
73
    CommonModule,
74
    DropdownSelectorComponent,
75
    MapContainerComponent,
76
    FeatureDetailComponent,
77
    PopupAlertComponent,
78
    TranslateModule,
79
    LoadingMaskComponent,
80
    NgIconComponent,
81
    ExternalViewerButtonComponent,
82
    ButtonComponent,
83
    MapLegendComponent,
84
  ],
85
  viewProviders: [provideIcons({ matClose })],
86
})
87
export class MapViewComponent implements AfterViewInit {
1✔
88
  @Input() set exceedsLimit(value: boolean) {
89
    this.excludeWfs$.next(value)
×
90
  }
91
  @Input() displaySource = true
52✔
92
  @ViewChild('mapContainer') mapContainer: MapContainerComponent
93

94
  excludeWfs$ = new BehaviorSubject(false)
52✔
95
  hidePreview = false
52✔
96
  selection: Feature
97
  showLegend = true
52✔
98
  legendExists = false
52✔
99

100
  toggleLegend() {
101
    this.showLegend = !this.showLegend
×
102
  }
103

104
  onLegendStatusChange(status: boolean) {
105
    this.legendExists = status
1✔
106
  }
107

108
  compatibleMapLinks$ = combineLatest([
52✔
109
    this.mdViewFacade.mapApiLinks$,
110
    this.mdViewFacade.geoDataLinksWithGeometry$,
111
  ]).pipe(
112
    switchMap(async ([mapApiLinks, geoDataLinksWithGeometry]) => {
44✔
113
      // looking for TMS links to process
114
      let processedMapApiLinks = await Promise.all(
44✔
115
        mapApiLinks.map((link) => {
116
          if (link.type === 'service' && link.accessServiceProtocol === 'tms') {
47✔
117
            return this.dataService.getGeodataLinksFromTms(link)
1✔
118
          }
119
          return link
46✔
120
        })
121
      )
122
      processedMapApiLinks = processedMapApiLinks.flat()
44✔
123
      return [...processedMapApiLinks, ...geoDataLinksWithGeometry]
44✔
124
    }),
125
    shareReplay(1)
126
  )
127

128
  dropdownChoices$ = this.compatibleMapLinks$.pipe(
52✔
129
    map((links) =>
130
      links.length
38✔
131
        ? links.map((link, index) => ({
70✔
132
            label: getLinkLabel(link),
133
            value: index,
134
          }))
135
        : [{ label: 'map.dropdown.placeholder', value: 0 }]
136
    )
137
  )
138
  selectedLinkIndex$ = new BehaviorSubject(0)
52✔
139

140
  loading = false
52✔
141
  error = null
52✔
142

143
  selectedLink$ = combineLatest([
52✔
144
    this.compatibleMapLinks$,
145
    this.selectedLinkIndex$.pipe(distinctUntilChanged()),
146
  ]).pipe(map(([links, index]) => links[index]))
84✔
147

148
  currentLayers$ = combineLatest([this.selectedLink$, this.excludeWfs$]).pipe(
52✔
149
    switchMap(([link, excludeWfs]) => {
150
      if (!link) {
44✔
151
        return of([])
4✔
152
      }
153
      if (excludeWfs && link.accessServiceProtocol === 'wfs') {
40✔
154
        this.hidePreview = true
4✔
155
        return of([])
4✔
156
      }
157
      this.hidePreview = false
36✔
158
      this.loading = true
36✔
159
      this.error = null
36✔
160
      if (link.accessRestricted) {
36✔
161
        this.handleError('dataset.error.restrictedAccess')
1✔
162
        return of([])
1✔
163
      }
164
      return this.getLayerFromLink(link).pipe(
35✔
165
        map((layer) => [layer]),
30✔
166
        catchError((e) => {
167
          this.handleError(e)
3✔
168
          return of([])
3✔
169
        }),
170
        finalize(() => (this.loading = false))
33✔
171
      )
172
    })
173
  )
174

175
  mapContext$: Observable<MapContext> = this.currentLayers$.pipe(
52✔
176
    switchMap((layers) =>
177
      from(createViewFromLayer(layers[0])).pipe(
42✔
178
        catchError(() => of(null)), // could not zoom on the layer: use the record extent
2✔
179
        map((view) => ({
36✔
180
          layers,
181
          view,
182
        })),
183
        tap(() => {
184
          this.resetSelection()
36✔
185
        })
186
      )
187
    ),
188
    startWith({
189
      layers: [],
190
      view: null,
191
    }),
192
    withLatestFrom(this.mdViewFacade.metadata$),
193
    map(([context, metadata]) => {
194
      if (context.view) return context
88✔
195
      const extent = this.mapUtils.getRecordExtent(metadata)
85✔
196
      const view = extent ? { extent } : null
85✔
197
      return {
85✔
198
        ...context,
199
        view,
200
      }
201
    }),
202
    shareReplay(1)
203
  )
204

205
  constructor(
206
    private mdViewFacade: MdViewFacade,
52!
207
    private mapUtils: MapUtilsService,
52!
208
    private dataService: DataService,
52!
209
    private changeRef: ChangeDetectorRef,
52!
210
    private translateService: TranslateService
52✔
211
  ) {}
212

213
  async ngAfterViewInit() {
214
    const map = await this.mapContainer.openlayersMap
52✔
215
    prioritizePageScroll(map.getInteractions())
52✔
216
  }
217

218
  onMapFeatureSelect(features: Feature[]): void {
219
    this.resetSelection()
3✔
220
    this.selection = features?.length > 0 && features[0]
3!
221
    if (this.selection) {
3✔
222
      // FIXME: restore styling of selected feature
223
      // this.selection.setStyle(this.selectionStyle)
224
    }
225
    this.changeRef.detectChanges()
3✔
226
  }
227

228
  onSourceLoadError(error: SourceLoadErrorEvent) {
229
    if (error.httpStatus === 403 || error.httpStatus === 401) {
3✔
230
      this.error = this.translateService.instant(`dataset.error.forbidden`)
2✔
231
    } else {
232
      this.error = this.translateService.instant(`dataset.error.http`, {
1✔
233
        info: error.httpStatus,
234
      })
235
    }
236
  }
237

238
  resetSelection(): void {
239
    if (this.selection) {
40✔
240
      // FIXME: restore styling of selected feature
241
      // this.selection.setStyle(null)
242
    }
243
    this.selection = null
40✔
244
  }
245

246
  getLayerFromLink(link: DatasetOnlineResource): Observable<MapContextLayer> {
247
    if (link.type === 'service' && link.accessServiceProtocol === 'wms') {
35✔
248
      return of({
21✔
249
        url: link.url.toString(),
250
        type: 'wms',
251
        name: link.name,
252
      })
253
    } else if (
14✔
254
      link.type === 'service' &&
24✔
255
      link.accessServiceProtocol === 'tms'
256
    ) {
257
      // FIXME: here we're assuming that the TMS serves vector tiles only; should be checked with ogc-client first
258
      return of({
1✔
259
        url: link.url.toString().replace(/\/?$/, '/{z}/{x}/{y}.pbf'),
260
        type: 'xyz',
261
        tileFormat: 'application/vnd.mapbox-vector-tile',
262
        name: link.name,
263
      })
264
    } else if (
13✔
265
      link.type === 'service' &&
22✔
266
      link.accessServiceProtocol === 'maplibre-style'
267
    ) {
268
      return of({
1✔
269
        type: 'maplibre-style',
270
        name: link.name,
271
        styleUrl: link.url.toString(),
272
      })
273
    } else if (
12✔
274
      link.type === 'service' &&
20✔
275
      link.accessServiceProtocol === 'wmts'
276
    ) {
277
      return of({
1✔
278
        url: link.url.toString(),
279
        type: 'wmts',
280
        name: link.name,
281
      })
282
    } else if (
11✔
283
      (link.type === 'service' &&
27✔
284
        (link.accessServiceProtocol === 'wfs' ||
285
          link.accessServiceProtocol === 'esriRest' ||
286
          link.accessServiceProtocol === 'ogcFeatures')) ||
287
      link.type === 'download'
288
    ) {
289
      const cacheActive = true // TODO implement whether should be true or false
11✔
290
      return this.dataService.readAsGeoJson(link, cacheActive).pipe(
11✔
291
        map((data) => ({
6✔
292
          type: 'geojson',
293
          data,
294
        }))
295
      )
296
    }
UNCOV
297
    return throwError(() => 'protocol not supported')
×
298
  }
299

300
  selectLinkToDisplay(link: number) {
301
    this.selectedLinkIndex$.next(link)
13✔
302
  }
303

304
  handleError(error: FetchError | Error | string) {
305
    if (error instanceof FetchError) {
7✔
306
      this.error = this.translateService.instant(
1✔
307
        `dataset.error.${error.type}`,
308
        {
309
          info: error.info,
310
        }
311
      )
312
      console.warn(error.message)
1✔
313
    } else if (error instanceof Error) {
6✔
314
      this.error = this.translateService.instant(error.message)
4✔
315
      console.warn(error.stack || error)
4!
316
    } else {
317
      this.error = this.translateService.instant(error)
2✔
318
      console.warn(error)
2✔
319
    }
320
    this.loading = false
7✔
321
    this.changeRef.detectChanges()
7✔
322
  }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc