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

geonetwork / geonetwork-ui / 14377612187

10 Apr 2025 09:56AM UTC coverage: 84.313% (-0.1%) from 84.432%
14377612187

Pull #1214

github

web-flow
Merge 727560b5a into 582b4530a
Pull Request #1214: [Datahub]: Reuse page

3246 of 4317 branches covered (75.19%)

Branch coverage included in aggregate %.

137 of 166 new or added lines in 27 files covered. (82.53%)

9 existing lines in 6 files now uncovered.

9395 of 10676 relevant lines covered (88.0%)

277.59 hits per line

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

94.38
/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
  prioritizePageScroll,
43
  MapLegendComponent,
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) {
UNCOV
89
    this.excludeWfs$.next(value)
×
90
  }
91
  @Input() displaySource = true
50✔
92
  @ViewChild('mapContainer') mapContainer: MapContainerComponent
93

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

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

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

108
  compatibleMapLinks$ = combineLatest([
50✔
109
    this.mdViewFacade.mapApiLinks$,
110
    this.mdViewFacade.geoDataLinksWithGeometry$,
111
  ]).pipe(
112
    map(([mapApiLinks, geoDataLinksWithGeometry]) => {
113
      return [...mapApiLinks, ...geoDataLinksWithGeometry]
126✔
114
    })
115
  )
116

117
  dropdownChoices$ = this.compatibleMapLinks$.pipe(
50✔
118
    map((links) =>
119
      links.length
42✔
120
        ? links.map((link, index) => ({
77✔
121
            label: getLinkLabel(link),
122
            value: index,
123
          }))
124
        : [{ label: 'map.dropdown.placeholder', value: 0 }]
125
    )
126
  )
127
  selectedLinkIndex$ = new BehaviorSubject(0)
50✔
128

129
  loading = false
50✔
130
  error = null
50✔
131

132
  selectedLink$ = combineLatest([
50✔
133
    this.compatibleMapLinks$,
134
    this.selectedLinkIndex$.pipe(distinctUntilChanged()),
135
  ]).pipe(map(([links, index]) => links[index]))
110✔
136

137
  currentLayers$ = combineLatest([this.selectedLink$, this.excludeWfs$]).pipe(
50✔
138
    switchMap(([link, excludeWfs]) => {
139
      if (!link) {
57✔
140
        return of([])
4✔
141
      }
142
      if (excludeWfs && link.accessServiceProtocol === 'wfs') {
53✔
143
        this.hidePreview = true
4✔
144
        return of([])
4✔
145
      }
146
      this.hidePreview = false
49✔
147
      this.loading = true
49✔
148
      this.error = null
49✔
149
      if (link.accessRestricted) {
49✔
150
        this.handleError('dataset.error.restrictedAccess')
1✔
151
        return of([])
1✔
152
      }
153
      return this.getLayerFromLink(link).pipe(
48✔
154
        map((layer) => [layer]),
40✔
155
        catchError((e) => {
156
          this.handleError(e)
3✔
157
          return of([])
3✔
158
        }),
159
        finalize(() => (this.loading = false))
46✔
160
      )
161
    })
162
  )
163

164
  mapContext$: Observable<MapContext> = this.currentLayers$.pipe(
50✔
165
    switchMap((layers) =>
166
      from(createViewFromLayer(layers[0])).pipe(
52✔
167
        catchError(() => of(null)), // could not zoom on the layer: use the record extent
2✔
168
        map((view) => ({
34✔
169
          layers,
170
          view,
171
        })),
172
        tap(() => {
173
          this.resetSelection()
34✔
174
        })
175
      )
176
    ),
177
    startWith({
178
      layers: [],
179
      view: null,
180
    }),
181
    withLatestFrom(this.mdViewFacade.metadata$),
182
    map(([context, metadata]) => {
183
      if (context.view) return context
84✔
184
      const extent = this.mapUtils.getRecordExtent(metadata)
81✔
185
      const view = extent ? { extent } : null
81✔
186
      return {
81✔
187
        ...context,
188
        view,
189
      }
190
    }),
191
    shareReplay(1)
192
  )
