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.",