diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index 0c69562559..bf521dbb71 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -43,6 +43,7 @@ import { import { UiSearchModule } from '@geonetwork-ui/ui/search' import { getGlobalConfig, + getOptionalMapConfig, getOptionalSearchConfig, getThemeConfig, TRANSLATE_WITH_OVERRIDES_CONFIG, @@ -95,6 +96,12 @@ import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { RecordUserFeedbacksComponent } from './record/record-user-feedbacks/record-user-feedbacks.component' import { LetDirective } from '@ngrx/component' import { OrganizationPageComponent } from './organization/organization-page/organization-page.component' +import { + BASEMAP_LAYERS, + DO_NOT_USE_DEFAULT_BASEMAP, + MAP_VIEW_CONSTRAINTS, +} from '@geonetwork-ui/ui/map' +import { getMapContextLayerFromConfig } from '../../../../libs/util/app-config/src/lib/map-layers' export const metaReducers: MetaReducer[] = !environment.production ? [] : [] @@ -229,6 +236,22 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] provide: ORGANIZATION_URL_TOKEN, useValue: `${ROUTER_ROUTE_SEARCH}?${ROUTE_PARAMS.PUBLISHER}=\${name}`, }, + { + provide: DO_NOT_USE_DEFAULT_BASEMAP, + useFactory: () => getOptionalMapConfig()?.DO_NOT_USE_DEFAULT_BASEMAP, + }, + { + provide: BASEMAP_LAYERS, + useFactory: () => + getOptionalMapConfig()?.MAP_LAYERS.map(getMapContextLayerFromConfig), + }, + { + provide: MAP_VIEW_CONSTRAINTS, + useFactory: () => ({ + maxExtent: getOptionalMapConfig()?.MAX_EXTENT, + maxZoom: getOptionalMapConfig()?.MAX_ZOOM, + }), + }, ], bootstrap: [AppComponent], }) diff --git a/libs/feature/map/src/index.ts b/libs/feature/map/src/index.ts index 9504431bf5..fb179415c9 100644 --- a/libs/feature/map/src/index.ts +++ b/libs/feature/map/src/index.ts @@ -3,7 +3,6 @@ export * from './lib/+state/map.selectors' export * from './lib/+state/map.reducer' export * from './lib/+state/map.actions' export * from './lib/feature-map.module' -export * from './lib/map-context/map-context.service' export * from './lib/map-state-container/map-state-container.component' export * from './lib/constant' export * from './lib/utils' 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 6ad4b96600..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 { MAP_CONFIG_FIXTURE, MapConfig } 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 { - DEFAULT_STYLE_FIXTURE, - DEFAULT_STYLE_HL_FIXTURE, -} from '../style/map-style.fixtures' -import { MapStyleService } from '../style/map-style.service' -import { - MAP_CTX_EXTENT_FIXTURE, - MAP_CTX_FIXTURE, - MAP_CTX_LAYER_GEOJSON_FIXTURE, - MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE, - MAP_CTX_LAYER_WFS_FIXTURE, - MAP_CTX_LAYER_WMS_FIXTURE, - MAP_CTX_LAYER_XYZ_FIXTURE, -} 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: DEFAULT_STYLE_FIXTURE, - defaultHL: DEFAULT_STYLE_HL_FIXTURE, - }, -} - -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 = MAP_CTX_LAYER_XYZ_FIXTURE - 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 = { - ...MAP_CONFIG_FIXTURE, - DO_NOT_TILE_WMS: false, - } - ;(layerModel = MAP_CTX_LAYER_WMS_FIXTURE), - (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 = { - ...MAP_CONFIG_FIXTURE, - DO_NOT_TILE_WMS: true, - } - ;(layerModel = MAP_CTX_LAYER_WMS_FIXTURE), - (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 = MAP_CTX_LAYER_WFS_FIXTURE), - (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 = MAP_CTX_LAYER_GEOJSON_FIXTURE - 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 = { ...MAP_CTX_LAYER_GEOJSON_FIXTURE } - 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( - (MAP_CTX_LAYER_GEOJSON_FIXTURE.data as FeatureCollection).features - .length - ) - }) - }) - describe('with invalid inline data as string', () => { - beforeEach(() => { - const spy = jest.spyOn(global.console, 'warn') - spy.mockClear() - layerModel = { ...MAP_CTX_LAYER_GEOJSON_FIXTURE, 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 = MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE - 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 = MAP_CTX_FIXTURE - 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 = MAP_CTX_FIXTURE - contextModel.view.extent = MAP_CTX_EXTENT_FIXTURE - 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 = MAP_CTX_FIXTURE - 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 = MAP_CTX_FIXTURE - const mapConfig = MAP_CONFIG_FIXTURE - 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 = MAP_CTX_FIXTURE - const mapConfig = MAP_CONFIG_FIXTURE - 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: [ - MAP_CTX_LAYER_XYZ_FIXTURE, - MAP_CTX_LAYER_WMS_FIXTURE, - MAP_CTX_LAYER_GEOJSON_FIXTURE, - ], - } - 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 = MAP_CONFIG_FIXTURE - 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 = MAP_CTX_FIXTURE - const mapConfig = MAP_CONFIG_FIXTURE - beforeEach(() => { - mapConfig.DO_NOT_USE_DEFAULT_BASEMAP = true - }) - it('merges mapconfig into existing mapcontext', () => { - const mergedMapContext = service.mergeMapConfigWithContext( - mapContext, - mapConfig - ) - const layersContext = MAP_CONFIG_FIXTURE.MAP_LAYERS.map( - service.getContextLayerFromConfig - ) - - expect(mergedMapContext).toEqual({ - ...MAP_CTX_FIXTURE, - view: { - ...MAP_CTX_FIXTURE.view, - maxZoom: MAP_CONFIG_FIXTURE.MAX_ZOOM, - maxExtent: MAP_CONFIG_FIXTURE.MAX_EXTENT, - }, - layers: [ - layersContext[0], - layersContext[1], - layersContext[2], - ...MAP_CTX_FIXTURE.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 9df36db082..0000000000 --- a/libs/feature/map/src/lib/map-context/map-context.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable } from '@angular/core' -import { LayerConfig } from '@geonetwork-ui/util/app-config' -import { MapContextLayer } from '@geospatial-sdk/core' - -@Injectable({ - providedIn: 'root', -}) -export class MapContextService { - // 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): MapContextLayer { - switch (config.TYPE) { - case 'wms': - return { - type: 'wms', - url: config.URL, - name: config.NAME, - } - case 'wfs': - return { - type: 'wfs', - url: config.URL, - featureType: config.NAME, - } - case 'xyz': - return { - type: config.TYPE, - url: config.URL, - } - case 'geojson': - return { - type: config.TYPE, - ...(config.DATA ? { data: config.DATA } : { url: config.URL }), - } - } - } -} diff --git a/libs/feature/map/src/lib/map-state-container/map-state-container.component.ts b/libs/feature/map/src/lib/map-state-container/map-state-container.component.ts index 166fc70ff9..5b748de57b 100644 --- a/libs/feature/map/src/lib/map-state-container/map-state-container.component.ts +++ b/libs/feature/map/src/lib/map-state-container/map-state-container.component.ts @@ -1,19 +1,12 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' import { MapFacade } from '../+state/map.facade' -import { MapContext, MapContextLayerXyz } from '@geospatial-sdk/core' +import { MapContext } from '@geospatial-sdk/core' import { MapContainerComponent } from '@geonetwork-ui/ui/map' import { CommonModule } from '@angular/common' import Feature from 'ol/Feature' import GeoJSON from 'ol/format/GeoJSON' -export const DEFAULT_BASELAYER_CONTEXT: MapContextLayerXyz = { - type: 'xyz', - url: `https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, - attributions: `© OpenStreetMap contributors, © Carto`, -} - @Component({ selector: 'gn-ui-map-state-container', templateUrl: './map-state-container.component.html', @@ -23,16 +16,7 @@ export const DEFAULT_BASELAYER_CONTEXT: MapContextLayerXyz = { imports: [MapContainerComponent, CommonModule], }) export class MapStateContainerComponent { - context$: Observable = this.mapFacade.context$.pipe( - map((context) => ({ - ...context, - view: { - center: [4, 42], - zoom: 6, - }, - layers: [DEFAULT_BASELAYER_CONTEXT, ...context.layers], - })) - ) + context$: Observable = this.mapFacade.context$ constructor(private mapFacade: MapFacade) {} diff --git a/libs/feature/map/src/lib/utils/map-utils.service.ts b/libs/feature/map/src/lib/utils/map-utils.service.ts index 200ce2de99..1088099849 100644 --- a/libs/feature/map/src/lib/utils/map-utils.service.ts +++ b/libs/feature/map/src/lib/utils/map-utils.service.ts @@ -12,16 +12,8 @@ import Source from 'ol/source/Source' import ImageWMS from 'ol/source/ImageWMS' import TileWMS from 'ol/source/TileWMS' import VectorSource from 'ol/source/Vector' -import { defaults, DragPan, Interaction, MouseWheelZoom } from 'ol/interaction' -import { - mouseOnly, - noModifierKeys, - platformModifierKeyOnly, - primaryAction, -} from 'ol/events/condition' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import Collection from 'ol/Collection' import MapBrowserEvent from 'ol/MapBrowserEvent' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { ProxyService } from '@geonetwork-ui/util/shared' @@ -178,29 +170,6 @@ export class MapUtilsService { } } - prioritizePageScroll(interactions: Collection) { - interactions.clear() - interactions.extend( - defaults({ - // remove rotate interactions - altShiftDragRotate: false, - pinchRotate: false, - // replace drag and zoom interactions - dragPan: false, - mouseWheelZoom: false, - }) - .extend([ - new DragPan({ - condition: dragPanCondition, - }), - new MouseWheelZoom({ - condition: mouseWheelZoomCondition, - }), - ]) - .getArray() - ) - } - getRecordExtent(record: Partial): Extent { if (!('spatialExtents' in record) || record.spatialExtents.length === 0) { return null @@ -216,25 +185,3 @@ export class MapUtilsService { return transformExtent(totalExtent, 'EPSG:4326', 'EPSG:3857') } } - -export function dragPanCondition( - this: DragPan, - event: MapBrowserEvent -) { - const dragPanCondition = this.getPointerCount() === 2 || mouseOnly(event) - if (!dragPanCondition) { - this.getMap().dispatchEvent('mapmuted') - } - // combine the condition with the default DragPan conditions - return dragPanCondition && noModifierKeys(event) && primaryAction(event) -} - -export function mouseWheelZoomCondition( - this: MouseWheelZoom, - event: MapBrowserEvent -) { - if (!platformModifierKeyOnly(event) && event.type === 'wheel') { - this.getMap().dispatchEvent('mapmuted') - } - return platformModifierKeyOnly(event) -} diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index d3ed188234..7080492f38 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -6,7 +6,6 @@ import { ViewChild, } from '@angular/core' import { MapStyleService, MapUtilsService } from '@geonetwork-ui/feature/map' -import { getOptionalMapConfig, MapConfig } from '@geonetwork-ui/util/app-config' import { getLinkLabel } from '@geonetwork-ui/util/shared' import Feature from 'ol/Feature' import { Geometry } from 'ol/geom' @@ -33,7 +32,10 @@ import { MdViewFacade } from '../state/mdview.facade' import { DataService } from '@geonetwork-ui/feature/dataviz' import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' import { MapContext, MapContextLayer } from '@geospatial-sdk/core' -import { MapContainerComponent } from '@geonetwork-ui/ui/map' +import { + MapContainerComponent, + prioritizePageScroll, +} from '@geonetwork-ui/ui/map' @Component({ selector: 'gn-ui-map-view', @@ -44,7 +46,6 @@ import { MapContainerComponent } from '@geonetwork-ui/ui/map' export class MapViewComponent implements AfterViewInit { @ViewChild(MapContainerComponent) mapContainer: MapContainerComponent - mapConfig: MapConfig = getOptionalMapConfig() selection: Feature private selectionStyle: StyleLike @@ -144,9 +145,7 @@ export class MapViewComponent implements AfterViewInit { ) {} ngAfterViewInit(): void { - this.mapUtils.prioritizePageScroll( - this.mapContainer.openlayersMap.getInteractions() - ) + prioritizePageScroll(this.mapContainer.openlayersMap.getInteractions()) this.selectionStyle = this.styleService.styles.defaultHL } diff --git a/libs/ui/map/src/index.ts b/libs/ui/map/src/index.ts index 83e8a70a07..e923e08ab9 100644 --- a/libs/ui/map/src/index.ts +++ b/libs/ui/map/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/components/map/map-container.component' export * from './lib/components/feature-detail/feature-detail.component' +export * from './lib/map-utils' diff --git a/libs/ui/map/src/lib/components/map/map-container.component.ts b/libs/ui/map/src/lib/components/map/map-container.component.ts index 3f599c389f..7715951c26 100644 --- a/libs/ui/map/src/lib/components/map/map-container.component.ts +++ b/libs/ui/map/src/lib/components/map/map-container.component.ts @@ -4,6 +4,8 @@ import { Component, ElementRef, EventEmitter, + Inject, + InjectionToken, Input, OnChanges, Output, @@ -15,7 +17,14 @@ import { delay, map, startWith, switchMap } from 'rxjs/operators' import { CommonModule } from '@angular/common' import { MatIconModule } from '@angular/material/icon' import { TranslateModule } from '@ngx-translate/core' -import { computeMapContextDiff, MapContext } from '@geospatial-sdk/core' +import { + computeMapContextDiff, + Extent, + MapContext, + MapContextLayer, + MapContextLayerXyz, + MapContextView, +} from '@geospatial-sdk/core' import OlMap from 'ol/Map' import { applyContextDiffToMap, @@ -23,6 +32,32 @@ import { } from '@geospatial-sdk/openlayers' import Feature from 'ol/Feature' +export const DO_NOT_USE_DEFAULT_BASEMAP = new InjectionToken( + 'doNotUseDefaultBasemap', + { factory: () => false } +) +export const BASEMAP_LAYERS = new InjectionToken( + 'basemapLayers', + { factory: () => [] } +) +export const MAP_VIEW_CONSTRAINTS = new InjectionToken<{ + maxZoom?: number + maxExtent?: Extent +}>('mapViewConstraints', { + factory: () => ({}), +}) + +const DEFAULT_BASEMAP_LAYER: MapContextLayerXyz = { + type: 'xyz', + url: `https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, + attributions: `© OpenStreetMap contributors, © Carto`, +} + +const DEFAULT_VIEW: MapContextView = { + center: [0, 15], + zoom: 2, +} + @Component({ selector: 'gn-ui-map-container', templateUrl: './map-container.component.html', @@ -39,6 +74,16 @@ export class MapContainerComponent implements AfterViewInit, OnChanges { displayMessage$: Observable olMap: OlMap + constructor( + @Inject(DO_NOT_USE_DEFAULT_BASEMAP) private disableBaseMap: boolean, + @Inject(BASEMAP_LAYERS) private basemapLayers: MapContextLayer[], + @Inject(MAP_VIEW_CONSTRAINTS) + private mapViewConstraints: { + maxZoom?: number + maxExtent?: Extent + } + ) {} + public get openlayersMap(): OlMap { return this.olMap } @@ -68,10 +113,44 @@ export class MapContainerComponent implements AfterViewInit, OnChanges { ngOnChanges(changes: SimpleChanges) { if ('context' in changes && !changes['context'].isFirstChange()) { const diff = computeMapContextDiff( - changes['context'].currentValue, - changes['context'].previousValue + this.processContext(changes['context'].currentValue), + this.processContext(changes['context'].previousValue) ) applyContextDiffToMap(this.olMap, diff) } } + + // This will apply basemap layers & view constraints + processContext(context: MapContext): MapContext { + const processed = { ...context } + if (this.basemapLayers.length) { + processed.layers = [...this.basemapLayers, ...processed.layers] + } + if (!this.disableBaseMap) { + processed.layers = [DEFAULT_BASEMAP_LAYER, ...processed.layers] + } + if (this.mapViewConstraints.maxZoom) { + processed.view = { + maxZoom: this.mapViewConstraints.maxZoom, + ...processed.view, + } + } + if (this.mapViewConstraints.maxExtent) { + processed.view = { + maxExtent: this.mapViewConstraints.maxExtent, + ...processed.view, + } + } + if (!('zoom' in processed.view) && !('center' in processed.view)) { + if (this.mapViewConstraints.maxExtent) { + processed.view = { + extent: this.mapViewConstraints.maxExtent, + ...processed.view, + } + } else { + processed.view = { ...DEFAULT_VIEW, ...processed.view } + } + } + return processed + } } diff --git a/libs/ui/map/src/lib/map-utils.ts b/libs/ui/map/src/lib/map-utils.ts new file mode 100644 index 0000000000..866be4d646 --- /dev/null +++ b/libs/ui/map/src/lib/map-utils.ts @@ -0,0 +1,54 @@ +import Collection from 'ol/Collection' +import { defaults, DragPan, Interaction, MouseWheelZoom } from 'ol/interaction' +import MapBrowserEvent from 'ol/MapBrowserEvent' +import { + mouseOnly, + noModifierKeys, + platformModifierKeyOnly, + primaryAction, +} from 'ol/events/condition' + +export function prioritizePageScroll(interactions: Collection) { + interactions.clear() + interactions.extend( + defaults({ + // remove rotate interactions + altShiftDragRotate: false, + pinchRotate: false, + // replace drag and zoom interactions + dragPan: false, + mouseWheelZoom: false, + }) + .extend([ + new DragPan({ + condition: dragPanCondition, + }), + new MouseWheelZoom({ + condition: mouseWheelZoomCondition, + }), + ]) + .getArray() + ) +} + +export function dragPanCondition( + this: DragPan, + event: MapBrowserEvent +) { + const dragPanCondition = this.getPointerCount() === 2 || mouseOnly(event) + if (!dragPanCondition) { + this.getMap().dispatchEvent('mapmuted') + } + // combine the condition with the default DragPan conditions + return dragPanCondition && noModifierKeys(event) && primaryAction(event) +} + +export function mouseWheelZoomCondition( + this: MouseWheelZoom, + event: MapBrowserEvent +) { + if (!platformModifierKeyOnly(event) && event.type === 'wheel') { + this.getMap().dispatchEvent('mapmuted') + } + return platformModifierKeyOnly(event) +} diff --git a/libs/util/app-config/src/lib/map-layers.ts b/libs/util/app-config/src/lib/map-layers.ts new file mode 100644 index 0000000000..8f6e75ddc8 --- /dev/null +++ b/libs/util/app-config/src/lib/map-layers.ts @@ -0,0 +1,31 @@ +import { LayerConfig } from './model' +import { MapContextLayer } from '@geospatial-sdk/core' + +export function getMapContextLayerFromConfig( + config: LayerConfig +): MapContextLayer { + switch (config.TYPE) { + case 'wms': + return { + type: 'wms', + url: config.URL, + name: config.NAME, + } + case 'wfs': + return { + type: 'wfs', + url: config.URL, + featureType: config.NAME, + } + case 'xyz': + return { + type: config.TYPE, + url: config.URL, + } + case 'geojson': + return { + type: config.TYPE, + ...(config.DATA ? { data: config.DATA } : { url: config.URL }), + } + } +}