diff --git a/gulpfile.js b/gulpfile.js
index 9781737..a2f99f8 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -43,6 +43,7 @@ var paths = {
'js/Browser.js',
'js/Util.js',
'js/Map.js',
+ 'js/LayersConfig.js',
'js/router/BRouter.js',
'js/plugin/*.js',
'js/control/*.js',
diff --git a/index.html b/index.html
index 0919c95..035afa5 100644
--- a/index.html
+++ b/index.html
@@ -286,6 +286,8 @@
+
+
diff --git a/js/LayersConfig.js b/js/LayersConfig.js
new file mode 100644
index 0000000..3826701
--- /dev/null
+++ b/js/LayersConfig.js
@@ -0,0 +1,239 @@
+BR.LayersConfig = L.Class.extend({
+ defaultBaseLayers: [
+ 'standard',
+ 'osm-mapnik-german_style',
+ 'OpenTopoMap',
+ 'Stamen.Terrain',
+ 'Esri.WorldImagery'
+ ],
+ defaultOverlays: [
+ 'HikeBike.HillShading',
+ 'Waymarked_Trails-Cycling',
+ 'Waymarked_Trails-Hiking'
+ ],
+
+ initialize: function (map) {
+ this._map = map;
+
+ this._addLeafletProvidersLayers();
+
+ this._customizeLayers();
+ },
+
+ _addLeafletProvidersLayers: function () {
+ var includeList = [
+ 'Stamen.Terrain',
+ 'MtbMap',
+ 'OpenStreetMap.CH',
+ 'HikeBike.HillShading',
+ 'Esri.WorldImagery'
+ ];
+
+ for (var i = 0; i < includeList.length; i++) {
+ var id = includeList[i];
+ var obj = {
+ geometry: null,
+ properties: {
+ id: id,
+ name: id.replace('.', ' '),
+ dataSource: 'leaflet-providers'
+ },
+ type: "Feature"
+ };
+ BR.layerIndex[id] = obj;
+ }
+ },
+
+ _customizeLayers: function () {
+ // add Thunderforest API key variable
+ BR.layerIndex['opencylemap'].properties.url = 'https://{switch:a,b,c}.tile.thunderforest.com/cycle/{zoom}/{x}/{y}.png?apikey={keys_thunderforest}';
+ BR.layerIndex['1061'].properties.url = 'http://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey={keys_thunderforest}';
+
+ BR.layerIndex['HikeBike.HillShading'].properties.overlay = true;
+
+
+ function setProperty(layerId, key, value) {
+ var layer = BR.layerIndex[layerId];
+ if (layer) {
+ layer.properties[key] = value;
+ } else {
+ console.error('Layer not found: ' + layerId);
+ }
+ }
+ function setMapUrl(layerId, url) {
+ setProperty(layerId, 'mapUrl', url);
+ }
+ function setName(layerId, url) {
+ setProperty(layerId, 'name', url);
+ }
+
+ // Layer attribution here only as short link to original site,
+ // to keep current position use placeholders: {zoom}/{lat}/{lon}
+ // Copyright attribution in index.html #credits
+ setMapUrl('standard', '
OpenStreetMap');
+ setMapUrl('osm-mapnik-german_style', '
OpenStreetMap.de');
+ setMapUrl('OpenTopoMap', '
OpenTopoMap');
+ setMapUrl('Stamen.Terrain', '
' + i18next.t('map.layer.stamen-terrain') + '');
+ setMapUrl('opencylemap', '
OpenCycleMap');
+ setMapUrl('1061', '
Outdoors');
+ setMapUrl('Esri.WorldImagery', '
' + i18next.t('credits.esri-tiles') + '');
+ setMapUrl('HikeBike.HillShading', '
' + i18next.t('map.hikebike-hillshading') + '');
+ setMapUrl('Waymarked_Trails-Cycling', '
' + i18next.t('map.cycling') + '');
+ setMapUrl('Waymarked_Trails-Hiking', '
' + i18next.t('map.hiking') + '');
+
+ setName('standard', i18next.t('map.layer.osm'));
+ setName('osm-mapnik-german_style', i18next.t('map.layer.osmde'));
+ setName('OpenTopoMap', i18next.t('map.layer.topo'));
+ setName('Stamen.Terrain', i18next.t('map.layer.stamen-terrain'));
+ setName('opencylemap', i18next.t('map.layer.cycle'));
+ setName('1061', i18next.t('map.layer.outdoors'));
+ setName('Esri.WorldImagery', i18next.t('map.layer.esri'));
+ setName('HikeBike.HillShading', i18next.t('map.layer.hikebike-hillshading'));
+ setName('Waymarked_Trails-Cycling', i18next.t('map.layer.cycling'));
+ setName('Waymarked_Trails-Hiking', i18next.t('map.layer.hiking'));
+ },
+
+ isDefaultLayer: function(id, overlay) {
+ var result = false;
+ if (overlay) {
+ result = this.defaultOverlays.indexOf(id) > -1;
+ } else {
+ result = this.defaultBaseLayers.indexOf(id) > -1;
+ }
+ return result;
+ },
+
+ getBaseLayers: function() {
+ return this._getLayers(this.defaultBaseLayers);
+ },
+
+ getOverlays: function() {
+ return this._getLayers(this.defaultOverlays);
+ },
+
+ _getLayers: function(ids) {
+ var layers = {};
+
+ for (var i = 0; i < ids.length; i++) {
+ var layerId = ids[i];
+ var layerData = BR.layerIndex[layerId];
+
+ if (layerData) {
+ layers[layerData.properties.name] = this.createLayer(layerData);
+ } else {
+ console.error('Layer not found: ' + layerId);
+ }
+ }
+
+ return layers;
+ },
+
+ // own convention: key placeholder with prefix
+ // e.g. ?api_key={keys_openrouteservice}
+ getKeyName: function (url) {
+ var result = null;
+ // L.Util.template only matches [\w_-]
+ var prefix = 'keys_';
+ var regex = new RegExp('{' + prefix + '([^}]*)}');
+ var found, name;
+
+ if (!url) return result;
+
+ found = url.match(regex);
+ if (found) {
+ name = found[1];
+ result = {
+ name: name,
+ urlVar: prefix + name
+ };
+ }
+
+ return result;
+ },
+
+ createLayer: function (layerData) {
+ var props = layerData.properties;
+ var url = props.url;
+ var layer;
+
+ // JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
+ // Leaflet: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
+ function convertUrlJosm(url) {
+ var rxSwitch = /{switch:[^}]*}/;
+ var rxZoom = /{zoom}/g;
+ var result = url.replace(rxSwitch, '{s}');
+ result = result.replace(rxZoom, '{z}');
+ return result;
+ }
+
+ // JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
+ // Leaflet: ['a','b','c']
+ function getSubdomains(url) {
+ var result = 'abc';
+ var regex = /{switch:([^}]*)}/;
+ var found = url.match(regex);
+ if (found) {
+ result = found[1].split(',');
+ }
+ return result;
+ }
+
+
+ var options = {
+ maxZoom: this._map.getMaxZoom(),
+ mapUrl: props.mapUrl
+ };
+
+ var keyObj = this.getKeyName(url);
+ if (keyObj && BR.keys[keyObj.name]) {
+ options[keyObj.urlVar] = BR.keys[keyObj.name];
+ }
+
+ if (props.dataSource === 'leaflet-providers') {
+ layer = L.tileLayer.provider(props.id);
+
+ var layerOptions = L.Util.extend(options, {
+ maxNativeZoom: layer.options.maxZoom,
+ });
+ L.setOptions(layer, layerOptions);
+
+ } else if (props.dataSource === 'LayersCollection') {
+ layer = L.tileLayer(url, L.Util.extend(options, {
+ minZoom: props.minZoom,
+ maxNativeZoom: props.maxZoom,
+ }));
+ if (props.subdomains) {
+ layer.subdomains = props.subdomains;
+ }
+ } else {
+ // JOSM
+ var url = convertUrlJosm(url);
+
+ var josmOptions = L.Util.extend(options, {
+ minZoom: props.min_zoom,
+ maxNativeZoom: props.max_zoom,
+ subdomains: getSubdomains(url),
+ });
+
+ if (props.type && props.type === 'wms') {
+ layer = L.tileLayer.wms(url, L.Util.extend(josmOptions, {
+ layers: props.layers,
+ format: props.format
+ }));
+ } else {
+ layer = L.tileLayer(url, josmOptions);
+ }
+ }
+
+ var getAttribution = function () {
+ return this.options.mapUrl;
+ }
+ layer.getAttribution = getAttribution;
+
+ return layer;
+ }
+});
+
+BR.layersConfig = function (map) {
+ return new BR.LayersConfig(map);
+};
diff --git a/js/Map.js b/js/Map.js
index 7724248..3268d29 100644
--- a/js/Map.js
+++ b/js/Map.js
@@ -8,59 +8,6 @@ BR.Map = {
var maxZoom = 19;
- // Layer attribution here only as short link to original site,
- // to keep current position use placeholders: {zoom}/{lat}/{lon}
- // Copyright attribution in index.html #credits
-
- var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: maxZoom,
- attribution: '
OpenStreetMap'
- });
-
- var osmde = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', {
- maxNativeZoom: 19,
- maxZoom: maxZoom,
- attribution: '
OpenStreetMap.de'
- });
-
- var topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
- maxNativeZoom: 17,
- maxZoom: maxZoom,
- attribution: '
OpenTopoMap'
- });
-
- var thunderforestAuth = BR.keys.thunderforest ? '?apikey=' + BR.keys.thunderforest : '';
- var cycle = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png' + thunderforestAuth, {
- maxNativeZoom: 18,
- maxZoom: maxZoom,
- attribution: '
OpenCycleMap'
- });
- var outdoors = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png' + thunderforestAuth, {
- maxNativeZoom: 18,
- maxZoom: maxZoom,
- attribution: '
Outdoors'
- });
-
- var esri = L.tileLayer('https://{s}.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
- maxNativeZoom: 19,
- maxZoom: maxZoom,
- subdomains: ['server', 'services'],
- attribution: '
' + i18next.t('credits.esri-tiles') + ''
- });
-
- var cycling = L.tileLayer('https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png', {
- maxNativeZoom: 18,
- opacity: 0.7,
- maxZoom: maxZoom,
- attribution: '
' + i18next.t('map.cycling') + ''
- });
- var hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {
- maxNativeZoom: 18,
- opacity: 0.7,
- maxZoom: maxZoom,
- attribution: '
' + i18next.t('map.hiking') + ''
- });
-
map = new L.Map('map', {
zoomControl: false, // add it manually so that we can translate it
worldCopyJump: true,
@@ -85,16 +32,9 @@ BR.Map = {
new L.Control.PermalinkAttribution().addTo(map);
map.attributionControl.setPrefix(false);
- var baseLayers = {}
- baseLayers[i18next.t('map.layer.osm')] = osm;
- baseLayers[i18next.t('map.layer.osmde')] = osmde;
- baseLayers[i18next.t('map.layer.topo')] = topo;
- baseLayers[i18next.t('map.layer.cycle')] = cycle;
- baseLayers[i18next.t('map.layer.outdoors')] = outdoors;
- baseLayers[i18next.t('map.layer.esri')] = esri;
- var overlays = {}
- overlays[i18next.t('map.layer.cycling')] = cycling;
- overlays[i18next.t('map.layer.hiking')] = hiking;
+ var layersConfig = BR.layersConfig(map);
+ var baseLayers = layersConfig.getBaseLayers();
+ var overlays = layersConfig.getOverlays();
if (BR.keys.bing) {
baseLayers[i18next.t('map.layer.bing')] = new BR.BingLayer(BR.keys.bing);
@@ -129,7 +69,7 @@ BR.Map = {
map.addLayer(defaultLayer);
}
- layersControl = BR.layersTab(baseLayers, overlays).addTo(map);
+ layersControl = BR.layersTab(layersConfig, baseLayers, overlays).addTo(map);
var secureContext = 'isSecureContext' in window ? isSecureContext : location.protocol === 'https:';
if (secureContext) {
diff --git a/js/control/LayersTab.js b/js/control/LayersTab.js
index 66e49e8..0247fb4 100644
--- a/js/control/LayersTab.js
+++ b/js/control/LayersTab.js
@@ -2,6 +2,12 @@ BR.LayersTab = L.Control.Layers.extend({
previewLayer: null,
saveLayers: [],
+ initialize: function (layersConfig, baseLayers, overlays, options) {
+ L.Control.Layers.prototype.initialize.call(this, baseLayers, overlays, options);
+
+ this.layersConfig = layersConfig;
+ },
+
addTo: function (map) {
this._map = map;
this.onAdd(map);
@@ -12,33 +18,32 @@ BR.LayersTab = L.Control.Layers.extend({
this.initButtons();
- this.addLeafletProvidersLayers();
-
var structure = {
'Base layers': {
'Worldwide international': [
'standard',
'OpenTopoMap',
'Stamen.Terrain',
- 'HDM_HOT',
+ 'Esri.WorldImagery',
'wikimedia-map',
+ 'HDM_HOT',
+ '1010', // OpenStreetMap.se (Hydda.Full)
'opencylemap',
- "1061", // Thunderforest Outdoors
- "1065", // Hike & Bike Map
- "1016", // 4UMaps,
- "openmapsurfer"
+ '1061', // Thunderforest Outdoors
+ '1065', // Hike & Bike Map
+ '1016', // 4UMaps,
+ 'openmapsurfer'
],
'Worldwide monolingual': [
'osm-mapnik-german_style',
'osmfr',
- "1023", // Osmapa.pl - Mapa OpenStreetMap Polska
- "1021", // kosmosnimki.ru
- "1017", // sputnik.ru
- "1010" // OpenStreetMap.se (Hydda.Full)
+ '1023', // Osmapa.pl - Mapa OpenStreetMap Polska
+ '1021', // kosmosnimki.ru
+ '1017' // sputnik.ru
],
'Europe': [
'MtbMap',
- "1069", // MRI (maps.refuges.info)
+ '1069' // MRI (maps.refuges.info)
],
'Country': [
'topplus-open',
@@ -76,7 +81,7 @@ BR.LayersTab = L.Control.Layers.extend({
'mapaszlakow-bike',
'mapaszlakow-hike',
'mapaszlakow-mtb',
- 'mapaszlakow-incline',
+ 'mapaszlakow-incline'
]
}
]
@@ -119,13 +124,14 @@ BR.LayersTab = L.Control.Layers.extend({
var obj = this._getLayerObjByName(data.node.text);
if (!obj) return;
+ this.removeLayer(obj.layer);
+
if (this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
if (!obj.overlay) {
this.addFirstLayer();
}
}
- this.removeLayer(obj.layer);
};
$('#optional-layers-tree')
@@ -188,21 +194,45 @@ BR.LayersTab = L.Control.Layers.extend({
var data = [];
var self = this;
+ function createRootNode(name) {
+ var children = [];
+ var rootNode = {
+ 'text': name,
+ 'state': {
+ 'disabled': true
+ },
+ 'children': children
+ };
+ return rootNode;
+ }
+
+ function createNode(id, layerData) {
+ var props = layerData.properties;
+ var url = props.url;
+ var keyObj = self.layersConfig.getKeyName(url);
+ var childNode = null;
+
+ // when key required only add if configured
+ if (!keyObj || keyObj && BR.keys[keyObj.name]) {
+ childNode = {
+ 'id': id,
+ 'text': props.name,
+ 'state': {
+ 'checked': self.layersConfig.isDefaultLayer(id, props.overlay)
+ }
+ };
+ }
+ return childNode;
+ }
+
function walkTree(inTree, outTree) {
function walkObject(obj) {
for (name in obj) {
var value = obj[name];
- var children = [];
- var rootNode = {
- 'text': name,
- 'state': {
- 'disabled': true
- },
- 'children': children
- };
- outTree.push(rootNode);
+ var rootNode = createRootNode(name)
- walkTree(value, children);
+ outTree.push(rootNode);
+ walkTree(value, rootNode.children);
}
}
@@ -215,16 +245,8 @@ BR.LayersTab = L.Control.Layers.extend({
var layer = BR.layerIndex[entry];
if (layer) {
- var props = layer.properties;
- var url = props.url;
- var keyName = self.getKeyName(url);
-
- // when key required only add if configured
- if (!keyName || keyName && BR.keys[keyName]) {
- var childNode = {
- 'id': entry,
- 'text': props.name
- };
+ var childNode = createNode(entry, layer);
+ if (childNode) {
outTree.push(childNode);
}
} else {
@@ -241,51 +263,13 @@ BR.LayersTab = L.Control.Layers.extend({
return data;
},
- addLeafletProvidersLayers: function () {
- var includeList = [
- 'Stamen.Terrain',
- 'MtbMap',
- 'OpenStreetMap.CH',
- 'HikeBike.HillShading'
- ];
-
- for (var i = 0; i < includeList.length; i++) {
- var id = includeList[i];
- var obj = {
- geometry: null,
- properties: {
- id: id,
- name: id.replace('.', ' '),
- dataSource: 'leaflet-providers'
- },
- type: "Feature"
- };
- BR.layerIndex[id] = obj;
- }
-
- BR.layerIndex['HikeBike.HillShading'].properties.overlay = true;
- },
-
- // own convention: key placeholder prefixed with 'key_'
- // e.g. ?api_key={keys_openrouteservice}
- getKeyName: function (url) {
- var name = null;
- var regex = /{keys_([^}]*)}/;
- var found;
-
- if (!url) return name;
-
- found = url.match(regex);
- if (found) {
- name = found[1];
- }
-
- return name;
- },
-
- addFirstLayer() {
- if (this._layers.length > 0) {
- this._map.addLayer(this._layers[0].layer);
+ addFirstLayer: function () {
+ for (var i = 0; i < this._layers.length; i++) {
+ var obj = this._layers[i];
+ if (!obj.overlay) {
+ this._map.addLayer(obj.layer);
+ break;
+ }
}
},
@@ -298,73 +282,9 @@ BR.LayersTab = L.Control.Layers.extend({
},
createLayer: function (layerData) {
- var props = layerData.properties;
- var url = props.url;
- var layer;
-
- // JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
- // Leaflet: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
- function convertUrlJosm(url) {
- var rxSwitch = /{switch:[^}]*}/;
- var rxZoom = /{zoom}/g;
- var result = url.replace(rxSwitch, '{s}');
- result = result.replace(rxZoom, '{z}');
- return result;
- }
-
- // JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
- // Leaflet: ['a','b','c']
- function getSubdomains(url) {
- var result = 'abc';
- var regex = /{switch:([^}]*)}/;
- var found = url.match(regex);
- if (found) {
- result = found[1].split(',');
- }
- return result;
- }
-
- var options = {
- maxZoom: this._map.getMaxZoom(),
- zIndex: this._lastZIndex + 1
- };
-
- var keyName = this.getKeyName(url);
- if (keyName && BR.keys[keyName]) {
- options['keys_' + keyName] = BR.keys[keyName];
- }
-
- if (props.dataSource === 'leaflet-providers') {
- layer = L.tileLayer.provider(props.id);
- } else if (props.dataSource === 'LayersCollection') {
- layer = L.tileLayer(url, L.Util.extend(options, {
- minZoom: props.minZoom,
- maxNativeZoom: props.maxZoom,
- }));
- if (props.subdomains) {
- layer.subdomains = props.subdomains;
- }
- } else {
- // JOSM
- var url = convertUrlJosm(url);
-
- var josmOptions = L.Util.extend(options, {
- minZoom: props.min_zoom,
- maxNativeZoom: props.max_zoom,
- subdomains: getSubdomains(url),
- });
-
- if (props.type && props.type === 'wms') {
- layer = L.tileLayer.wms(url, L.Util.extend(josmOptions, {
- layers: props.layers,
- format: props.format
- }));
- } else {
- layer = L.tileLayer(url, josmOptions);
- }
- }
-
- return layer
+ var layer = this.layersConfig.createLayer(layerData);
+ layer.options.zIndex = this._lastZIndex + 1;
+ return layer;
},
removeSelectedLayers: function () {
diff --git a/locales/en.json b/locales/en.json
index 1cec229..a941489 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -74,6 +74,7 @@
"delete-route": "Delete route?",
"draw-route-start": "Draw route (D key)",
"draw-route-stop": "Stop drawing route (ESC key)",
+ "hikebike-hillshading": "Hillshading",
"hiking": "Hiking",
"layer": {
"bing": "Bing Aerial",
@@ -81,10 +82,12 @@
"cycling": "Cycling (Waymarked Trails)",
"digitalglobe": "DigitalGlobe Recent Imagery",
"esri": "Esri World Imagery",
+ "hikebike-hillshading": "Hillshading (Hike & Bike Map)",
"hiking": "Hiking (Waymarked Trails)",
"osm": "OpenStreetMap",
"osmde": "OpenStreetMap.de",
"outdoors": "Outdoors (Thunderforest)",
+ "stamen-terrain": "Terrain (Stamen)",
"strava-segments": "Strava segments",
"topo": "OpenTopoMap"
},
@@ -157,8 +160,12 @@
"title": "Itinerary"
},
"layers": {
+ "collapse": "Collapse all",
"custom-layers": "Custom layers",
"customize": "Add or remove custom layers",
+ "expand": "Expand all",
+ "optional": "Add or remove optional layers",
+ "optional-layers": "More",
"table": {
"URL": "URL",
"empty": "No custom layer configured yet.",