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

geonetwork / geonetwork-ui / 15462883404

05 Jun 2025 08:58AM UTC coverage: 83.925% (-0.4%) from 84.332%
15462883404

Pull #1259

github

web-flow
Merge dee23520e into 320c95c81
Pull Request #1259: [Datahub] FIX handle error (display msg) also for TMS

796 of 1077 branches covered (73.91%)

Branch coverage included in aggregate %.

10 of 11 new or added lines in 4 files covered. (90.91%)

11 existing lines in 3 files now uncovered.

2112 of 2388 relevant lines covered (88.44%)

9.03 hits per line

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

94.26
/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 {
33
  DatasetOnlineResource,
34
  DatasetServiceDistribution,
35
} from '@geonetwork-ui/common/domain/model/record'
36
import {
1✔
37
  createViewFromLayer,
38
  MapContext,
39
  MapContextLayer,
40
  SourceLoadErrorEvent,
41
} from '@geospatial-sdk/core'
42
import {
1✔
43
  FeatureDetailComponent,
44
  MapContainerComponent,
45
  MapLegendComponent,
46
  prioritizePageScroll,
47
} from '@geonetwork-ui/ui/map'
48
import { Feature } from 'geojson'
49
import { NgIconComponent, provideIcons } from '@ng-icons/core'
1✔
50
import { matClose } from '@ng-icons/material-icons/baseline'
1✔
51
import { CommonModule } from '@angular/common'
1✔
52
import {
1✔
53
  ButtonComponent,
54
  DropdownSelectorComponent,
55
} from '@geonetwork-ui/ui/inputs'
56
import { TranslateModule, TranslateService } from '@ngx-translate/core'
1✔
57
import { ExternalViewerButtonComponent } from '../external-viewer-button/external-viewer-button.component'
1✔
58
import {
1✔
59
  LoadingMaskComponent,
60
  PopupAlertComponent,
61
} from '@geonetwork-ui/ui/widgets'
62
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
1✔
63
import { FetchError } from '@geonetwork-ui/data-fetcher'
1✔
64

65
marker('map.dropdown.placeholder')
1✔
66
marker('wfs.feature.limit')
1✔
67
marker('dataset.error.restrictedAccess')
1✔
68
marker('map.select.style')
1✔
69

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

98
  excludeWfs$ = new BehaviorSubject(false)
57✔
99
  hidePreview = false
57✔
100
  selection: Feature
101
  showLegend = true
57✔
102
  legendExists = false
57✔
103
  loading = false
57✔
104
  error = null
57✔
105

106
  selectLinkToDisplay(i: number) {
107
    this.selectedLinkIndex$.next(i)
15✔
108
  }
109

110
  selectStyleToDisplay(i: number) {
111
    this.selectedStyleIndex$.next(i)
1✔
112
  }
113

114
  toggleLegend() {
115
    this.showLegend = !this.showLegend
×
116
  }
117
  onLegendStatusChange(v: boolean) {
118
    this.legendExists = v
1✔
119
  }
120

121
  compatibleMapLinks$ = combineLatest([
57✔
122
    this.mdViewFacade.mapApiLinks$,
123
    this.mdViewFacade.geoDataLinksWithGeometry$,
124
  ]).pipe(
125
    map(([mapApiLinks, geoDataLinksWithGeometry]) => [
49✔
126
      ...mapApiLinks,
127
      ...geoDataLinksWithGeometry,
128
    ]),
129
    shareReplay(1)
130
  )
131

132
  dropdownChoices$ = this.compatibleMapLinks$.pipe(
57✔
133
    map((links) =>
134
      links.length
49✔
135
        ? links.map((link, index) => ({
85✔
136
            label: getLinkLabel(link),
137
            value: index,
138
          }))
139
        : [{ label: 'map.dropdown.placeholder', value: 0 }]
140
    )
141
  )
142

143
  private selectedLinkIndex$ = new BehaviorSubject(0)
57✔
144
  private selectedStyleIndex$ = new BehaviorSubject(0)
57✔
145

146
  selectedSourceLink$ = combineLatest([
57✔
147
    this.compatibleMapLinks$,
148
    this.selectedLinkIndex$.pipe(distinctUntilChanged()),
149
  ]).pipe(
150
    tap(() => {
151
      this.error = null
64✔
152
    }),
153
    map(([links, idx]) => links[idx]),
64✔
154
    shareReplay(1)
155
  )
