diff --git a/libs/feature/map/src/lib/manager/map-instance.directive.ts b/libs/feature/map/src/lib/manager/map-instance.directive.ts deleted file mode 100644 index 692ad3bb57..0000000000 --- a/libs/feature/map/src/lib/manager/map-instance.directive.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Directive } from '@angular/core' -import { MapManagerService } from './map-manager.service' - -@Directive({ - selector: '[gnUiMapContainer]', - providers: [MapManagerService], -}) -export class MapInstanceDirective { - constructor(private manager: MapManagerService) {} -} diff --git a/libs/feature/map/src/lib/manager/map-manager.service.spec.ts b/libs/feature/map/src/lib/manager/map-manager.service.spec.ts deleted file mode 100644 index cab187c00e..0000000000 --- a/libs/feature/map/src/lib/manager/map-manager.service.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TestBed } from '@angular/core/testing' -import { MapUtilsService } from '../utils/map-utils.service' - -import { MapManagerService } from './map-manager.service' - -const mapUtilsServiceMock = { - createEmptyMap: jest.fn(), -} -describe('MapManagerService', () => { - let service: MapManagerService - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - { - provide: MapUtilsService, - useValue: mapUtilsServiceMock, - }, - ], - }) - service = TestBed.inject(MapManagerService) - }) - - it('should be created', () => { - expect(service).toBeTruthy() - }) - - it('instanciate an empty map', () => { - expect(mapUtilsServiceMock.createEmptyMap).toHaveBeenCalled() - }) -}) diff --git a/libs/feature/map/src/lib/manager/map-manager.service.ts b/libs/feature/map/src/lib/manager/map-manager.service.ts deleted file mode 100644 index d84a068909..0000000000 --- a/libs/feature/map/src/lib/manager/map-manager.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Injectable } from '@angular/core' -import Map from 'ol/Map' -import { MapUtilsService } from '../utils/map-utils.service' - -@Injectable({ - providedIn: 'root', -}) -export class MapManagerService { - readonly map: Map - constructor(private utils: MapUtilsService) { - this.map = this.utils.createEmptyMap() - } -} diff --git a/libs/feature/map/src/lib/map-container/map-container.component.css b/libs/feature/map/src/lib/map-container/map-container.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/feature/map/src/lib/map-container/map-container.component.html b/libs/feature/map/src/lib/map-container/map-container.component.html deleted file mode 100644 index 5fe34f6eb5..0000000000 --- a/libs/feature/map/src/lib/map-container/map-container.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/feature/map/src/lib/map-container/map-container.component.spec.ts b/libs/feature/map/src/lib/map-container/map-container.component.spec.ts deleted file mode 100644 index 74d23e513d..0000000000 --- a/libs/feature/map/src/lib/map-container/map-container.component.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MapContainerComponent } from './map-container.component' -import { MapFacade } from '../+state/map.facade' -import { of } from 'rxjs' -import { mapCtxLayerXyzFixture } from '../map-context/map-context.fixtures' -import { readFirst } from '@nx/angular/testing' -import { DEFAULT_BASELAYER_CONTEXT } from '../map-context/map-context.service' -import { NO_ERRORS_SCHEMA } from '@angular/core' - -class MapFacadeMock { - layers$ = of([mapCtxLayerXyzFixture()]) -} - -describe('MapContainerComponent', () => { - let component: MapContainerComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [MapContainerComponent], - providers: [ - { - provide: MapFacade, - useClass: MapFacadeMock, - }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents() - - fixture = TestBed.createComponent(MapContainerComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - describe('context$', () => { - it('emits a context containing the layers in the map', async () => { - const context = await readFirst(component.context$) - expect(context).toStrictEqual({ - layers: [DEFAULT_BASELAYER_CONTEXT, mapCtxLayerXyzFixture()], - view: { - center: expect.any(Array), - zoom: expect.any(Number), - }, - }) - }) - }) -}) diff --git a/libs/feature/map/src/lib/map-container/map-container.component.ts b/libs/feature/map/src/lib/map-container/map-container.component.ts deleted file mode 100644 index 8b9faf42db..0000000000 --- a/libs/feature/map/src/lib/map-container/map-container.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' -import { MapFacade } from '../+state/map.facade' -import { MapContextModel } from '../map-context/map-context.model' -import { DEFAULT_BASELAYER_CONTEXT } from '../map-context/map-context.service' - -@Component({ - selector: 'gn-ui-map-container', - templateUrl: './map-container.component.html', - styleUrls: ['./map-container.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MapContainerComponent { - context$: Observable = this.mapFacade.layers$.pipe( - map((layers) => ({ - view: { - center: [4, 42], - zoom: 6, - }, - layers: [DEFAULT_BASELAYER_CONTEXT, ...layers], - })) - ) - - constructor(private mapFacade: MapFacade) {} -} diff --git a/libs/feature/map/src/lib/map-context/component/map-context.component.css b/libs/feature/map/src/lib/map-context/component/map-context.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/feature/map/src/lib/map-context/component/map-context.component.html b/libs/feature/map/src/lib/map-context/component/map-context.component.html deleted file mode 100644 index 84fd91619e..0000000000 --- a/libs/feature/map/src/lib/map-context/component/map-context.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/feature/map/src/lib/map-context/component/map-context.component.spec.ts b/libs/feature/map/src/lib/map-context/component/map-context.component.spec.ts deleted file mode 100644 index 9d5e7f3270..0000000000 --- a/libs/feature/map/src/lib/map-context/component/map-context.component.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MapManagerService } from '../../manager/map-manager.service' -import { mapCtxFixture } from '../map-context.fixtures' -import { MapContextService } from '../map-context.service' - -import { MapContextComponent } from './map-context.component' -import { mapConfigFixture } from '@geonetwork-ui/util/app-config' -import { HttpClientModule } from '@angular/common/http' -import { MapUtilsService } from '../../utils' - -class MapContextServiceMock { - resetMapFromContext = jest.fn() -} - -class MapUtilsServiceMock { - prioritizePageScroll = jest.fn() -} - -let resizeCallBack -class OpenLayersMapMock { - _size = undefined - once(type, callback) { - if (type === 'change:size') { - resizeCallBack = callback - } - } - updateSize() { - this._size = [100, 100] - } - getSize() { - return this._size - } -} - -class MapManagerMock { - map = new OpenLayersMapMock() -} - -describe('MapContextComponent', () => { - let component: MapContextComponent - let fixture: ComponentFixture - let mapContextService - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [MapContextComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [HttpClientModule], - providers: [ - { - provide: MapContextService, - useClass: MapContextServiceMock, - }, - { - provide: MapUtilsService, - useClass: MapUtilsServiceMock, - }, - { - provide: MapManagerService, - useClass: MapManagerMock, - }, - ], - }).compileComponents() - mapContextService = TestBed.inject(MapContextService) - }) - - beforeEach(() => { - fixture = TestBed.createComponent(MapContextComponent) - component = fixture.componentInstance - }) - - it('should create', () => { - fixture.detectChanges() - expect(component).toBeTruthy() - }) - - describe('with initial value', () => { - beforeEach(() => { - component.context = mapCtxFixture() - component.mapConfig = mapConfigFixture() - fixture.detectChanges() - component.ngOnChanges({}) - }) - it('reset the map from context', () => { - expect(mapContextService.resetMapFromContext).toHaveBeenCalledWith( - expect.any(OpenLayersMapMock), - mapCtxFixture(), - mapConfigFixture() - ) - }) - }) - - describe('no initial value', () => { - beforeEach(() => { - component.context = null - fixture.detectChanges() - component.ngOnChanges({}) - }) - it('does not reset the map', () => { - expect(mapContextService.resetMapFromContext).not.toHaveBeenCalled() - }) - }) - - describe('no initial value, two values afterwards', () => { - beforeEach(() => { - component.context = null - component.mapConfig = mapConfigFixture() - fixture.detectChanges() - component.ngOnChanges({}) - component.context = { ...mapCtxFixture() } - component.ngOnChanges({}) - component.context = { ...mapCtxFixture() } - component.ngOnChanges({}) - }) - it('reset the map from context twice', () => { - expect(mapContextService.resetMapFromContext).toHaveBeenCalledWith( - expect.any(OpenLayersMapMock), - mapCtxFixture(), - mapConfigFixture() - ) - expect(mapContextService.resetMapFromContext).toHaveBeenCalledTimes(2) - }) - }) - describe('mapContext with extent', () => { - const MAP_CTX_EXTENT = { - ...mapCtxFixture(), - view: { - extent: [-100, -200, 300, 400], - }, - } - - describe('initial context is provided', () => { - describe('before change detection and when map has no size', () => { - beforeEach(() => { - component.context = MAP_CTX_EXTENT - }) - it('does not reset the map', () => { - expect(mapContextService.resetMapFromContext).not.toHaveBeenCalled() - }) - }) - describe('after change detection and when map has no size', () => { - beforeEach(() => { - component.context = MAP_CTX_EXTENT - component.ngOnChanges({ context: MAP_CTX_EXTENT }) - }) - it('does not reset the map', () => { - expect(mapContextService.resetMapFromContext).not.toHaveBeenCalled() - }) - }) - describe('after change detection and when map has a size', () => { - beforeEach(() => { - component.context = MAP_CTX_EXTENT - component.mapConfig = mapConfigFixture() - component.ngOnChanges({ context: MAP_CTX_EXTENT }) - resizeCallBack() - }) - it('resets the map with a view computed from extent', () => { - expect(mapContextService.resetMapFromContext).toHaveBeenCalledWith( - expect.any(OpenLayersMapMock), - MAP_CTX_EXTENT, - mapConfigFixture() - ) - }) - }) - }) - }) -}) diff --git a/libs/feature/map/src/lib/map-context/component/map-context.component.stories.ts b/libs/feature/map/src/lib/map-context/component/map-context.component.stories.ts deleted file mode 100644 index 74d5aad9c6..0000000000 --- a/libs/feature/map/src/lib/map-context/component/map-context.component.stories.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { HttpClientModule } from '@angular/common/http' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { UiMapModule } from '@geonetwork-ui/ui/map' -import { - applicationConfig, - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { - mapCtxLayerGeojsonFixture, - mapCtxLayerWmsFixture, - mapCtxLayerXyzFixture, -} from '../map-context.fixtures' -import { MapContextComponent } from './map-context.component' -import { importProvidersFrom } from '@angular/core' - -export default { - title: 'Map/MapContext', - component: MapContextComponent, - decorators: [ - moduleMetadata({ - imports: [UiMapModule], - }), - applicationConfig({ - providers: [ - importProvidersFrom(BrowserAnimationsModule), - importProvidersFrom(HttpClientModule), - ], - }), - componentWrapperDecorator( - (story) => `
${story}
` - ), - ], -} as Meta - -type Story = StoryObj -export const WMS: Story = { - args: { - context: { - layers: [mapCtxLayerXyzFixture(), mapCtxLayerWmsFixture()], - view: { - center: [7.75, 48.6], - zoom: 4, - }, - }, - }, -} - -export const GEOJSON: Story = { - args: { - context: { - layers: [mapCtxLayerXyzFixture(), mapCtxLayerGeojsonFixture()], - view: { - center: [7.75, 48.6], - zoom: 4, - }, - }, - }, -} diff --git a/libs/feature/map/src/lib/map-context/component/map-context.component.ts b/libs/feature/map/src/lib/map-context/component/map-context.component.ts deleted file mode 100644 index f96eafdd19..0000000000 --- a/libs/feature/map/src/lib/map-context/component/map-context.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - Output, -} from '@angular/core' -import { MapUtilsService } from '../../utils/map-utils.service' -import Feature from 'ol/Feature' -import { Geometry } from 'ol/geom' - -import Map from 'ol/Map' -import { FeatureInfoService } from '../../feature-info/feature-info.service' -import { MapManagerService } from '../../manager/map-manager.service' -import { MapContextModel } from '../map-context.model' -import { MapContextService } from '../map-context.service' -import { MapConfig } from '@geonetwork-ui/util/app-config' - -@Component({ - selector: 'gn-ui-map-context', - templateUrl: './map-context.component.html', - styleUrls: ['./map-context.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MapContextComponent implements OnChanges { - @Input() context: MapContextModel - @Input() mapConfig: MapConfig - @Output() featureClicked = new EventEmitter[]>() - - map: Map - - constructor( - private service: MapContextService, - private featureInfo: FeatureInfoService, - private manager: MapManagerService, - private utils: MapUtilsService - ) { - this.map = manager.map - } - - ngOnChanges() { - if (this.context?.view) { - if (this.context.view.extent && !this.map.getSize()) { - this.map.once('change:size', () => { - this.service.resetMapFromContext( - this.map, - this.context, - this.mapConfig - ) - }) - } else { - this.service.resetMapFromContext(this.map, this.context, this.mapConfig) - } - } - } -} diff --git a/libs/feature/map/src/lib/map-context/map-context.fixtures.ts b/libs/feature/map/src/lib/map-context/map-context.fixtures.ts deleted file mode 100644 index e4e46666e0..0000000000 --- a/libs/feature/map/src/lib/map-context/map-context.fixtures.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { polygonFeatureCollectionFixture } from '@geonetwork-ui/common/fixtures' -import { Extent } from 'ol/extent' -import { - MapContextLayerGeojsonModel, - MapContextLayerModel, - MapContextLayerTypeEnum, - MapContextModel, - MapContextViewModel, -} from '../map-context/map-context.model' - -export const mapCtxLayerXyzFixture = (): MapContextLayerModel => ({ - type: MapContextLayerTypeEnum.XYZ, - url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png', -}) - -export const mapCtxLayerWmsFixture = (): MapContextLayerModel => ({ - type: MapContextLayerTypeEnum.WMS, - url: 'https://www.geograndest.fr/geoserver/region-grand-est/ows?REQUEST=GetCapabilities&SERVICE=WMS', - name: 'commune_actuelle_3857', -}) - -export const mapCtxLayerWfsFixture = (): MapContextLayerModel => ({ - type: MapContextLayerTypeEnum.WFS, - url: 'https://www.geograndest.fr/geoserver/region-grand-est/ows?REQUEST=GetCapabilities&SERVICE=WFS&VERSION=1.1.0', - name: 'ms:commune_actuelle_3857', -}) - -export const mapCtxLayerGeojsonFixture = (): MapContextLayerGeojsonModel => ({ - type: MapContextLayerTypeEnum.GEOJSON, - data: polygonFeatureCollectionFixture(), -}) - -export const mapCtxLayerGeojsonRemoteFixture = - (): MapContextLayerGeojsonModel => ({ - type: MapContextLayerTypeEnum.GEOJSON, - url: 'https://my.host.com/data/regions.json', - }) - -export const mapCtxViewFixture = (): MapContextViewModel => ({ - center: [7.75, 48.6], - zoom: 9, -}) - -export const mapCtxFixture = (): MapContextModel => ({ - layers: [ - mapCtxLayerXyzFixture(), - mapCtxLayerWmsFixture(), - mapCtxLayerGeojsonFixture(), - ], - view: mapCtxViewFixture(), -}) - -export const mapCtxExtentFixture = (): Extent => [ - 171083.69713494915, 6246047.945419401, 476970.39956295764, 6631079.362882684, -] diff --git a/libs/feature/map/src/lib/map-context/map-context.model.ts b/libs/feature/map/src/lib/map-context/map-context.model.ts deleted file mode 100644 index 0b138243c8..0000000000 --- a/libs/feature/map/src/lib/map-context/map-context.model.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { FeatureCollection } from 'geojson' -import { Coordinate } from 'ol/coordinate' -import type { Extent } from 'ol/extent' - -export enum MapContextLayerTypeEnum { - XYZ = 'xyz', - WMS = 'wms', - WMTS = 'wmts', - WFS = 'wfs', - GEOJSON = 'geojson', - OGCAPI = 'ogcapi', -} - -export interface MapContextModel { - layers: MapContextLayerModel[] - view?: MapContextViewModel -} - -export interface MapContextLayerWmsModel { - type: 'wms' - url: string - name: string - attributions?: string -} - -export interface MapContextLayerWmtsModel { - type: 'wmts' - url: string - name: string - attributions?: string -} - -interface MapContextLayerWfsModel { - type: 'wfs' - url: string - name: string - attributions?: string -} - -export interface MapContextLayerOgcapiModel { - type: 'ogcapi' - url: string - name: string - layerType: 'feature' | 'vectorTiles' | 'mapTiles' | 'record' - attributions?: string -} - -interface LayerXyzModel { - type: 'xyz' - name?: string - attributions?: string -} -interface LayerXyzModelWithUrl extends LayerXyzModel { - url: string - urls?: never -} -interface LayerXyzModelWithUrls extends LayerXyzModel { - urls: string[] - url?: never -} -export type MapContextLayerXyzModel = - | LayerXyzModelWithUrl - | LayerXyzModelWithUrls - -interface LayerGeojson { - type: 'geojson' - attributions?: string -} -interface LayerGeojsonWithUrl extends LayerGeojson { - url: string - data?: never -} -interface LayerGeojsonWithData extends LayerGeojson { - data: FeatureCollection | string - url?: never -} -export type MapContextLayerGeojsonModel = - | LayerGeojsonWithUrl - | LayerGeojsonWithData - -export type MapContextLayerModel = - | MapContextLayerWmsModel - | MapContextLayerWmtsModel - | MapContextLayerWfsModel - | MapContextLayerXyzModel - | MapContextLayerGeojsonModel - | MapContextLayerOgcapiModel - -export interface MapContextViewModel { - center?: Coordinate // expressed in long/lat (EPSG:4326) - zoom?: number - extent?: Extent // expressed in long/lat (EPSG:4326) - maxZoom?: number - maxExtent?: Extent // expressed in long/lat (EPSG:4326) -} diff --git a/libs/feature/map/src/lib/map-context/map-context.service.spec.ts b/libs/feature/map/src/lib/map-context/map-context.service.spec.ts deleted file mode 100644 index 2826f42aaf..0000000000 --- a/libs/feature/map/src/lib/map-context/map-context.service.spec.ts +++ /dev/null @@ -1,525 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing' -import { TestBed } from '@angular/core/testing' -import { MapConfig, mapConfigFixture } from '@geonetwork-ui/util/app-config' -import { FeatureCollection } from 'geojson' -import { Geometry } from 'ol/geom' -import TileLayer from 'ol/layer/Tile' -import VectorLayer from 'ol/layer/Vector' -import Map from 'ol/Map' -import TileWMS from 'ol/source/TileWMS' -import VectorSource from 'ol/source/Vector' -import XYZ from 'ol/source/XYZ' -import { Style } from 'ol/style' -import View from 'ol/View' -import GeoJSON from 'ol/format/GeoJSON' -import { - defaultMapStyleFixture, - defaultMapStyleHlFixture, -} from '../style/map-style.fixtures' -import { MapStyleService } from '../style/map-style.service' -import { - mapCtxExtentFixture, - mapCtxFixture, - mapCtxLayerGeojsonFixture, - mapCtxLayerGeojsonRemoteFixture, - mapCtxLayerWfsFixture, - mapCtxLayerWmsFixture, - mapCtxLayerXyzFixture, -} from './map-context.fixtures' - -import { - DEFAULT_BASELAYER_CONTEXT, - DEFAULT_VIEW, - MapContextService, -} from './map-context.service' -import Feature from 'ol/Feature' -import ImageWMS from 'ol/source/ImageWMS' -import ImageLayer from 'ol/layer/Image' - -const mapStyleServiceMock = { - createDefaultStyle: jest.fn(() => new Style()), - styles: { - default: defaultMapStyleFixture(), - defaultHL: defaultMapStyleHlFixture(), - }, -} - -jest.mock('@camptocamp/ogc-client', () => ({ - WmtsEndpoint: class { - constructor(private url) {} - isReady() { - return Promise.resolve({ - getLayerByName: (name) => { - if (this.url.indexOf('error') > -1) { - throw new Error('Something went wrong') - } - return { - name, - latLonBoundingBox: [1.33, 48.81, 4.3, 51.1], - } - }, - }) - } - }, - WfsEndpoint: class { - constructor(private url) {} - isReady() { - return Promise.resolve({ - getLayerByName: (name) => { - if (this.url.indexOf('error') > -1) { - throw new Error('Something went wrong') - } - return { - name, - latLonBoundingBox: [1.33, 48.81, 4.3, 51.1], - } - }, - getSingleFeatureTypeName: () => { - return 'ms:commune_actuelle_3857' - }, - getFeatureUrl: () => { - return 'https://www.geograndest.fr/geoserver/region-grand-est/ows?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typename=ms%3Acommune_actuelle_3857&srsname=EPSG%3A3857&bbox=10%2C20%2C30%2C40%2CEPSG%3A3857&maxFeatures=10000' - }, - }) - } - }, -})) - -describe('MapContextService', () => { - let service: MapContextService - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - { - provide: MapStyleService, - useValue: mapStyleServiceMock, - }, - ], - }) - service = TestBed.inject(MapContextService) - }) - - it('should be created', () => { - expect(service).toBeTruthy() - }) - - describe('#createLayer', () => { - let layerModel, layer - - describe('XYZ', () => { - beforeEach(() => { - layerModel = mapCtxLayerXyzFixture() - layer = service.createLayer(layerModel) - }) - it('create a tile layer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(TileLayer) - }) - it('create a XYZ source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(XYZ) - }) - it('set correct urls', () => { - const source = layer.getSource() - const urls = source.getUrls() - expect(urls.length).toBe(3) - expect(urls[0]).toEqual( - 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png' - ) - }) - }) - - describe('WMS', () => { - describe('when mapConfig.DO_NOT_TILE_WMS === false', () => { - beforeEach(() => { - const mapConfig: MapConfig = { - ...mapConfigFixture(), - DO_NOT_TILE_WMS: false, - } - ;(layerModel = mapCtxLayerWmsFixture()), - (layer = service.createLayer(layerModel, mapConfig)) - }) - it('create a tile layer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(TileLayer) - }) - it('create a TileWMS source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(TileWMS) - }) - it('set correct WMS params', () => { - const source = layer.getSource() - const params = source.getParams() - expect(params.LAYERS).toBe(layerModel.name) - }) - it('set correct url without existing REQUEST and SERVICE params', () => { - const source = layer.getSource() - const urls = source.getUrls() - expect(urls.length).toBe(1) - expect(urls[0]).toBe( - 'https://www.geograndest.fr/geoserver/region-grand-est/ows?REQUEST=GetCapabilities&SERVICE=WMS' - ) - }) - }) - - describe('when mapConfig.DO_NOT_TILE_WMS === true', () => { - beforeEach(() => { - const mapConfig: MapConfig = { - ...mapConfigFixture(), - DO_NOT_TILE_WMS: true, - } - ;(layerModel = mapCtxLayerWmsFixture()), - (layer = service.createLayer(layerModel, mapConfig)) - }) - it('create an image layer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(ImageLayer) - }) - it('create an ImageWMS source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(ImageWMS) - }) - it('set correct WMS params', () => { - const source = layer.getSource() - const params = source.getParams() - expect(params.LAYERS).toBe(layerModel.name) - }) - }) - }) - - describe('WFS', () => { - beforeEach(() => { - ;(layerModel = mapCtxLayerWfsFixture()), - (layer = service.createLayer(layerModel)) - }) - it('create a vector layer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(VectorLayer) - }) - it('create a Vector source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(VectorSource) - }) - it('set correct url load function', () => { - const source = layer.getSource() - const urlLoader = source.getUrl() - expect(urlLoader([10, 20, 30, 40])).toBe( - 'https://www.geograndest.fr/geoserver/region-grand-est/ows?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typename=ms%3Acommune_actuelle_3857&srsname=EPSG%3A3857&bbox=10%2C20%2C30%2C40%2CEPSG%3A3857&maxFeatures=10000' - ) - }) - }) - - describe('GEOJSON', () => { - describe('with inline data', () => { - beforeEach(() => { - layerModel = mapCtxLayerGeojsonFixture() - layer = service.createLayer(layerModel) - }) - it('create a VectorLayer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(VectorLayer) - }) - it('create a VectorSource source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(VectorSource) - }) - it('add features', () => { - const source = layer.getSource() - const features = source.getFeatures() - expect(features.length).toBe(layerModel.data.features.length) - }) - }) - describe('with inline data as string', () => { - beforeEach(() => { - layerModel = { ...mapCtxLayerGeojsonFixture() } - layerModel.data = JSON.stringify(layerModel.data) - layer = service.createLayer(layerModel) - }) - it('create a VectorLayer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(VectorLayer) - }) - it('create a VectorSource source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(VectorSource) - }) - it('add features', () => { - const source = layer.getSource() - const features = source.getFeatures() - expect(features.length).toBe( - (mapCtxLayerGeojsonFixture().data as FeatureCollection).features - .length - ) - }) - }) - describe('with invalid inline data as string', () => { - beforeEach(() => { - const spy = jest.spyOn(global.console, 'warn') - spy.mockClear() - layerModel = { ...mapCtxLayerGeojsonFixture(), data: 'blargz' } - layer = service.createLayer(layerModel) - }) - it('create a VectorLayer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(VectorLayer) - }) - it('outputs error in the console', () => { - expect(global.console.warn).toHaveBeenCalled() - }) - it('create an empty VectorSource source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(VectorSource) - expect(source.getFeatures().length).toBe(0) - }) - }) - describe('with remote file url', () => { - beforeEach(() => { - layerModel = mapCtxLayerGeojsonRemoteFixture() - layer = service.createLayer(layerModel) - }) - it('create a VectorLayer', () => { - expect(layer).toBeTruthy() - expect(layer).toBeInstanceOf(VectorLayer) - }) - it('create a VectorSource source', () => { - const source = layer.getSource() - expect(source).toBeInstanceOf(VectorSource) - }) - it('sets the format as GeoJSON', () => { - const source = layer.getSource() - expect(source.getFormat()).toBeInstanceOf(GeoJSON) - }) - it('set the url to point to the file', () => { - const source = layer.getSource() - expect(source.getUrl()).toBe(layerModel.url) - }) - }) - }) - }) - - describe('#createView', () => { - describe('from center and zoom', () => { - let view - const contextModel = mapCtxFixture() - beforeEach(() => { - view = service.createView(contextModel.view) - }) - it('create a view', () => { - expect(view).toBeTruthy() - expect(view).toBeInstanceOf(View) - }) - it('set center', () => { - const center = view.getCenter() - expect(center).toEqual([862726.0536478702, 6207260.308175252]) - }) - it('set zoom', () => { - const zoom = view.getZoom() - expect(zoom).toEqual(contextModel.view.zoom) - }) - }) - describe('from extent', () => { - let view - const contextModel = mapCtxFixture() - contextModel.view.extent = mapCtxExtentFixture() - const map = new Map({}) - map.setSize([100, 100]) - beforeEach(() => { - view = service.createView(contextModel.view, map) - }) - it('create a view', () => { - expect(view).toBeTruthy() - expect(view).toBeInstanceOf(View) - }) - it('set center', () => { - const center = view.getCenter() - expect(center).toEqual([324027.04834895337, 6438563.654151043]) - }) - it('set zoom', () => { - const zoom = view.getZoom() - expect(zoom).toEqual(5) - }) - }) - }) - describe('#resetMapFromContext', () => { - describe('without config', () => { - const map = new Map({}) - const mapContext = mapCtxFixture() - beforeEach(() => { - service.resetMapFromContext(map, mapContext) - }) - it('create a map', () => { - expect(map).toBeTruthy() - expect(map).toBeInstanceOf(Map) - }) - it('add layers', () => { - const layers = map.getLayers().getArray() - expect(layers.length).toEqual(4) - }) - it('set view', () => { - const view = map.getView() - expect(view).toBeTruthy() - expect(view).toBeInstanceOf(View) - }) - it('set first layer as baselayer', () => { - const baselayerUrls = (map.getLayers().item(0) as TileLayer) - .getSource() - .getUrls() - expect(baselayerUrls).toEqual(DEFAULT_BASELAYER_CONTEXT.urls) - }) - }) - describe('with config', () => { - const map = new Map({}) - const mapContext = mapCtxFixture() - const mapConfig = mapConfigFixture() - beforeEach(() => { - mapConfig.DO_NOT_USE_DEFAULT_BASEMAP = true - service.resetMapFromContext(map, mapContext, mapConfig) - }) - it('set maxZoom', () => { - const maxZoom = map.getView().getMaxZoom() - expect(maxZoom).toBe(10) - }) - it('set first layer as baselayer', () => { - const baselayerUrls = (map.getLayers().item(0) as TileLayer) - .getSource() - .getUrls() - expect(baselayerUrls).toEqual(['https://some-basemap-server']) - }) - it('add one WMS layer from config on top of baselayer', () => { - const layerWMSUrl = (map.getLayers().item(1) as TileLayer) - .getSource() - .getUrls()[0] - expect(layerWMSUrl).toEqual('https://some-wms-server') - }) - it('add one WFS layer from config on top of baselayer', () => { - const layerWFSSource = ( - map.getLayers().item(2) as VectorLayer< - VectorSource> - > - ).getSource() - expect(layerWFSSource).toBeInstanceOf(VectorSource) - }) - }) - describe('with config, but keeping default basemap', () => { - const map = new Map({}) - const mapContext = mapCtxFixture() - const mapConfig = mapConfigFixture() - beforeEach(() => { - mapConfig.DO_NOT_USE_DEFAULT_BASEMAP = false - service.resetMapFromContext(map, mapContext, mapConfig) - }) - it('set first layer as baselayer', () => { - const baselayerUrls = (map.getLayers().item(0) as TileLayer) - .getSource() - .getUrls() - expect(baselayerUrls).toEqual(DEFAULT_BASELAYER_CONTEXT.urls) - }) - }) - describe('uses default fallback view (without config)', () => { - let view - const map = new Map({}) - const mapContext = { - extent: null, - center: null, - zoom: null, - layers: [ - mapCtxLayerXyzFixture(), - mapCtxLayerWmsFixture(), - mapCtxLayerGeojsonFixture(), - ], - } - beforeEach(() => { - service.resetMapFromContext(map, mapContext) - }) - it('create a map', () => { - expect(map).toBeTruthy() - expect(map).toBeInstanceOf(Map) - }) - it('add layers', () => { - const layers = map.getLayers().getArray() - expect(layers.length).toEqual(4) - }) - it('set view', () => { - view = map.getView() - expect(view).toBeTruthy() - expect(view).toBeInstanceOf(View) - }) - it('set center', () => { - const center = view.getCenter() - expect(center).toEqual([0, 1689200.1396078935]) - }) - it('set zoom', () => { - const zoom = view.getZoom() - expect(zoom).toEqual(DEFAULT_VIEW.zoom) - }) - }) - describe('uses fallback view from config', () => { - let view - const map = new Map({}) - const mapConfig = mapConfigFixture() - const mapContext = { - extent: null, - center: null, - zoom: null, - layers: [], - } - beforeEach(() => { - service.resetMapFromContext(map, mapContext, mapConfig) - }) - it('create a map', () => { - expect(map).toBeTruthy() - expect(map).toBeInstanceOf(Map) - }) - it('add layers', () => { - const layers = map.getLayers().getArray() - expect(layers.length).toEqual(4) - }) - it('set view', () => { - view = map.getView() - expect(view).toBeTruthy() - expect(view).toBeInstanceOf(View) - }) - it('set center', () => { - const center = view.getCenter() - expect(center).toEqual([271504.324469, 5979210.100579999]) - }) - it('set zoom', () => { - const zoom = view.getZoom() - expect(zoom).toEqual(3) - }) - }) - }) - describe('#mergeMapConfigWithContext', () => { - const mapContext = mapCtxFixture() - const mapConfig = mapConfigFixture() - beforeEach(() => { - mapConfig.DO_NOT_USE_DEFAULT_BASEMAP = true - }) - it('merges mapconfig into existing mapcontext', () => { - const mergedMapContext = service.mergeMapConfigWithContext( - mapContext, - mapConfig - ) - const layersContext = mapConfigFixture().MAP_LAYERS.map( - service.getContextLayerFromConfig - ) - - expect(mergedMapContext).toEqual({ - ...mapCtxFixture(), - view: { - ...mapCtxFixture().view, - maxZoom: mapConfigFixture().MAX_ZOOM, - maxExtent: mapConfigFixture().MAX_EXTENT, - }, - layers: [ - layersContext[0], - layersContext[1], - layersContext[2], - ...mapCtxFixture().layers, - ], - }) - }) - }) -}) diff --git a/libs/feature/map/src/lib/map-context/map-context.service.ts b/libs/feature/map/src/lib/map-context/map-context.service.ts deleted file mode 100644 index 3119c01656..0000000000 --- a/libs/feature/map/src/lib/map-context/map-context.service.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { Injectable } from '@angular/core' -import { MapStyleService } from '../style/map-style.service' -import { - MapContextLayerModel, - MapContextLayerTypeEnum, - MapContextLayerXyzModel, - MapContextModel, - MapContextViewModel, -} from './map-context.model' -import Map from 'ol/Map' -import View from 'ol/View' -import Layer from 'ol/layer/Base' -import VectorLayer from 'ol/layer/Vector' -import TileWMS from 'ol/source/TileWMS' -import TileLayer from 'ol/layer/Tile' -import XYZ from 'ol/source/XYZ' -import VectorSource from 'ol/source/Vector' -import GeoJSON from 'ol/format/GeoJSON' -import { MapUtilsService } from '../utils/map-utils.service' -import { bbox as bboxStrategy } from 'ol/loadingstrategy' -import { LayerConfig, MapConfig } from '@geonetwork-ui/util/app-config' -import { FeatureCollection } from 'geojson' -import { fromLonLat } from 'ol/proj' -import WMTS from 'ol/source/WMTS' -import { Geometry } from 'ol/geom' -import Feature from 'ol/Feature' -import { WfsEndpoint, WmtsEndpoint } from '@camptocamp/ogc-client' -import OGCVectorTile from 'ol/source/OGCVectorTile.js' -import { MVT } from 'ol/format' -import VectorTileLayer from 'ol/layer/VectorTile' -import OGCMapTile from 'ol/source/OGCMapTile.js' -import ImageLayer from 'ol/layer/Image' -import ImageWMS from 'ol/source/ImageWMS' - -export const DEFAULT_BASELAYER_CONTEXT: MapContextLayerXyzModel = { - type: MapContextLayerTypeEnum.XYZ, - urls: [ - `https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, - `https://b.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, - `https://c.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, - ], - attributions: `© OpenStreetMap contributors, © Carto`, -} - -export const DEFAULT_VIEW: MapContextViewModel = { - center: [0, 15], - zoom: 2, -} - -export const WFS_MAX_FEATURES = 10000 - -@Injectable({ - providedIn: 'root', -}) -export class MapContextService { - constructor( - private mapUtils: MapUtilsService, - private styleService: MapStyleService - ) {} - - resetMapFromContext( - map: Map, - mapContext: MapContextModel, - mapConfig?: MapConfig - ): Map { - if (mapConfig) { - mapContext = this.mergeMapConfigWithContext(mapContext, mapConfig) - } else { - mapContext.layers = this.addDefaultBaselayerContext(mapContext.layers) - } - if ( - !mapContext.view?.extent && - (!mapContext.view?.center || !mapContext.view?.zoom) - ) { - mapContext.view = this.getFallbackView(mapConfig) - } - map.setView(this.createView(mapContext.view, map)) - map.getLayers().clear() - mapContext.layers.forEach((layer) => - map.addLayer(this.createLayer(layer, mapConfig)) - ) - return map - } - - createLayer(layerModel: MapContextLayerModel, mapConfig?: MapConfig): Layer { - const { type } = layerModel - const style = this.styleService.styles.default - switch (type) { - case MapContextLayerTypeEnum.OGCAPI: - if (layerModel.layerType === 'vectorTiles') { - return new VectorTileLayer({ - source: new OGCVectorTile({ - url: layerModel.url, - format: new MVT(), - attributions: layerModel.attributions, - }), - }) - } else if (layerModel.layerType === 'mapTiles') { - return new TileLayer({ - source: new OGCMapTile({ - url: layerModel.url, - attributions: layerModel.attributions, - }), - }) - } else { - return new VectorLayer({ - source: new VectorSource({ - format: new GeoJSON(), - url: layerModel.url, - attributions: layerModel.attributions, - }), - style, - }) - } - case MapContextLayerTypeEnum.XYZ: - return new TileLayer({ - source: new XYZ({ - url: 'url' in layerModel ? layerModel.url : undefined, - urls: 'urls' in layerModel ? layerModel.urls : undefined, - attributions: layerModel.attributions, - }), - }) - case MapContextLayerTypeEnum.WMS: - if (mapConfig?.DO_NOT_TILE_WMS) { - return new ImageLayer({ - source: new ImageWMS({ - url: layerModel.url, - params: { LAYERS: layerModel.name }, - attributions: layerModel.attributions, - }), - }) - } else { - return new TileLayer({ - source: new TileWMS({ - url: layerModel.url, - params: { LAYERS: layerModel.name, TILED: true }, - attributions: layerModel.attributions, - }), - }) - } - - case MapContextLayerTypeEnum.WMTS: { - // TODO: isolate this in utils service - const olLayer = new TileLayer({}) - const endpoint = new WmtsEndpoint(layerModel.url) - endpoint.isReady().then(async (endpoint) => { - const layerName = endpoint.getSingleLayerName() ?? layerModel.name - const layer = endpoint.getLayerByName(layerName) - const matrixSet = layer.matrixSets[0] - const tileGrid = await endpoint.getOpenLayersTileGrid(layer.name) - const resourceUrl = layer.resourceLinks[0] - const dimensions = endpoint.getDefaultDimensions(layer.name) - olLayer.setSource( - new WMTS({ - layer: layer.name, - style: layer.defaultStyle, - matrixSet: matrixSet.identifier, - format: resourceUrl.format, - url: resourceUrl.url, - requestEncoding: resourceUrl.encoding, - tileGrid, - projection: matrixSet.crs, - dimensions, - attributions: layerModel.attributions, - }) - ) - }) - return olLayer - } - case MapContextLayerTypeEnum.WFS: { - const olLayer = new VectorLayer({ - style, - }) - new WfsEndpoint(layerModel.url).isReady().then((endpoint) => { - const featureType = - endpoint.getSingleFeatureTypeName() ?? layerModel.name - olLayer.setSource( - new VectorSource({ - format: new GeoJSON(), - url: function (extent: [number, number, number, number]) { - return endpoint.getFeatureUrl(featureType, { - maxFeatures: WFS_MAX_FEATURES, - asJson: true, - outputCrs: 'EPSG:3857', - extent, - extentCrs: 'EPSG:3857', - }) - }, - strategy: bboxStrategy, - attributions: layerModel.attributions, - }) - ) - }) - return olLayer - } - case MapContextLayerTypeEnum.GEOJSON: { - if ('url' in layerModel) { - return new VectorLayer({ - source: new VectorSource({ - format: new GeoJSON(), - url: layerModel.url, - }), - style, - }) - } else { - let geojson = layerModel.data - if (typeof geojson === 'string') { - try { - geojson = JSON.parse(geojson) - } catch (e) { - console.warn('A layer could not be created', layerModel, e) - geojson = { type: 'FeatureCollection', features: [] } - } - } - const features = this.mapUtils.readFeatureCollection( - geojson as FeatureCollection - ) as Feature[] - return new VectorLayer({ - source: new VectorSource({ - features, - }), - style, - }) - } - } - default: - throw new Error(`Unrecognized layer type: ${layerModel.type}`) - } - } - - createView(viewModel: MapContextViewModel, map?: Map): View { - const { center: centerInViewProj, zoom, maxZoom, maxExtent } = viewModel - const center = centerInViewProj - ? fromLonLat(centerInViewProj, 'EPSG:3857') - : [0, 0] - const view = new View({ - center, - zoom, - maxZoom, - extent: maxExtent, - multiWorld: false, - constrainResolution: true, - }) - if (viewModel.extent && map) { - view.fit(viewModel.extent, { - size: map.getSize(), - }) - } - return view - } - - addDefaultBaselayerContext( - layers: MapContextLayerModel[] - ): MapContextLayerModel[] { - return layers.includes(DEFAULT_BASELAYER_CONTEXT) - ? layers - : [DEFAULT_BASELAYER_CONTEXT, ...layers] - } - - mergeMapConfigWithContext( - mapContext: MapContextModel, - mapConfig: MapConfig - ): MapContextModel { - return { - ...mapContext, - view: { - ...mapContext.view, - ...(mapConfig.MAX_ZOOM && { - maxZoom: mapConfig.MAX_ZOOM, - }), - ...(mapConfig.MAX_EXTENT && { - maxExtent: mapConfig.MAX_EXTENT, - }), - }, - layers: [ - ...(mapConfig.DO_NOT_USE_DEFAULT_BASEMAP - ? [] - : [DEFAULT_BASELAYER_CONTEXT]), - ...mapConfig.MAP_LAYERS.map(this.getContextLayerFromConfig), - ...mapContext.layers, - ], - } - } - - getFallbackView(mapConfig: MapConfig): MapContextViewModel { - return mapConfig?.MAX_EXTENT - ? { extent: mapConfig.MAX_EXTENT } - : DEFAULT_VIEW - } - - getContextLayerFromConfig(config: LayerConfig): MapContextLayerModel { - switch (config.TYPE) { - case 'wms': - return { - type: 'wms', - url: config.URL, - name: config.NAME, - } - case 'wfs': - return { - type: 'wfs', - url: config.URL, - name: config.NAME, - } - case 'xyz': - return { - type: config.TYPE, - url: config.URL, - name: config.NAME, - } - case 'geojson': - return { - type: config.TYPE, - ...(config.DATA ? { data: config.DATA } : { url: config.URL }), - } - } - } -}