-
-
-
-
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'}}
-
+
1">{{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"