156

157
  styleLinks$ = this.selectedSourceLink$.pipe(
57✔
158
    switchMap((src) => {
159
      if (
64✔
160
        src &&
177✔
161
        src.type === 'service' &&
162
        src.accessServiceProtocol === 'tms'
163
      ) {
164
        return from(
6✔
165
          this.dataService.getGeodataLinksFromTms(
166
            src as DatasetServiceDistribution,
167
            false
168
          )
169
        ).pipe(
170
          // We need to check for maplibre-style links because when a TMS service has no styles,
171
          // getGeodataLinksFromTms returns the original TMS link, which isn't a maplibre-style link
172
          map(
173
            (links) =>
174
              links?.filter(
4!
175
                (link) =>
176
                  link.type === 'service' &&
10✔
177
                  link.accessServiceProtocol === 'maplibre-style'
178
              ) || []
179
          ),
180
          catchError((error) => {
181
            this.handleError(error)
2✔
182
            return of(src)
2✔
183
          })
184
        )
185
      }
186
      return of([])
58✔
187
    }),
188
    tap(() => this.selectedStyleIndex$.next(0)),
64✔
189
    shareReplay(1)
190
  )
191

192
  styleDropdownChoices$ = this.styleLinks$.pipe(
57✔
193
    map((links) =>
194
      links.length
68✔
195
        ? links.map((link, index) => ({
12✔
196
            label: getLinkLabel(link),
197
            value: index,
198
          }))
199
        : [
200
            {
201
              label: '\u00A0\u00A0\u00A0\u00A0',
202
              value: 0,
203
            },
204
          ]
205
    )
206
  )
207

208
  selectedLink$ = combineLatest([
57✔
209
    this.selectedSourceLink$,
210
    this.styleLinks$,
211
    this.selectedStyleIndex$.pipe(distinctUntilChanged()),
212
  ]).pipe(
213
    map(([src, styles, styleIdx]) => (styles.length ? styles[styleIdx] : src)),
87✔
214
    shareReplay(1)
215
  )
216

217
  currentLayers$ = combineLatest([this.selectedLink$, this.excludeWfs$]).pipe(
57✔
218
    switchMap(([link, excludeWfs]) => {
219
      if (!link) {
89✔
220
        return of([])
4✔
221
      }
222
      if (excludeWfs && link.accessServiceProtocol === 'wfs') {
85✔
223
        this.hidePreview = true
4✔
224
        return of([])
4✔
225
      }
226
      this.hidePreview = false
81✔
227
      this.loading = true
81✔
228
      if (link.accessRestricted) {
81✔
229
        this.handleError('dataset.error.restrictedAccess')
1✔
230
        return of([])
1✔
231
      }
232
      return this.getLayerFromLink(link).pipe(
80✔
233
        map((layer) => [layer]),
69✔
234
        catchError((e) => {
235
          this.handleError(e)
3✔
236
          return of([])
3✔
237
        }),
238
        finalize(() => (this.loading = false))
78✔
239
      )
240
    })
241
  )
242

243
  mapContext$: Observable<MapContext> = this.currentLayers$.pipe(
57✔
244
    switchMap((layers) =>
245
      from(createViewFromLayer(layers[0])).pipe(
81✔
246
        catchError(() => of(null)), // could not zoom on the layer: use the record extent
2✔
247
        map((view) => ({
44✔
248
          layers,
249
          view,
250
        })),
251
        tap(() => {
252
          this.resetSelection()
44✔
253
        })
254
      )
255
    ),
256
    startWith({
257
      layers: [],
258
      view: null,
259
    }),
260
    withLatestFrom(this.mdViewFacade.metadata$),
261
    map(([context, metadata]) => {
262
      if (context.view) return context
101✔
263
      const extent = this.mapUtils.getRecordExtent(metadata)
98✔
264
      const view = extent ? { extent } : null
98✔
265
      return {
98✔
266
        ...context,
267
        view,
268
      }
269
    }),
270
    shareReplay(1)
271
  )
272

