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

geonetwork / geonetwork-ui / 14994093986

13 May 2025 10:16AM UTC coverage: 85.0% (+0.9%) from 84.075%
14994093986

Pull #1235

github

web-flow
Merge 81102c823 into e052ad8ba
Pull Request #1235: Metadata Editor: Converter read fixes

2611 of 3426 branches covered (76.21%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

13 existing lines in 4 files now uncovered.

7130 of 8034 relevant lines covered (88.75%)

367.03 hits per line

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

94.62
/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
          console.error(e)
3✔
168
          this.handleError(e)
3✔
169
          return of([])
3✔
170
        }),
171
        finalize(() => (this.loading = false))
33✔
172
      )
173
    })
174
  )
175

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

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

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

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

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

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

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

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

305
  handleError(error: FetchError | Error | string) {
306
    if (error instanceof FetchError) {
7✔
307
      this.error = this.translateService.instant(
1✔
308
        `dataset.error.${error.type}`,
309
        {
310
          info: error.info,
311
        }
312
      )
313
      console.warn(error.message)
1✔
314
    } else if (error instanceof Error) {
6✔
315
      this.error = this.translateService.instant(error.message)
4✔
316
      console.warn(error.stack || error)
4!
317
    } else {
318
      this.error = this.translateService.instant(error)
2✔
319
      console.warn(error)
2✔
320
    }
321
    this.loading = false
7✔
322
    this.changeRef.detectChanges()
7✔
323
  }
324
}
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