From 2e68341106962c37c73e806acdfbed70b93ac8c4 Mon Sep 17 00:00:00 2001 From: Oliver Jowett Date: Sat, 2 Jul 2016 21:12:31 +0100 Subject: [PATCH] Layer switching support, ChartBundle, Bing maps. --- debian/copyright | 6 + public_html/config.js | 18 +- public_html/gmap.html | 5 +- public_html/ol3/ol3-layerswitcher.css | 92 +++++++++ public_html/ol3/ol3-layerswitcher.js | 283 ++++++++++++++++++++++++++ public_html/script.js | 121 ++++++++++- 6 files changed, 498 insertions(+), 27 deletions(-) create mode 100644 public_html/ol3/ol3-layerswitcher.css create mode 100644 public_html/ol3/ol3-layerswitcher.js diff --git a/debian/copyright b/debian/copyright index b52968e..1ebfe50 100644 --- a/debian/copyright +++ b/debian/copyright @@ -44,6 +44,12 @@ Comment: _generic_plane_svg was added with https://github.com/mutability/dump109 Files: public_html/jquery/* Copyright: 2015 jQuery Foundation and other contributors License: MIT +Source: http://www.jquery.com/ + +Files: public_html/ol3/ol3-layerswitcher.js public_html/ol3/ol3-layerswitcher.css +Copyright: Matt Walker +License: MIT +Source: https://github.com/walkermatt/ol3-layerswitcher Files: debian/* Copyright: 2014,2015 Oliver Jowett diff --git a/public_html/config.js b/public_html/config.js index 10c2bba..b513b26 100644 --- a/public_html/config.js +++ b/public_html/config.js @@ -39,21 +39,6 @@ SiteLat = 45.0; // position of the marker SiteLon = 9.0; SiteName = "My Radar Site"; // tooltip of the marker - -// Extra map types to include. These work for maps with 256x256 tiles where a -// URL can be constructed by simple substition of x/y tile number and zoom level -var ExtraMapTypes = { - 'OpenStreetMap' : 'http://tile.openstreetmap.org/{z}/{x}/{y}.png', - // NB: the following generally only cover the US - 'Sectional Charts' : 'http://wms.chartbundle.com/tms/1.0.0/sec/{z}/{x}/{y}.png?origin=nw', - 'Terminal Charts' : 'http://wms.chartbundle.com/tms/1.0.0/tac/{z}/{x}/{y}.png?origin=nw', - 'World Charts' : 'http://wms.chartbundle.com/tms/1.0.0/wac/{z}/{x}/{y}.png?origin=nw', - 'IFR Low Charts' : 'http://wms.chartbundle.com/tms/1.0.0/enrl/{z}/{x}/{y}.png?origin=nw', - 'IFR Area Charts' : 'http://wms.chartbundle.com/tms/1.0.0/enra/{z}/{x}/{y}.png?origin=nw', - 'IFR High Charts' : 'http://wms.chartbundle.com/tms/1.0.0/enrh/{z}/{x}/{y}.png?origin=nw' -}; - - // -- Marker settings ------------------------------------- // These settings control the coloring of aircraft by altitude. @@ -127,3 +112,6 @@ ShowFlags = true; // Path to country flags (can be a relative or absolute URL; include a trailing /) FlagPath = "flags-tiny/"; + +// Provide a Bing Maps API key here to enable the Bing imagery layer. +BingMapsAPIKey = null; diff --git a/public_html/gmap.html b/public_html/gmap.html index f46b14a..4a053fb 100644 --- a/public_html/gmap.html +++ b/public_html/gmap.html @@ -7,9 +7,12 @@ - + + + + diff --git a/public_html/ol3/ol3-layerswitcher.css b/public_html/ol3/ol3-layerswitcher.css new file mode 100644 index 0000000..088abd9 --- /dev/null +++ b/public_html/ol3/ol3-layerswitcher.css @@ -0,0 +1,92 @@ +.layer-switcher.shown.ol-control { + background-color: transparent; +} + +.layer-switcher.shown.ol-control:hover { + background-color: transparent; +} + +.layer-switcher { + position: absolute; + top: 3.5em; + right: 0.5em; + text-align: left; +} + +.layer-switcher.shown { + bottom: 3em; +} + +.layer-switcher .panel { + padding: 0 1em 0 0; + margin: 0; + border: 4px solid #eee; + border-radius: 4px; + background-color: white; + display: none; + max-height: 100%; + overflow-y: auto; +} + +.layer-switcher.shown .panel { + display: block; +} + +.layer-switcher button { + float: right; + width: 38px; + height: 38px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACE1BMVEX///8A//8AgICA//8AVVVAQID///8rVVVJtttgv98nTmJ2xNgkW1ttyNsmWWZmzNZYxM4gWGgeU2JmzNNr0N1Rwc0eU2VXxdEhV2JqytQeVmMhVmNoydUfVGUgVGQfVGQfVmVqy9hqy9dWw9AfVWRpydVry9YhVmMgVGNUw9BrytchVWRexdGw294gVWQgVmUhVWPd4N6HoaZsy9cfVmQgVGRrytZsy9cgVWQgVWMgVWRsy9YfVWNsy9YgVWVty9YgVWVry9UgVWRsy9Zsy9UfVWRsy9YgVWVty9YgVWRty9Vsy9aM09sgVWRTws/AzM0gVWRtzNYgVWRuy9Zsy9cgVWRGcHxty9bb5ORbxdEgVWRty9bn6OZTws9mydRfxtLX3Nva5eRix9NFcXxOd4JPeINQeIMiVmVUws9Vws9Vw9BXw9BYxNBaxNBbxNBcxdJexdElWWgmWmhjyNRlx9IqXGtoipNpytVqytVryNNrytZsjZUuX210k5t1y9R2zNR3y9V4lp57zth9zdaAnKOGoaeK0NiNpquV09mesrag1tuitbmj1tuj19uktrqr2d2svcCu2d2xwMO63N+7x8nA3uDC3uDFz9DK4eHL4eLN4eIyYnDX5OM5Z3Tb397e4uDf4uHf5uXi5ePi5+Xj5+Xk5+Xm5+Xm6OY6aHXQ19fT4+NfhI1Ww89gx9Nhx9Nsy9ZWw9Dpj2abAAAAWnRSTlMAAQICAwQEBgcIDQ0ODhQZGiAiIyYpKywvNTs+QklPUlNUWWJjaGt0dnd+hIWFh4mNjZCSm6CpsbW2t7nDzNDT1dje5efr7PHy9PT29/j4+Pn5+vr8/f39/f6DPtKwAAABTklEQVR4Xr3QVWPbMBSAUTVFZmZmhhSXMjNvkhwqMzMzMzPDeD+xASvObKePPa+ffHVl8PlsnE0+qPpBuQjVJjno6pZpSKXYl7/bZyFaQxhf98hHDKEppwdWIW1frFnrxSOWHFfWesSEWC6R/P4zOFrix3TzDFLlXRTR8c0fEEJ1/itpo7SVO9Jdr1DVxZ0USyjZsEY5vZfiiAC0UoTGOrm9PZLuRl8X+Dq1HQtoFbJZbv61i+Poblh/97TC7n0neCcK0ETNUrz1/xPHf+DNAW9Ac6t8O8WH3Vp98f5lCaYKAOFZMLyHL4Y0fe319idMNgMMp+zWVSybUed/+/h7I4wRAG1W6XDy4XmjR9HnzvDRZXUAYDFOhC1S/Hh+fIXxen+eO+AKqbs+wAo30zDTDvDxKoJN88sjUzDFAvBzEUGFsnADoIvAJzoh2BZ8sner+Ke/vwECuQAAAABJRU5ErkJggg==') /*logo.png*/; + background-repeat: no-repeat; + background-position: 2px; + background-color: white; + border: none; +} + +.layer-switcher.shown button { + display: none; +} + +.layer-switcher button:focus, .layer-switcher button:hover { + background-color: white; +} + +.layer-switcher ul { + padding-left: 1em; + list-style: none; +} + +.layer-switcher li.group { + padding-top: 5px; +} + +.layer-switcher li.group > label { + font-weight: bold; +} + +.layer-switcher li.layer { + display: table; +} + +.layer-switcher li.layer label, .layer-switcher li.layer input { + display: table-cell; + vertical-align: sub; +} + +.layer-switcher input { + margin: 4px; +} + +.layer-switcher.touch ::-webkit-scrollbar { + width: 4px; +} + +.layer-switcher.touch ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; +} + +.layer-switcher.touch ::-webkit-scrollbar-thumb { + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); +} diff --git a/public_html/ol3/ol3-layerswitcher.js b/public_html/ol3/ol3-layerswitcher.js new file mode 100644 index 0000000..ee44e2f --- /dev/null +++ b/public_html/ol3/ol3-layerswitcher.js @@ -0,0 +1,283 @@ +/** + * OpenLayers 3 Layer Switcher Control. + * See [the examples](./examples) for usage. + * @constructor + * @extends {ol.control.Control} + * @param {Object} opt_options Control options, extends olx.control.ControlOptions adding: + * **`tipLabel`** `String` - the button tooltip. + */ +ol.control.LayerSwitcher = function(opt_options) { + + var options = opt_options || {}; + + var tipLabel = options.tipLabel ? + options.tipLabel : 'Legend'; + + this.mapListeners = []; + + this.hiddenClassName = 'ol-unselectable ol-control layer-switcher'; + if (ol.control.LayerSwitcher.isTouchDevice_()) { + this.hiddenClassName += ' touch'; + } + this.shownClassName = this.hiddenClassName + ' shown'; + + var element = document.createElement('div'); + element.className = this.hiddenClassName; + + var button = document.createElement('button'); + button.setAttribute('title', tipLabel); + element.appendChild(button); + + this.panel = document.createElement('div'); + this.panel.className = 'panel'; + element.appendChild(this.panel); + ol.control.LayerSwitcher.enableTouchScroll_(this.panel); + + var this_ = this; + + button.onmouseover = function(e) { + this_.showPanel(); + }; + + button.onclick = function(e) { + e = e || window.event; + this_.showPanel(); + e.preventDefault(); + }; + + this_.panel.onmouseout = function(e) { + e = e || window.event; + if (!this_.panel.contains(e.toElement || e.relatedTarget)) { + this_.hidePanel(); + } + }; + + ol.control.Control.call(this, { + element: element, + target: options.target + }); + +}; + +ol.inherits(ol.control.LayerSwitcher, ol.control.Control); + +/** + * Show the layer panel. + */ +ol.control.LayerSwitcher.prototype.showPanel = function() { + if (this.element.className != this.shownClassName) { + this.element.className = this.shownClassName; + this.renderPanel(); + } +}; + +/** + * Hide the layer panel. + */ +ol.control.LayerSwitcher.prototype.hidePanel = function() { + if (this.element.className != this.hiddenClassName) { + this.element.className = this.hiddenClassName; + } +}; + +/** + * Re-draw the layer panel to represent the current state of the layers. + */ +ol.control.LayerSwitcher.prototype.renderPanel = function() { + + this.ensureTopVisibleBaseLayerShown_(); + + while(this.panel.firstChild) { + this.panel.removeChild(this.panel.firstChild); + } + + var ul = document.createElement('ul'); + this.panel.appendChild(ul); + this.renderLayers_(this.getMap(), ul); + +}; + +/** + * Set the map instance the control is associated with. + * @param {ol.Map} map The map instance. + */ +ol.control.LayerSwitcher.prototype.setMap = function(map) { + // Clean up listeners associated with the previous map + for (var i = 0, key; i < this.mapListeners.length; i++) { + this.getMap().unByKey(this.mapListeners[i]); + } + this.mapListeners.length = 0; + // Wire up listeners etc. and store reference to new map + ol.control.Control.prototype.setMap.call(this, map); + if (map) { + var this_ = this; + this.mapListeners.push(map.on('pointerdown', function() { + this_.hidePanel(); + })); + this.renderPanel(); + } +}; + +/** + * Ensure only the top-most base layer is visible if more than one is visible. + * @private + */ +ol.control.LayerSwitcher.prototype.ensureTopVisibleBaseLayerShown_ = function() { + var lastVisibleBaseLyr; + ol.control.LayerSwitcher.forEachRecursive(this.getMap(), function(l, idx, a) { + if (l.get('type') === 'base' && l.getVisible()) { + lastVisibleBaseLyr = l; + } + }); + if (lastVisibleBaseLyr) this.setVisible_(lastVisibleBaseLyr, true); +}; + +/** + * Toggle the visible state of a layer. + * Takes care of hiding other layers in the same exclusive group if the layer + * is toggle to visible. + * @private + * @param {ol.layer.Base} The layer whos visibility will be toggled. + */ +ol.control.LayerSwitcher.prototype.setVisible_ = function(lyr, visible) { + var map = this.getMap(); + lyr.setVisible(visible); + if (visible && lyr.get('type') === 'base') { + // Hide all other base layers regardless of grouping + ol.control.LayerSwitcher.forEachRecursive(map, function(l, idx, a) { + if (l != lyr && l.get('type') === 'base') { + l.setVisible(false); + } + }); + } +}; + +/** + * Render all layers that are children of a group. + * @private + * @param {ol.layer.Base} lyr Layer to be rendered (should have a title property). + * @param {Number} idx Position in parent group list. + */ +ol.control.LayerSwitcher.prototype.renderLayer_ = function(lyr, idx) { + + var this_ = this; + + var li = document.createElement('li'); + + var lyrTitle = lyr.get('title'); + var lyrId = ol.control.LayerSwitcher.uuid(); + + var label = document.createElement('label'); + + if (lyr.getLayers && !lyr.get('combine')) { + + li.className = 'group'; + label.innerHTML = lyrTitle; + li.appendChild(label); + var ul = document.createElement('ul'); + li.appendChild(ul); + + this.renderLayers_(lyr, ul); + + } else { + + li.className = 'layer'; + var input = document.createElement('input'); + if (lyr.get('type') === 'base') { + input.type = 'radio'; + input.name = 'base'; + } else { + input.type = 'checkbox'; + } + input.id = lyrId; + input.checked = lyr.get('visible'); + input.onchange = function(e) { + this_.setVisible_(lyr, e.target.checked); + }; + li.appendChild(input); + + label.htmlFor = lyrId; + label.innerHTML = lyrTitle; + li.appendChild(label); + + } + + return li; + +}; + +/** + * Render all layers that are children of a group. + * @private + * @param {ol.layer.Group} lyr Group layer whos children will be rendered. + * @param {Element} elm DOM element that children will be appended to. + */ +ol.control.LayerSwitcher.prototype.renderLayers_ = function(lyr, elm) { + var lyrs = lyr.getLayers().getArray().slice().reverse(); + for (var i = 0, l; i < lyrs.length; i++) { + l = lyrs[i]; + if (l.get('title')) { + elm.appendChild(this.renderLayer_(l, i)); + } + } +}; + +/** + * **Static** Call the supplied function for each layer in the passed layer group + * recursing nested groups. + * @param {ol.layer.Group} lyr The layer group to start iterating from. + * @param {Function} fn Callback which will be called for each `ol.layer.Base` + * found under `lyr`. The signature for `fn` is the same as `ol.Collection#forEach` + */ +ol.control.LayerSwitcher.forEachRecursive = function(lyr, fn) { + lyr.getLayers().forEach(function(lyr, idx, a) { + fn(lyr, idx, a); + if (lyr.getLayers) { + ol.control.LayerSwitcher.forEachRecursive(lyr, fn); + } + }); +}; + +/** + * Generate a UUID + * @returns {String} UUID + * + * Adapted from http://stackoverflow.com/a/2117523/526860 + */ +ol.control.LayerSwitcher.uuid = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +} + +/** +* @private +* @desc Apply workaround to enable scrolling of overflowing content within an +* element. Adapted from https://gist.github.com/chrismbarr/4107472 +*/ +ol.control.LayerSwitcher.enableTouchScroll_ = function(elm) { + if(ol.control.LayerSwitcher.isTouchDevice_()){ + var scrollStartPos = 0; + elm.addEventListener("touchstart", function(event) { + scrollStartPos = this.scrollTop + event.touches[0].pageY; + }, false); + elm.addEventListener("touchmove", function(event) { + this.scrollTop = scrollStartPos - event.touches[0].pageY; + }, false); + } +}; + +/** + * @private + * @desc Determine if the current browser supports touch events. Adapted from + * https://gist.github.com/chrismbarr/4107472 + */ +ol.control.LayerSwitcher.isTouchDevice_ = function() { + try { + document.createEvent("TouchEvent"); + return true; + } catch(e) { + return false; + } +}; diff --git a/public_html/script.js b/public_html/script.js index 9b3a6c6..19618eb 100644 --- a/public_html/script.js +++ b/public_html/script.js @@ -18,7 +18,7 @@ var SpecialSquawks = { }; // Get current map settings -var CenterLat, CenterLon, ZoomLvl; +var CenterLat, CenterLon, ZoomLvl, MapType; var Dump1090Version = "unknown version"; var RefreshInterval = 1000; @@ -326,6 +326,7 @@ function initialize_map() { CenterLat = Number(localStorage['CenterLat']) || DefaultCenterLat; CenterLon = Number(localStorage['CenterLon']) || DefaultCenterLon; ZoomLvl = Number(localStorage['ZoomLvl']) || DefaultZoomLvl; + MapType = localStorage['MapType']; // Set SitePosition, initialize sorting if (SiteShow && (typeof SiteLat !== 'undefined') && (typeof SiteLon !== 'undefined')) { @@ -346,27 +347,122 @@ function initialize_map() { } // Initialize OL3 - // TODO map types etc - var rasterLayer = new ol.layer.Tile({ - source: new ol.source.OSM() - }); + var baseLayerGroups = { + 'world': new ol.layer.Group({ + title: 'Worldwide' + }), - var staticLayer = new ol.layer.Vector({ + 'chartbundle': new ol.layer.Group({ + title: 'ChartBundle (US)' + }) + }; + + var baseLayers = [] + + baseLayers.push(new ol.layer.Tile({ + source: new ol.source.OSM(), + name: 'osm', + title: 'OpenStreetMap', + type: 'base', + group: 'world' + })); + + baseLayers.push(new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'sat'}), + name: 'mapquest_sat', + title: 'MapQuest satellite', + type: 'base', + group: 'world' + })); + + if (BingMapsAPIKey) { + baseLayers.push(new ol.layer.Tile({ + source: new ol.source.BingMaps({ + key: BingMapsAPIKey, + imagerySet: 'Aerial' + }), + name: 'bing_aerial', + title: 'Bing Aerial', + type: 'base', + group: 'world' + })); + } + + var chartbundleTypes = { + sec: "Sectional Charts", + tac: "Terminal Area Charts", + wac: "World Aeronautical Charts", + enrl: "IFR Enroute Low Charts", + enra: "IFR Area Charts", + enrh: "IFR Enroute High Charts" + }; + + for (var type in chartbundleTypes) { + baseLayers.push(new ol.layer.Tile({ + source: new ol.source.TileWMS({ + url: 'http://wms.chartbundle.com/wms', + params: {LAYERS: type}, + projection: 'EPSG:3857', + attributions: 'Tiles courtesy of ChartBundle' + }), + name: 'chartbundle_' + type, + title: chartbundleTypes[type], + type: 'base', + group: 'chartbundle'})); + } + + var layers = []; + var found = false; + for (var i = 0; i < baseLayers.length; ++i) { + var layer = baseLayers[i]; + if (MapType === layer.get('name')) { + found = true; + layer.setVisible(true); + } else { + layer.setVisible(false); + } + + layer.on('change:visible', function(evt) { + if (evt.target.getVisible()) { + MapType = localStorage['MapType'] = evt.target.get('name'); + } + }); + + // The layer selector displays in reverse order for some reason, unreverse it + if (layer.get('group')) { + // hurf + baseLayerGroups[layer.get('group')].getLayers().insertAt(0, layer); + } else { + layers.unshift(layer); + } + } + + if (!found) { + baseLayers[0].setVisible(true); + } + + for (var key in baseLayerGroups) { + if (baseLayerGroups[key].getLayers().getLength() > 0) { + layers.unshift(baseLayerGroups[key]); + } + } + + layers.push(new ol.layer.Vector({ source: new ol.source.Vector({ features: StaticFeatures, updateWhileInteracting: true, updateWhileAnimating: true }) - }); + })); - var trailsLayer = new ol.layer.Vector({ + layers.push(new ol.layer.Vector({ source: new ol.source.Vector({ features: PlaneTrailFeatures, updateWhileInteracting: true, updateWhileAnimating: true }) - }); + })); var iconsLayer = new ol.layer.Vector({ source: new ol.source.Vector({ @@ -375,10 +471,11 @@ function initialize_map() { updateWhileAnimating: true }) }); + layers.push(iconsLayer); OLMap = new ol.Map({ target: 'map_canvas', - layers: [rasterLayer, staticLayer, trailsLayer, iconsLayer], + layers: layers, view: new ol.View({ center: ol.proj.fromLonLat([CenterLon, CenterLat]), zoom: ZoomLvl @@ -386,7 +483,9 @@ function initialize_map() { controls: [new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution(), - new ol.control.ScaleLine({units: Metric ? "metric" : "nautical"})], + new ol.control.ScaleLine({units: Metric ? "metric" : "nautical"}), + new ol.control.LayerSwitcher() + ], loadTilesWhileAnimating: true, loadTilesWhileInteracting: true });