273
  constructor(
274
    private mdViewFacade: MdViewFacade,
57!
275
    private mapUtils: MapUtilsService,
57!
276
    private dataService: DataService,
57!
277
    private changeRef: ChangeDetectorRef,
57!
278
    private translateService: TranslateService
57✔
279
  ) {}
280

281
  async ngAfterViewInit() {
282
    const map = await this.mapContainer.openlayersMap
57✔
283
    prioritizePageScroll(map.getInteractions())
57✔
284
  }
285

286
  onMapFeatureSelect(features: Feature[]): void {
287
    this.resetSelection()
3✔
288
    this.selection = features?.length > 0 && features[0]
3!
289
    if (this.selection) {
3✔
290
      // FIXME: restore styling of selected feature
291
      // this.selection.setStyle(this.selectionStyle)
292
    }
293
    this.changeRef.detectChanges()
3✔
294
  }
295

296
  onSourceLoadError(error: SourceLoadErrorEvent) {
297
    if (error.httpStatus === 403 || error.httpStatus === 401) {
3✔
298
      this.error = this.translateService.instant(`dataset.error.forbidden`)
2✔
299
    } else {
300
      this.error = this.translateService.instant(`dataset.error.http`, {
1✔
301
        info: error.httpStatus,
302
      })
303
    }
304
  }
305

306
  resetSelection(): void {
307
    if (this.selection) {
48✔
308
      // FIXME: restore styling of selected feature
309
      // this.selection.setStyle(null)
310
    }
311
    this.selection = null
48✔
312
  }
313

314
  getLayerFromLink(link: DatasetOnlineResource): Observable<MapContextLayer> {
315
    if (link.type === 'service' && link.accessServiceProtocol === 'wms') {
80✔
316
      return of({
51✔
317
        url: link.url.toString(),
318
        type: 'wms',
319
        name: link.name,
320
      })
321
    } else if (
29✔
322
      link.type === 'service' &&
48✔
323
      link.accessServiceProtocol === 'tms'
324
    ) {
325
      // FIXME: here we're assuming that the TMS serves vector tiles only; should be checked with ogc-client first
326
      return of({
5✔
327
        url: link.url.toString().replace(/\/?$/, '/{z}/{x}/{y}.pbf'),
328
        type: 'xyz',
329
        tileFormat: 'application/vnd.mapbox-vector-tile',
330
        name: link.name,
331
      })
332
    } else if (
24✔
333
      link.type === 'service' &&
38✔
334
      link.accessServiceProtocol === 'maplibre-style'
335
    ) {
336
      return of({
6✔
337
        type: 'maplibre-style',
338
        name: link.name,
339
        styleUrl: link.url.toString(),
340
      })
341
    } else if (
18✔
342
      link.type === 'service' &&
26✔
343
      link.accessServiceProtocol === 'wmts'
344
    ) {
345
      return of({
1✔
346
        url: link.url.toString(),
347
        type: 'wmts',
348
        name: link.name,
349
      })
350
    } else if (
17✔
351
      (link.type === 'service' &&
39✔
352
        (link.accessServiceProtocol === 'wfs' ||
353
          link.accessServiceProtocol === 'esriRest' ||
354
          link.accessServiceProtocol === 'ogcFeatures')) ||
355
      link.type === 'download'
356
    ) {
357
      const cacheActive = true // TODO implement whether should be true or false
17✔
358
      return this.dataService.readAsGeoJson(link, cacheActive).pipe(
17✔
359
        map((data) => ({
6✔
360
          type: 'geojson',
361
          data,
362
        }))
363
      )
364
    }
UNCOV
365
    return throwError(() => 'protocol not supported')
×
366
  }
367
  handleError(error: FetchError | Error | string) {
368
    if (error instanceof FetchError) {
9✔
369
      this.error = this.translateService.instant(
1✔
370
        `dataset.error.${error.type}`,
371
        {
372
          info: error.info,
373
        }
374
      )
375
      console.warn(error.message)
1✔
376
    } else if (error instanceof Error) {
8✔
377
      this.error = this.translateService.instant(error.message)
5✔
378
      console.warn(error.stack || error)
5!
379
    } else {
380
      this.error = this.translateService.instant(error)
3✔
381
      console.warn(error)
3✔
382
    }
383
    this.loading = false
9✔
384
    this.changeRef.detectChanges()
9✔
385
  }
386
}
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