193

194
  constructor(
195
    private mdViewFacade: MdViewFacade,
50!
196
    private mapUtils: MapUtilsService,
50!
197
    private dataService: DataService,
50!
198
    private changeRef: ChangeDetectorRef,
50!
199
    private translateService: TranslateService
50✔
200
  ) {}
201

202
  async ngAfterViewInit() {
203
    const map = await this.mapContainer.openlayersMap
50✔
204
    prioritizePageScroll(map.getInteractions())
50✔
205
  }
206

207
  onMapFeatureSelect(features: Feature[]): void {
208
    this.resetSelection()
3✔
209
    this.selection = features?.length > 0 && features[0]
3✔
210
    if (this.selection) {
3✔
211
      // FIXME: restore styling of selected feature
212
      // this.selection.setStyle(this.selectionStyle)
213
    }
214
    this.changeRef.detectChanges()
3✔
215
  }
216

217
  onSourceLoadError(error: SourceLoadErrorEvent) {
218
    if (error.httpStatus === 403 || error.httpStatus === 401) {
3✔
219
      this.error = this.translateService.instant(`dataset.error.forbidden`)
2✔
220
    } else {
221
      this.error = this.translateService.instant(`dataset.error.http`, {
1✔
222
        info: error.httpStatus,
223
      })
224
    }
225
  }
226

227
  resetSelection(): void {
228
    if (this.selection) {
38✔
229
      // FIXME: restore styling of selected feature
230
      // this.selection.setStyle(null)
231
    }
232
    this.selection = null
38✔
233
  }
234

235
  getLayerFromLink(link: DatasetOnlineResource): Observable<MapContextLayer> {
236
    if (link.type === 'service' && link.accessServiceProtocol === 'wms') {
48✔
237
      return of({
33✔
238
        url: link.url.toString(),
239
        type: 'wms',
240
        name: link.name,
241
      })
242
    } else if (
15✔
243
      link.type === 'service' &&
23✔
244
      link.accessServiceProtocol === 'wmts'
245
    ) {
246
      return of({
1✔
247
        url: link.url.toString(),
248
        type: 'wmts',
249
        name: link.name,
250
      })
251
    } else if (
14✔
252
      (link.type === 'service' &&
33✔
253
        (link.accessServiceProtocol === 'wfs' ||
254
          link.accessServiceProtocol === 'esriRest' ||
255
          link.accessServiceProtocol === 'ogcFeatures')) ||
256
      link.type === 'download'
257
    ) {
258
      const cacheActive = true // TODO implement whether should be true or false
14✔
259
      return this.dataService.readAsGeoJson(link, cacheActive).pipe(
14✔
260
        map((data) => ({
6✔
261
          type: 'geojson',
262
          data,
263
        }))
264
      )
265
    }
UNCOV
266
    return throwError(() => 'protocol not supported')
×
267
  }
268

269
  selectLinkToDisplay(link: number) {
270
    this.selectedLinkIndex$.next(link)
13✔
271
  }
272

273
  handleError(error: FetchError | Error | string) {
274
    if (error instanceof FetchError) {
7✔
275
      this.error = this.translateService.instant(
1✔
276
        `dataset.error.${error.type}`,
277
        {
278
          info: error.info,
279
        }
280
      )
281
      console.warn(error.message)
1✔
282
    } else if (error instanceof Error) {
6✔
283
      this.error = this.translateService.instant(error.message)
4✔
284
      console.warn(error.stack || error)
4!
285
    } else {
286
      this.error = this.translateService.instant(error)
2✔
287
      console.warn(error)
2✔
288
    }
289
    this.loading = false
7✔
290
    this.changeRef.detectChanges()
7✔
291
  }
292
}
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