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

geonetwork / geonetwork-ui / 14268286450

04 Apr 2025 03:02PM UTC coverage: 84.275% (+2.5%) from 81.764%
14268286450

Pull #1193

github

web-flow
Merge 5df097990 into 4fa0a0afc
Pull Request #1193: Section subsection rework

1587 of 2126 branches covered (74.65%)

Branch coverage included in aggregate %.

5080 of 5785 relevant lines covered (87.81%)

10.3 hits per line

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

94.19
/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

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

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

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

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

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

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

128
  loading = false
49✔
129
  error = null
49✔
130

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

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

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

189
  constructor(
190
    private mdViewFacade: MdViewFacade,
49!
191
    private mapUtils: MapUtilsService,
49!
192
    private dataService: DataService,
49!
193
    private changeRef: ChangeDetectorRef,
49!
194
    private translateService: TranslateService
49✔
195
  ) {}
196

197
  async ngAfterViewInit() {
198
    const map = await this.mapContainer.openlayersMap
49✔
199
    prioritizePageScroll(map.getInteractions())
49✔
200
  }
201

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

212
  onSourceLoadError(error: SourceLoadErrorEvent) {
213
    if (error.httpStatus === 403 || error.httpStatus === 401) {
3✔
214
      this.error = this.translateService.instant(`dataset.error.forbidden`)
2✔
215
    } else {
216
      this.error = this.translateService.instant(`dataset.error.http`, {
1✔
217
        info: error.httpStatus,
218
      })
219
    }
220
  }
221

222
  resetSelection(): void {
223
    if (this.selection) {
38✔
224
      // FIXME: restore styling of selected feature
225
      // this.selection.setStyle(null)
226
    }
227
    this.selection = null
38✔
228
  }
229

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

264
  selectLinkToDisplay(link: number) {
265
    this.selectedLinkIndex$.next(link)
11✔
266
  }
267

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