diff --git a/package-lock.json b/package-lock.json index a56933f5..1e604871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "ng-lazyload-image": "^9.0.0", "ngeohash": "^0.6.0", "ngx-loading": "^14.0.0", + "pmtiles": "^2.11.0", "rxjs": "~7.4.0", "shpjs": "^4.0.4", "stream": "0.0.2", @@ -8135,6 +8136,11 @@ "node": ">=0.8.0" } }, + "node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -12382,6 +12388,14 @@ "node": ">=8" } }, + "node_modules/pmtiles": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.11.0.tgz", + "integrity": "sha512-dU9SzzaqmCGpdEuTnIba6bDHT6j09ZJFIXxwGpvkiEnce3ZnBB1VKt6+EOmJGueriweaZLAMTUmKVElU2CBe0g==", + "dependencies": { + "fflate": "^0.8.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -21674,6 +21688,11 @@ "websocket-driver": ">=0.5.1" } }, + "fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -24633,6 +24652,14 @@ "find-up": "^4.0.0" } }, + "pmtiles": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.11.0.tgz", + "integrity": "sha512-dU9SzzaqmCGpdEuTnIba6bDHT6j09ZJFIXxwGpvkiEnce3ZnBB1VKt6+EOmJGueriweaZLAMTUmKVElU2CBe0g==", + "requires": { + "fflate": "^0.8.0" + } + }, "postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index dcf17bd2..d724e463 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "tslib": "^2.3.0", "turf-extent": "1.0.4", "wellknown": "0.5.0", - "zone.js": "^0.11.4" + "zone.js": "^0.11.4", + "pmtiles": "^2.11.0" }, "devDependencies": { "@angular-devkit/build-angular": "^14.2.13", diff --git a/projects/arlas-components/package.json b/projects/arlas-components/package.json index 0806545d..0ab04ec9 100644 --- a/projects/arlas-components/package.json +++ b/projects/arlas-components/package.json @@ -43,7 +43,8 @@ "shpjs": "^4.0.4", "turf-extent": "1.0.4", "wellknown": "0.5.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "pmtiles": "^2.11.0" }, "exports": { "./assets/i18n/en.json": { diff --git a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.css b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.css index 0ed08549..be7e60f7 100644 --- a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.css +++ b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.css @@ -17,7 +17,7 @@ cursor: pointer; } -.basemap-container .basemap.selected { +.basemap-container .selected { border: 2px solid #337ab7; background-color: #337ab7; color: #337ab7; diff --git a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.html b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.html index c97d5d3f..c68d9ad9 100644 --- a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.html +++ b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.html @@ -1,12 +1,23 @@ -
- -
-
- -
- wallpaper +
+ +
+
+ +
+ wallpaper +
+
{{style.name | translate}}
-
{{style.name | translate}}
-
+ + +
+
+
+ wallpaper +
+
{{theme.name | translate}}
+
+
+
diff --git a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.spec.ts b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.spec.ts index 11ea9c31..55d07b6a 100644 --- a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.spec.ts +++ b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.spec.ts @@ -1,14 +1,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MapglBasemapComponent } from './mapgl-basemap.component'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { MapboxBasemapService } from '../mapgl/basemaps/basemap.service'; describe('MapglBasemapComponent', () => { let component: MapglBasemapComponent; let fixture: ComponentFixture; - + const mockMapboxBasemapService = jasmine.createSpyObj('MapboxBasemapService', ['isOnline']); + mockMapboxBasemapService.isOnline.and.returnValue(false); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [MapglBasemapComponent] + declarations: [MapglBasemapComponent], + imports: [ + HttpClientModule + ], + providers: [ + HttpClient, + { + provide: MapboxBasemapService, + useValue: mockMapboxBasemapService + } + ] }) .compileComponents(); }); diff --git a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.ts b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.ts index 1adab242..b16d5485 100644 --- a/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.ts +++ b/projects/arlas-components/src/lib/components/mapgl-basemap/mapgl-basemap.component.ts @@ -1,6 +1,13 @@ -import { Component, Input, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnInit, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core'; import { Subject } from 'rxjs/internal/Subject'; -import { BasemapStyle } from '../mapgl/model/mapLayers'; +import mapboxgl, { AnyLayer } from 'mapbox-gl'; +import { MapSource } from '../mapgl/model/mapSource'; +import { MapglService } from '../../services/mapgl.service'; +import { HttpClient } from '@angular/common/http'; +import { MapboxBasemapService } from '../mapgl/basemaps/basemap.service'; +import { BasemapStyle, OfflineBasemapTheme } from '../mapgl/basemaps/basemap.config'; +import { OfflineBasemap } from '../mapgl/basemaps/offline-basemap'; +import { OnlineBasemap } from '../mapgl/basemaps/online-basemap'; @Component({ selector: 'arlas-mapgl-basemap', @@ -8,18 +15,39 @@ import { BasemapStyle } from '../mapgl/model/mapLayers'; styleUrls: ['./mapgl-basemap.component.css'] }) export class MapglBasemapComponent implements OnInit { + private LOCAL_STORAGE_BASEMAPS = 'arlas_last_base_map'; - @Input() public basemapStyles: BasemapStyle[]; - @Input() public selectedBasemap: BasemapStyle; + @Input() public map: mapboxgl.Map; + @Input() public mapSources: Array; - @Output() public basemapChanged = new Subject(); + @Output() public basemapChanged = new EventEmitter(); @Output() public blur = new Subject(); - public constructor() { } + public showList = false; + public isOnline = true; + public onlineBasemaps: OnlineBasemap; + public offlineBasemaps: OfflineBasemap; + + public constructor( + private mapglService: MapglService, + private basemapService: MapboxBasemapService, + private http: HttpClient) { } public ngOnInit(): void { - if (this.basemapStyles) { - this.basemapStyles.filter(bm => !bm.image).forEach(bm => { + this.isOnline = this.basemapService.isOnline(); + if (this.isOnline) { + this.initOnlineBasemaps(); + } else { + this.initOfflineBasemaps(); + } + } + + private initOnlineBasemaps() { + this.onlineBasemaps = this.basemapService.onlineBasemaps; + const styles = this.onlineBasemaps.styles(); + if (!!this.onlineBasemaps && !!styles) { + this.showList = styles.length > 1; + styles.filter(bm => !bm.image).forEach(bm => { const splitUrl = bm.styleFile.toString().split('/style.json?key='); if (splitUrl.length === 2) { bm.image = `${splitUrl[0]}/0/0/0.png?key=${splitUrl[1]}`; @@ -28,7 +56,64 @@ export class MapglBasemapComponent implements OnInit { } } - public onChangeBasemapStyle(style: BasemapStyle){ - this.basemapChanged.next(style); + private initOfflineBasemaps() { + this.offlineBasemaps = this.basemapService.offlineBasemaps; + if (!!this.offlineBasemaps && !!this.offlineBasemaps.themes()) { + this.showList = this.offlineBasemaps.themes().length > 1; + } + } + + public onChangeOfflineBasemap(selectedTheme: OfflineBasemapTheme) { + this.basemapService.changeOfflineBasemap(this.map, selectedTheme); + } + + public onChangeOnlineBasemap(selectedStyle: BasemapStyle) { + this.setBaseMapStyle(selectedStyle.styleFile); + localStorage.setItem(this.LOCAL_STORAGE_BASEMAPS, JSON.stringify(selectedStyle)); + this.onlineBasemaps.setSelected(selectedStyle); + } + + public setBaseMapStyle(style: string | mapboxgl.Style) { + if (this.map) { + const selectedStyle = this.onlineBasemaps.getSelected(); + if (typeof selectedStyle.styleFile === 'string') { + this.http.get(selectedStyle.styleFile).subscribe((s: any) => { + this.setStyle(s, style); + }); + } else { + this.setStyle(selectedStyle.styleFile, style); + } + } + } + + public setStyle(s: mapboxgl.Style, style: string | mapboxgl.Style) { + const selectedBasemapLayersSet = new Set(); + const layers: Array = (this.map).getStyle().layers; + const sources = (this.map).getStyle().sources; + if (s.layers) { + s.layers.forEach(l => selectedBasemapLayersSet.add(l.id)); + } + const layersToSave = new Array(); + const sourcesToSave = new Array(); + layers.filter((l: mapboxgl.Layer) => !selectedBasemapLayersSet.has(l.id)).forEach(l => { + layersToSave.push(l); + if (sourcesToSave.filter(ms => ms.id === l.source.toString()).length === 0) { + sourcesToSave.push({ id: l.source.toString(), source: sources[l.source.toString()] }); + } + }); + const sourcesToSaveSet = new Set(); + sourcesToSave.forEach(mapSource => sourcesToSaveSet.add(mapSource.id)); + if (this.mapSources) { + this.mapSources.forEach(mapSource => { + if (!sourcesToSaveSet.has(mapSource.id)) { + sourcesToSave.push(mapSource); + } + }); + } + this.map.setStyle(style).once('styledata', () => { + this.mapglService.addSourcesToMap(sourcesToSave, this.map); + layersToSave.forEach(l => this.map.addLayer(l as AnyLayer)); + this.basemapChanged.emit(); + }); } } diff --git a/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.config.ts b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.config.ts new file mode 100644 index 00000000..b025327f --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.config.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Gisaïa under one or more contributor + * license agreements. See the NOTICE.txt file distributed with + * this work for additional information regarding copyright + * ownership. Gisaïa licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +// DISCLAIMER +// -- The word 'Style' is reserved to online basemaps. +// -- The word 'Theme' is reserved to offline basemaps. + +export interface BasemapsConfig { + isOnline: boolean; + onlineConfig?: OnlineBasemapConfig; + offlineConfig?: OfflineBasemapsConfig; +} + +export interface OnlineBasemapConfig { + styles: BasemapStyle[]; + defaultStyle: BasemapStyle; +} + +export interface OfflineBasemapsConfig { + /** Path to pmtiles file */ + url: string; + glyphsUrl: string; + themes: OfflineBasemapTheme[]; + defaultTheme: OfflineBasemapTheme; +} + + +export interface OfflineBasemapTheme { + layers: mapboxgl.Layer[]; + name: string; +} + +export interface BasemapStyle { + name: string; + styleFile: string | mapboxgl.Style; + image?: string; +} + diff --git a/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.service.ts b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.service.ts new file mode 100644 index 00000000..eb8397ed --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemap.service.ts @@ -0,0 +1,117 @@ +/* + * Licensed to Gisaïa under one or more contributor + * license agreements. See the NOTICE.txt file distributed with + * this work for additional information regarding copyright + * ownership. Gisaïa licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable } from '@angular/core'; +import { ArlasBasemaps } from './basemaps'; +import * as pmtiles from 'pmtiles'; +import { CustomProtocol } from '../custom-protocol/mapbox-gl-custom-protocol'; +import { OfflineBasemapTheme } from './basemap.config'; +import mapboxgl from 'mapbox-gl'; +import { OfflineBasemap } from './offline-basemap'; +import { Subject } from 'rxjs'; +import { OnlineBasemap } from './online-basemap'; + +@Injectable({ + providedIn: 'root' +}) +export class MapboxBasemapService { + private basemaps: ArlasBasemaps; + private LOCAL_STORAGE_BASEMAPS = 'arlas_last_base_map'; + + public onlineBasemaps: OnlineBasemap; + public offlineBasemaps: OfflineBasemap; + + private offlineBasemapChangedSource = new Subject(); + public offlineBasemapChanged$ = this.offlineBasemapChangedSource.asObservable(); + + public setBasemaps(basemaps: ArlasBasemaps) { + this.basemaps = basemaps; + this.onlineBasemaps = basemaps.onlineBasemaps; + this.offlineBasemaps = basemaps.offlineBasemaps; + } + + /** Add offline basemap only if configured. */ + public addOfflineBasemap(map: mapboxgl.Map) { + if (!!this.basemaps && this.basemaps.isOnline) { + return; + } + const protocol = new pmtiles.Protocol(); + /** addSourceType is private */ + (map as any).addSourceType('pmtiles-type', CustomProtocol(mapboxgl).vector, (e) => e && console.error('There was an error', e)); + (mapboxgl as any).addProtocol('pmtiles', protocol.tile); + const pmtilesUrl = this.offlineBasemaps.getUrl(); + map.addSource('protomaps_source', { + 'type': 'pmtiles-type', + 'tiles': ['pmtiles://' + pmtilesUrl + '/{z}/{x}/{y}'], + 'maxzoom': 21 + } as any); + + if (this.offlineBasemaps && !!this.offlineBasemaps.getSelected()) { + this.offlineBasemaps.getSelected().layers.forEach(l =>{ + map.addLayer(l as any); + }); + } + } + + public changeOfflineBasemap(map: mapboxgl.Map, newTheme: OfflineBasemapTheme) { + const currentTheme = this.offlineBasemaps.getSelected(); + currentTheme.layers.forEach(l => { + if (!!map.getLayer(l.id)) { + map.removeLayer(l.id); + } + }); + this.offlineBasemaps.setSelected(newTheme).getSelected().layers.forEach(l =>{ + map.addLayer(l as any); + }); + this.offlineBasemapChangedSource.next(true); + } + + public isOnline(): boolean { + if (!this.basemaps) { + throw new Error('No basemap configuration is set'); + } + return !!this.basemaps && this.basemaps.isOnline; + } + + public getInitStyle() { + if (this.basemaps.isOnline) { + const initStyle = this.onlineBasemaps.getSelected(); + return initStyle.styleFile; + } else { + return { + version: 8, + name: 'Empty', + metadata: { + 'mapbox:autocomposite': true + }, + glyphs: this.offlineBasemaps.getGlyphs(), + sources: {}, + layers: [ + { + id: 'backgrounds', + type: 'background', + paint: { + 'background-color': 'rgba(0,0,0,0)' + } + } + ] + } as mapboxgl.Style; + } + } +} diff --git a/projects/arlas-components/src/lib/components/mapgl/basemaps/basemaps.ts b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemaps.ts new file mode 100644 index 00000000..6e5e9ec8 --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/basemaps/basemaps.ts @@ -0,0 +1,52 @@ +import { BasemapStyle, BasemapsConfig, OfflineBasemapsConfig } from './basemap.config'; +import { OfflineBasemap } from './offline-basemap'; +import { OnlineBasemap } from './online-basemap'; + +export class ArlasBasemaps { + private config: BasemapsConfig; + public isOnline; + public onlineBasemaps: OnlineBasemap; + public offlineBasemaps: OfflineBasemap; + public constructor(config: BasemapsConfig, defaultBasemapStyle?: BasemapStyle, basemapStyles?: BasemapStyle[]) { + if (defaultBasemapStyle && basemapStyles && !config) { + /** Retrocompatibility code. To be removed in v25.0.0 */ + this.isOnline = true; + this.onlineBasemaps = new OnlineBasemap({ + styles: basemapStyles, + defaultStyle: defaultBasemapStyle + }); + } else { + this.config = config; + this.throwErrorBasemapAbscense(); + this.throwErrorOnlineAbscense(); + this.throwErrorOfflineAbscense(); + this.isOnline = config.isOnline; + if (this.isOnline) { + this.onlineBasemaps = new OnlineBasemap(this.config.onlineConfig); + } else { + this.offlineBasemaps = new OfflineBasemap(this.config.offlineConfig); + } + } + } + + private throwErrorBasemapAbscense() { + if (!this.config) { + throw new Error('Basemap configuration is not set.'); + } + } + + private throwErrorOnlineAbscense() { + if (this.config.isOnline && !this.config.onlineConfig) { + throw new Error('Online basemap configuration is not set.'); + } + } + + private throwErrorOfflineAbscense() { + if (this.config.isOnline && !this.config.offlineConfig) { + throw new Error('Offline basemap configuration is not set.'); + } + } +} + + + diff --git a/projects/arlas-components/src/lib/components/mapgl/basemaps/offline-basemap.ts b/projects/arlas-components/src/lib/components/mapgl/basemaps/offline-basemap.ts new file mode 100644 index 00000000..9841910e --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/basemaps/offline-basemap.ts @@ -0,0 +1,60 @@ +import { OfflineBasemapTheme, OfflineBasemapsConfig } from './basemap.config'; + +export class OfflineBasemap { + + private config: OfflineBasemapsConfig; + public _selectedTheme: OfflineBasemapTheme; + public _themes: OfflineBasemapTheme[]; + + public constructor(config: OfflineBasemapsConfig) { + this.config = config; + this.getSelected(); + } + + public themes(): OfflineBasemapTheme[] { + if (!this._themes) { + this._themes = this.getAllBasemapThemes(this.config.defaultTheme, this.config.themes); + } + return this._themes; + } + + public setSelected(theme: OfflineBasemapTheme) { + this._selectedTheme = theme; + return this; + } + + public getSelected(): OfflineBasemapTheme { + if (!this._selectedTheme) { + const themes = this.themes(); + if (themes && themes.length > 0) { + this._selectedTheme = themes[0]; + } else { + throw new Error('No theme is defined for the offline basemap'); + } + } + return this._selectedTheme; + } + + public getGlyphs() { + return this.config.glyphsUrl; + } + + public getUrl() { + return this.config.url; + } + + private getAllBasemapThemes(defaultBasemapTheme: OfflineBasemapTheme, basemapThemes: OfflineBasemapTheme[]): Array { + const allBasemapThemes = new Array(); + if (basemapThemes) { + basemapThemes.forEach(b => allBasemapThemes.push(b)); + if (defaultBasemapTheme) { + if (basemapThemes.map(b => b.name).filter(n => n === defaultBasemapTheme.name).length === 0) { + allBasemapThemes.push(defaultBasemapTheme); + } + } + } else if (defaultBasemapTheme) { + allBasemapThemes.push(defaultBasemapTheme); + } + return allBasemapThemes; + } +} diff --git a/projects/arlas-components/src/lib/components/mapgl/basemaps/online-basemap.ts b/projects/arlas-components/src/lib/components/mapgl/basemaps/online-basemap.ts new file mode 100644 index 00000000..2f3b5e6f --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/basemaps/online-basemap.ts @@ -0,0 +1,61 @@ +import { BasemapStyle, OnlineBasemapConfig } from './basemap.config'; + +export class OnlineBasemap { + + private LOCAL_STORAGE_BASEMAPS = 'arlas_last_base_map'; + + private config: OnlineBasemapConfig; + public _selectedStyle: BasemapStyle; + public _styles: BasemapStyle[]; + + public constructor(config: OnlineBasemapConfig) { + this.config = config; + this.getSelected(); + } + + public styles(): BasemapStyle[] { + if (!this._styles) { + this._styles = this.getAllBasemapStyles(this.config.defaultStyle, this.config.styles); + } + return this._styles; + } + + public setSelected(styele: BasemapStyle) { + this._selectedStyle = styele; + return this; + } + + public getSelected(): BasemapStyle { + if (!this._selectedStyle) { + const styles = this.styles(); + const localStorageBasemapStyle: BasemapStyle = JSON.parse(localStorage.getItem(this.LOCAL_STORAGE_BASEMAPS)); + if (localStorageBasemapStyle && styles.filter(b => b.name === localStorageBasemapStyle.name + && b.styleFile === localStorageBasemapStyle.styleFile).length > 0) { + this._selectedStyle = localStorageBasemapStyle; + return this._selectedStyle; + } + if (styles && styles.length > 0) { + this._selectedStyle = styles[0]; + } else { + throw new Error('No Style is defined for the online basemap'); + } + } + return this._selectedStyle; + } + + + private getAllBasemapStyles(defaultBasemapTheme: BasemapStyle, basemapStyles: BasemapStyle[]): Array { + const allBasemapStyles = new Array(); + if (basemapStyles) { + basemapStyles.forEach(b => allBasemapStyles.push(b)); + if (defaultBasemapTheme) { + if (basemapStyles.map(b => b.name).filter(n => n === defaultBasemapTheme.name).length === 0) { + allBasemapStyles.push(defaultBasemapTheme); + } + } + } else if (defaultBasemapTheme) { + allBasemapStyles.push(defaultBasemapTheme); + } + return allBasemapStyles; + } +} diff --git a/projects/arlas-components/src/lib/components/mapgl/custom-protocol/mapbox-gl-custom-protocol.ts b/projects/arlas-components/src/lib/components/mapgl/custom-protocol/mapbox-gl-custom-protocol.ts new file mode 100644 index 00000000..221ec136 --- /dev/null +++ b/projects/arlas-components/src/lib/components/mapgl/custom-protocol/mapbox-gl-custom-protocol.ts @@ -0,0 +1,154 @@ +const getReqObjectUrl = (loadFn, rawUrl, type, collectResourceTiming) => new Promise((res, rej) => { + const requestParameters: any = { + url: rawUrl, + type: type === ('vector' || 'raster') ? 'arrayBuffer' : 'string', + collectResourceTiming: collectResourceTiming + }; + if (type === 'raster') { + requestParameters.headers = { + accept: 'image/webp,*/*', + }; + } else { + requestParameters.headers = { + 'Content-Encoding': 'gzip' + }; + } + const urlCallback = (error, data, cacheControl, expires) => { + if (error) { + rej(error); + } else { + let preparedData; + if (data instanceof Uint8Array) { + preparedData = new Uint8Array(data); + } else { + preparedData = JSON.stringify(data); + } + const blob = new Blob([preparedData]); + const url = URL.createObjectURL(blob); + res(url); + } + }; + loadFn(requestParameters, urlCallback); +}); +// eslint-disable-next-line @typescript-eslint/naming-convention +export const CustomProtocol = (mapLibrary) => { + // Adds the protocol tools to the mapLibrary, doesn't overwrite them if they already exist + const alreadySupported = mapLibrary.addProtocol !== undefined && mapLibrary._protocols === undefined; + if (!alreadySupported) { + mapLibrary._protocols = mapLibrary._protocols || new Map(); + mapLibrary.addProtocol = mapLibrary.addProtocol || ((customProtocol, loadFn) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _a; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-unused-expressions + (_a = mapLibrary._protocols) === null || _a === void 0 ? void 0 : _a.set(customProtocol, loadFn); + }); + mapLibrary.removeProtocol = mapLibrary.removeProtocol || ((customProtocol) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _a; + // eslint-disable-next-line @typescript-eslint/no-unused-expressions, no-unused-expressions + (_a = mapLibrary._protocols) === null || _a === void 0 ? void 0 : _a.delete(customProtocol); + }); + } + return { + 'vector': class VectorCustomProtocolSourceSpecification extends mapLibrary.Style.getSourceType('vector') { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + constructor() { + super(...arguments); + } + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + loadTile(tile, callback) { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _a, _b; + const rawUrl = tile.tileID.canonical.url(this.tiles, this.scheme); + const protocol = rawUrl.substring(0, rawUrl.indexOf('://')); + if (!alreadySupported && ((_a = mapLibrary._protocols) === null || _a === void 0 ? void 0 : _a.has(protocol))) { + const loadFn = (_b = mapLibrary._protocols) === null || _b === void 0 ? void 0 : _b.get(protocol); + getReqObjectUrl(loadFn, rawUrl, this.type, this._collectResourceTiming).then((url: string) => { + tile.tileID.canonical.url = function () { + delete tile.tileID.canonical.url; + return url; + }; + super.loadTile(tile, function () { + URL.revokeObjectURL(url); + callback(...arguments); + }); + }).catch((e) => { + console.error('Error loading tile', e.message); + throw e; + }); + } else { + super.loadTile(tile, callback); + } + } + }, + 'raster': class RasterCustomProtocolSourceSpecification extends mapLibrary.Style.getSourceType('raster') { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + constructor() { + super(...arguments); + } + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + loadTile(tile, callback) { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _a, _b; + const rawUrl = tile.tileID.canonical.url(this.tiles, this.scheme); + const protocol = rawUrl.substring(0, rawUrl.indexOf('://')); + if (!alreadySupported && ((_a = mapLibrary._protocols) === null || _a === void 0 ? void 0 : _a.has(protocol))) { + const loadFn = (_b = mapLibrary._protocols) === null || _b === void 0 ? void 0 : _b.get(protocol); + getReqObjectUrl(loadFn, rawUrl, this.type, this._collectResourceTiming).then((url: string) => { + tile.tileID.canonical.url = function () { + delete tile.tileID.canonical.url; + return url; + }; + super.loadTile(tile, function () { + URL.revokeObjectURL(url); + callback(...arguments); + }); + }).catch((e) => { + console.error('Error loading tile', e.message); + throw e; + }); + } else { + super.loadTile(tile, callback); + } + } + }, + 'geojson': class GeoJSONCustomProtocolSourceSpecification extends mapLibrary.Style.getSourceType('geojson') { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + constructor() { + super(...arguments); + this.type = 'geojson'; + } + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _updateWorkerData(callback) { + // eslint-disable-next-line @typescript-eslint/naming-convention + let _a, _b; + const that = this; + const data = that._data; + const done = (url) => { + super._updateWorkerData(function () { + if (url !== undefined) { + URL.revokeObjectURL(url); + } + callback(...arguments); + }); + }; + if (typeof data === 'string') { + const protocol = data.substring(0, data.indexOf('://')); + if (!alreadySupported && ((_a = mapLibrary._protocols) === null || _a === void 0 ? void 0 : _a.has(protocol))) { + const loadFn = (_b = mapLibrary._protocols) === null || _b === void 0 ? void 0 : _b.get(protocol); + getReqObjectUrl(loadFn, data, this.type, this._collectResourceTiming).then((url) => { + that._data = url; + done(url); + }); + } else { + // Use the build in code + done(undefined); + } + } else { + // If data is already GeoJSON, then pass it through + done(undefined); + } + } + } + }; +}; diff --git a/projects/arlas-components/src/lib/components/mapgl/mapgl.component.html b/projects/arlas-components/src/lib/components/mapgl/mapgl.component.html index 9df97aa5..c357b10b 100644 --- a/projects/arlas-components/src/lib/components/mapgl/mapgl.component.html +++ b/projects/arlas-components/src/lib/components/mapgl/mapgl.component.html @@ -37,8 +37,7 @@ Lat : {{currentLat | number:'1.5-5'}} Lng : {{currentLng | number:'1.5-5'}}
- +
{{FINISH_DRAWING | translate}}
\ No newline at end of file diff --git a/projects/arlas-components/src/lib/components/mapgl/mapgl.component.ts b/projects/arlas-components/src/lib/components/mapgl/mapgl.component.ts index cb17d922..814e3576 100644 --- a/projects/arlas-components/src/lib/components/mapgl/mapgl.component.ts +++ b/projects/arlas-components/src/lib/components/mapgl/mapgl.component.ts @@ -18,11 +18,9 @@ */ import { - AfterContentInit, AfterViewInit, Component, EventEmitter, - HostListener, Input, + HostListener, Input, ViewEncapsulation, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, - ViewEncapsulation } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Subject, Subscription, fromEvent } from 'rxjs'; @@ -32,7 +30,7 @@ import { ControlButton, PitchToggle, DrawControl } from './mapgl.component.contr import { paddedBounds, MapExtend, LegendData } from './mapgl.component.util'; import * as mapglJsonSchema from './mapgl.schema.json'; import { - MapLayers, BasemapStyle, BasemapStylesGroup, ExternalEvent, + MapLayers, ExternalEvent, ARLAS_ID, FILLSTROKE_LAYER_PREFIX, SCROLLABLE_ARLAS_ID, ARLAS_VSET } from './model/mapLayers'; import { MapSource } from './model/mapSource'; @@ -52,6 +50,9 @@ import * as styles from './model/theme'; import { getLayerName } from '../componentsUtils'; import { MapboxAoiDrawService } from './draw/draw.service'; import { AoiDimensions } from './draw/draw.models'; +import { BasemapStyle, BasemapsConfig } from './basemaps/basemap.config'; +import { MapboxBasemapService } from './basemaps/basemap.service'; +import { ArlasBasemaps } from './basemaps/basemaps'; export const CROSS_LAYER_PREFIX = 'arlas_cross'; @@ -99,7 +100,7 @@ export const GEOJSON_SOURCE_TYPE = 'geojson'; styleUrls: ['./mapgl.component.css'], encapsulation: ViewEncapsulation.None }) -export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterContentInit, OnDestroy { +export class MapglComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { public map: any; public draw: any; @@ -129,9 +130,8 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo public FINISH_DRAWING = 'Double click to finish drawing'; private POLYGON_LABEL_SOURCE = 'polygon_label'; - private LOCAL_STORAGE_BASEMAPS = 'arlas_last_base_map'; private ICONS_BASE_PATH = 'assets/icons/'; - + private offlineBasemapChangeSubscription!: Subscription; /** * @Input : Angular * @description element identifier given to map container @@ -172,6 +172,7 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo /** * @Input : Angular * @description Default style of the base map + * @deprecated Use [basemapConfig] instead */ @Input() public defaultBasemapStyle: BasemapStyle = { name: 'Positron Style', @@ -180,8 +181,11 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo /** * @Input : Angular * @description List of styles to apply to the base map + * @deprecated Use [basemapConfig] instead */ @Input() public basemapStyles = new Array(); + + @Input() public basemapConfig: BasemapsConfig; /** * @Input : Angular * @description Zoom of the map when it's initialized @@ -413,7 +417,6 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo public showBasemapsList = false; public layersMap: Map; - public basemapStylesGroup: BasemapStylesGroup; public currentLat: string; public currentLng: string; @@ -445,6 +448,7 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo private aoiEditSubscription: Subscription; public constructor(private http: HttpClient, private drawService: MapboxAoiDrawService, + private basemapService: MapboxBasemapService, private _snackBar: MatSnackBar, private translate: TranslateService) { this.aoiEditSubscription = this.drawService.editAoi$.subscribe(ae => this.onAoiEdit.emit(ae)); } @@ -552,7 +556,9 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo return mapglJsonSchema; } - public ngOnInit() { } + public ngOnInit() { + this.offlineBasemapChangeSubscription = this.basemapService.offlineBasemapChanged$.subscribe(() => this.reorderLayers()); + } /** puts the visualisation set list in the new order after dropping */ public drop(event: CdkDragDrop) { @@ -617,6 +623,10 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo } } } + this.map.getStyle().layers + .map(layer => layer.id) + .filter(id => id.indexOf('.cold') >= 0 || id.indexOf('.hot') >= 0).forEach(id => this.map.moveLayer(id)); + } public ngOnChanges(changes: SimpleChanges): void { @@ -665,56 +675,7 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo } } - public setBaseMapStyle(style: string | mapboxgl.Style) { - if (this.map) { - if (typeof this.basemapStylesGroup.selectedBasemapStyle.styleFile === 'string') { - this.http.get(this.basemapStylesGroup.selectedBasemapStyle.styleFile).subscribe((s: any) => this.setStyle(s, style)); - } else { - this.setStyle(this.basemapStylesGroup.selectedBasemapStyle.styleFile, style); - } - } - } - - public setStyle(s: mapboxgl.Style, style: string | mapboxgl.Style) { - const selectedBasemapLayersSet = new Set(); - const layers: Array = (this.map).getStyle().layers; - const sources = (this.map).getStyle().sources; - if (s.layers) { - s.layers.forEach(l => selectedBasemapLayersSet.add(l.id)); - } - const layersToSave = new Array(); - const sourcesToSave = new Array(); - layers.filter((l: mapboxgl.Layer) => !selectedBasemapLayersSet.has(l.id)).forEach(l => { - layersToSave.push(l); - if (sourcesToSave.filter(ms => ms.id === l.source.toString()).length === 0) { - sourcesToSave.push({ id: l.source.toString(), source: sources[l.source.toString()] }); - } - }); - const sourcesToSaveSet = new Set(); - sourcesToSave.forEach(mapSource => sourcesToSaveSet.add(mapSource.id)); - if (this.mapSources) { - this.mapSources.forEach(mapSource => { - if (!sourcesToSaveSet.has(mapSource.id)) { - sourcesToSave.push(mapSource); - } - }); - } - this.map.setStyle(style).once('styledata', () => { - this.addSourcesToMap(sourcesToSave, this.map); - layersToSave.forEach(l => this.map.addLayer(l)); - this.onBasemapChanged.next(true); - }); - } - - public ngAfterContentInit(): void { - /** [basemapStylesGroup] object includes the list of basemap styles and which one is selected */ - this.setBasemapStylesGroup(this.getAfterViewInitBasemapStyle()); - } - public ngAfterViewInit() { - - const afterViewInitbasemapStyle: BasemapStyle = this.getAfterViewInitBasemapStyle(); - /** init values */ if (!this.initCenter) { this.initCenter = [0, 0]; @@ -728,10 +689,10 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo if (this.minZoom === undefined || this.minZoom === null) { this.maxZoom = 0; } - + this.basemapService.setBasemaps(new ArlasBasemaps(this.basemapConfig, this.defaultBasemapStyle, this.basemapStyles)); this.map = new mapboxgl.Map({ container: this.id, - style: afterViewInitbasemapStyle.styleFile, + style: this.basemapService.getInitStyle(), center: this.initCenter, zoom: this.initZoom, maxZoom: this.maxZoom, @@ -747,7 +708,6 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo attributionControl: false }); (this.map).addControl(new mapboxgl.AttributionControl(), this.mapAttributionPosition); - this.drawService.setMap(this.map); fromEvent(window, 'beforeunload').subscribe(() => { const bounds = (this.map).getBounds(); @@ -801,6 +761,7 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo }; this.map.boxZoom.disable(); this.map.on('load', () => { + this.basemapService.addOfflineBasemap(this.map); this.draw.changeMode('static'); if (this.icons) { this.icons.forEach(icon => { @@ -1338,10 +1299,8 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo this.deleteSelectedItem(); } - public onChangeBasemapStyle(selectedStyle: BasemapStyle) { - this.setBaseMapStyle(selectedStyle.styleFile); - localStorage.setItem(this.LOCAL_STORAGE_BASEMAPS, JSON.stringify(selectedStyle)); - this.basemapStylesGroup.selectedBasemapStyle = selectedStyle; + public onChangeBasemapStyle() { + this.onBasemapChanged.next(true); } /** @@ -1420,7 +1379,12 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo } public ngOnDestroy(): void { - this.aoiEditSubscription.unsubscribe(); + if (!!this.aoiEditSubscription) { + this.aoiEditSubscription.unsubscribe(); + } + if (!!this.offlineBasemapChangeSubscription) { + this.offlineBasemapChangeSubscription.unsubscribe(); + } } public selectFeaturesByCollection(features: Array, collection: string) { @@ -1535,6 +1499,7 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo } }); + this.reorderLayers(); } } @@ -1542,55 +1507,6 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo this.mapLayers.layers .filter(layer => this.mapLayers.externalEventLayers.map(e => e.id).indexOf(layer.id) >= 0) .forEach(l => this.addLayer(l.id)); - - - } - - private getAllBasemapStyles(): Array { - const allBasemapStyles = new Array(); - if (this.basemapStyles) { - this.basemapStyles.forEach(b => allBasemapStyles.push(b)); - /** Check whether to add [defaultBasemapStyle] to [allBasemapStyles] list*/ - if (this.basemapStyles.map(b => b.name).filter(n => n === this.defaultBasemapStyle.name).length === 0) { - allBasemapStyles.push(this.defaultBasemapStyle); - } - } else { - allBasemapStyles.push(this.defaultBasemapStyle); - } - return allBasemapStyles; - } - - /** - * @description returns the basemap style that is displayed when the map is loaded for the first time - */ - private getAfterViewInitBasemapStyle(): BasemapStyle { - if (!this.defaultBasemapStyle) { - throw new Error('[defaultBasemapStyle] input is null or undefined.'); - } - const allBasemapStyles = this.getAllBasemapStyles(); - const localStorageBasemapStyle: BasemapStyle = JSON.parse(localStorage.getItem(this.LOCAL_STORAGE_BASEMAPS)); - /** check if a basemap style is saved in local storage and that it exists in [allBasemapStyles] list */ - if (localStorageBasemapStyle && allBasemapStyles.filter(b => b.name === localStorageBasemapStyle.name - && b.styleFile === localStorageBasemapStyle.styleFile).length > 0) { - return localStorageBasemapStyle; - } else { - localStorage.setItem(this.LOCAL_STORAGE_BASEMAPS, JSON.stringify(this.defaultBasemapStyle)); - return this.defaultBasemapStyle; - } - } - - /** - * @param selectedBasemapStyle the selected basemap style - * @description This method sets the [basemapStylesGroup] object that includes the list of basemapStyles - * and which basemapStyle is selected. - */ - private setBasemapStylesGroup(selectedBasemapStyle: BasemapStyle) { - const allBasemapStyles = this.getAllBasemapStyles(); - /** basemapStylesGroup object includes the list of basemap styles and which one is selected */ - this.basemapStylesGroup = { - basemapStyles: allBasemapStyles, - selectedBasemapStyle: selectedBasemapStyle - }; } private addLayer(layerId: string): void { @@ -1784,8 +1700,6 @@ export class MapglComponent implements OnInit, AfterViewInit, OnChanges, AfterCo } } - - private setStrokeLayoutVisibility(layerId: string, visibility: string): void { const layer = this.layersMap.get(layerId); if (layer.type === 'fill') { diff --git a/projects/arlas-components/src/lib/components/mapgl/model/mapLayers.ts b/projects/arlas-components/src/lib/components/mapgl/model/mapLayers.ts index 401a5857..39caf85a 100644 --- a/projects/arlas-components/src/lib/components/mapgl/model/mapLayers.ts +++ b/projects/arlas-components/src/lib/components/mapgl/model/mapLayers.ts @@ -19,17 +19,6 @@ import { AnyLayer } from 'mapbox-gl'; -export interface BasemapStyle { - name: string; - styleFile: string | mapboxgl.Style; - image?: string; -} - -export interface BasemapStylesGroup { - basemapStyles: Array; - selectedBasemapStyle: BasemapStyle; -} - export interface MapLayers { layers: Array; externalEventLayers?: Array; diff --git a/projects/arlas-components/src/lib/services/mapgl.service.ts b/projects/arlas-components/src/lib/services/mapgl.service.ts new file mode 100644 index 00000000..4a0e9328 --- /dev/null +++ b/projects/arlas-components/src/lib/services/mapgl.service.ts @@ -0,0 +1,48 @@ +/* + * Licensed to Gisaïa under one or more contributor + * license agreements. See the NOTICE.txt file distributed with + * this work for additional information regarding copyright + * ownership. Gisaïa licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Injectable } from '@angular/core'; +import { MapSource } from '../components/mapgl/model/mapSource'; + +@Injectable({ + providedIn: 'root' +}) +export class MapglService { + + /** + * @description Add map sources + */ + public addSourcesToMap(sources: Array, map: any) { + // Add sources defined as input in mapSources; + const mapSourcesMap = new Map(); + if (sources) { + sources.forEach(mapSource => { + mapSourcesMap.set(mapSource.id, mapSource); + }); + mapSourcesMap.forEach((mapSource, id) => { + if (map.getSource(id) === undefined) { + map.addSource(id, mapSource.source); + } + }); + } + } + +} + + diff --git a/projects/arlas-components/src/public-api.ts b/projects/arlas-components/src/public-api.ts index cf8f9040..a2654282 100644 --- a/projects/arlas-components/src/public-api.ts +++ b/projects/arlas-components/src/public-api.ts @@ -15,9 +15,12 @@ export { MapglImportModule } from './lib/components/mapgl-import/mapgl-import.mo export { MapglLayerIconComponent } from './lib/components/mapgl-layer-icon/mapgl-layer-icon.component'; export { MapglLayerIconModule } from './lib/components/mapgl-layer-icon/mapgl-layer-icon.module'; export { - MapLayers, BasemapStyle, ARLAS_VSET, LayerEvents, BasemapStylesGroup, FILLSTROKE_LAYER_PREFIX, ARLAS_ID, PaintValue, + MapLayers, ARLAS_VSET, LayerEvents, FILLSTROKE_LAYER_PREFIX, ARLAS_ID, PaintValue, ExternalEventLayer, ExternalEvent, SCROLLABLE_ARLAS_ID, FillStroke, LayerMetadata, PaintColor, HOVER_LAYER_PREFIX, SELECT_LAYER_PREFIX } from './lib/components/mapgl/model/mapLayers'; +export { + BasemapStyle, BasemapsConfig, OfflineBasemapTheme, OfflineBasemapsConfig, OnlineBasemapConfig +} from './lib/components/mapgl/basemaps/basemap.config'; export { MapSource } from './lib/components/mapgl/model/mapSource'; export { MapExtend, LegendData, Legend, PROPERTY_SELECTOR_SOURCE } from './lib/components/mapgl/mapgl.component.util'; export { AoiDimensions as AoiEdition } from './lib/components/mapgl/draw/draw.models'; diff --git a/projects/arlas-components/tsconfig.lib.json b/projects/arlas-components/tsconfig.lib.json index ad24bf84..a9abcb3e 100644 --- a/projects/arlas-components/tsconfig.lib.json +++ b/projects/arlas-components/tsconfig.lib.json @@ -8,6 +8,7 @@ "declarationMap": true, "inlineSources": true, "types": [], + "allowJs": true, "lib": [ "dom", "es2018"