├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── demo.gif ├── dist ├── check.png ├── iconLayers.css ├── iconLayers.js └── transparent-pixel.png ├── examples ├── browserify.html ├── browserify.js ├── icons │ ├── cartodb_positron.png │ ├── esri_oceanbasemap.png │ ├── esri_worldterrain.png │ ├── here_normalday.png │ ├── here_normaldaygrey.png │ ├── here_satelliteday.png │ ├── openstreetmap_blackandwhite.png │ ├── openstreetmap_de.png │ ├── openstreetmap_mapnik.png │ └── stamen_toner.png ├── index.html └── providers.js ├── gulpfile.js ├── package.json └── src ├── check.png ├── iconLayers.css ├── iconLayers.js └── transparent-pixel.png /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "camelcase": 2, 4 | "quotes": [2, "single"], 5 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 6 | "space-in-parens": 2, 7 | "space-before-blocks": 2, 8 | "space-after-keywords": 2, 9 | "no-lonely-if": 2, 10 | "comma-style": 2, 11 | "no-underscore-dangle": 0, 12 | "no-constant-condition": 0, 13 | "no-multi-spaces": 0, 14 | "strict": 0, 15 | "key-spacing": 0, 16 | "no-console": 0, 17 | "no-shadow": 0, 18 | "indent": [2, 4], 19 | "linebreak-style": [2, "windows"], 20 | "semi": [2, "always"], 21 | "curly": 2 22 | }, 23 | "env": { 24 | "browser": true 25 | }, 26 | "globals": { 27 | "L": true 28 | }, 29 | "extends": "eslint:recommended" 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.sublime-project 4 | *.sublime-workspace 5 | *_bundle.js 6 | *.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | *.sublime-project 4 | *.sublime-workspace 5 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, RDC ScanEx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Leaflet-IconLayers 2 | 3 | ![](demo.gif) 4 | 5 | Leaflet base layers switching control with icons ([example](https://scanex.github.com/Leaflet-IconLayers/examples)) 6 | 7 | *Requires Leaflet 0.7.3 or newer; IE9+* 8 | 9 | Extends `L.Control`. 10 | 11 | ## Using Control 12 | 13 | Copy files from `src` dir and include them to your project. 14 | 15 | Basic usage: 16 | 17 | ```javascript 18 | L.control.iconLayers(layers).addTo(map); 19 | ``` 20 | 21 | In order to interact with layers Leaflet-IconLayers uses an array of layer objects, that have following fields: 22 | - `icon` - icon url (typically 80x80) 23 | - `title` - a short string that is displayed at the bottom of each icon 24 | - `layer` - any Leaflet `ILayer` 25 | 26 | You can pass this array to construtor or use `setLayers` method. 27 | 28 | The second constructor argument may be `options` hash. It is also ok if it is the only one. 29 | 30 | ## Options 31 | 32 | - `maxLayersInRow` - the number of layers, that a row can contain 33 | - `manageLayers` - by default control manages map layers. Pass `false` if you want to manage layers manually. 34 | 35 | plus `L.Control` options (`position`) 36 | 37 | ## Methods 38 | 39 | - `setLayers( layers)` - replace layers array with a new one 40 | - `setActiveLayer( layer)` - set active layer 41 | - `collapse()` - hide secondary layers 42 | - `expand()` - show hidden layers 43 | 44 | ## Events 45 | 46 | - `activelayerchange` - fires when user changes active layer (clicks one of layer icons). The changed layer is passed in `layer` key of an event object (see an example). 47 | 48 | ## Detailed example 49 | ```javascript 50 | var iconLayersControl = new L.Control.IconLayers( 51 | [ 52 | { 53 | title: 'Map', // use any string 54 | layer: mapLayer, // any ILayer 55 | icon: 'img/mapIcon.png' // 80x80 icon 56 | }, 57 | { 58 | title: 'Satellite', 59 | layer: satLayer, 60 | icon: 'img/mapIcon.png' 61 | } 62 | ], { 63 | position: 'bottomleft', 64 | maxLayersInRow: 5 65 | } 66 | ); 67 | 68 | // new L.Control.IconLayers(layers) 69 | // new L.Control.IconLayers(options) 70 | // are also ok 71 | 72 | iconLayersControl.addTo(map); 73 | 74 | // we can modify layers list 75 | iconLayersControl.setLayers(layers); 76 | 77 | iconLayersControl.on('activelayerchange', function(e) { 78 | console.log('layer switched', e.layer); 79 | }); 80 | ``` -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/demo.gif -------------------------------------------------------------------------------- /dist/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/dist/check.png -------------------------------------------------------------------------------- /dist/iconLayers.css: -------------------------------------------------------------------------------- 1 | .leaflet-iconLayers { 2 | pointer-events: none; 3 | } 4 | 5 | .leaflet-iconLayers-layersRow { display: table; pointer-events: auto; } 6 | .leaflet-iconLayers-layerCell { display: table-cell; background-image: url('transparent-pixel.png'); /* ie9 fix */ } 7 | 8 | .leaflet-iconLayers_topleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomleft .leaflet-iconLayers-layerCell { padding-right: 5px; } 9 | .leaflet-iconLayers_topright .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomright .leaflet-iconLayers-layerCell { padding-left: 5px; } 10 | 11 | .leaflet-iconLayers_topleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_topright .leaflet-iconLayers-layerCell { padding-bottom: 5px; } 12 | .leaflet-iconLayers_bottomleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomright .leaflet-iconLayers-layerCell { padding-top: 5px; } 13 | 14 | .leaflet-iconLayers-layer { 15 | cursor: pointer; 16 | position: relative; 17 | width: 80px; 18 | height: 80px; 19 | background-color: #fff; 20 | background-repeat: no-repeat; 21 | background-size: cover; 22 | text-align: center; 23 | box-sizing: border-box; 24 | box-shadow: 0 0 5px #000; 25 | } 26 | 27 | .leaflet-iconLayers-layerTitleContainer { 28 | display: table; 29 | width: 100%; 30 | background: rgba(255,255,255,0.6); 31 | height: 25%; 32 | padding: 0; 33 | border: 0; 34 | position: absolute; 35 | bottom: 0%; 36 | transition: bottom .35s ease; 37 | } 38 | 39 | .leaflet-iconLayers-layerCheckIcon { 40 | display: none; 41 | position: absolute; 42 | top: 3px; 43 | right: 3px; 44 | width: 18px; 45 | height: 18px; 46 | background: url('check.png'); 47 | background-color: #fff; 48 | background-repeat: no-repeat; 49 | background-position: 4px 4px; 50 | border-radius: 10px; 51 | box-sizing: border-box; 52 | border: 1px solid rgba(0,0,0,0.6); 53 | } 54 | 55 | .leaflet-iconLayers-layerTitle { 56 | display: table-cell; 57 | vertical-align: middle; 58 | } 59 | 60 | .leaflet-iconLayers-layerCell_hidden { display: none; } 61 | .leaflet-iconLayers-layerCell_active .leaflet-iconLayers-layer { cursor: default; } 62 | .leaflet-iconLayers-layerCell_active .leaflet-iconLayers-layerCheckIcon { display: block; } -------------------------------------------------------------------------------- /dist/iconLayers.js: -------------------------------------------------------------------------------- 1 | /*eslint-env commonjs, browser */ 2 | (function(factory) { 3 | if (typeof module !== 'undefined' && module.exports) { 4 | module.exports = factory(require('leaflet')); 5 | } else { 6 | window.L.control.iconLayers = factory(window.L); 7 | window.L.Control.IconLayers = window.L.control.iconLayers.Constructor; 8 | } 9 | })(function(L) { 10 | function each(o, cb) { 11 | for (var p in o) { 12 | if (o.hasOwnProperty(p)) { 13 | cb(o[p], p, o); 14 | } 15 | } 16 | } 17 | 18 | function find(ar, cb) { 19 | if (ar.length) { 20 | for (var i = 0; i < ar.length; i++) { 21 | if (cb(ar[i])) { 22 | return ar[i]; 23 | } 24 | } 25 | } else { 26 | for (var p in ar) { 27 | if (ar.hasOwnProperty(p) && cb(ar[p])) { 28 | return ar[p]; 29 | } 30 | } 31 | } 32 | } 33 | 34 | function first(o) { 35 | for (var p in o) { 36 | if (o.hasOwnProperty(p)) { 37 | return o[p]; 38 | } 39 | } 40 | } 41 | 42 | function length(o) { 43 | var length = 0; 44 | for (var p in o) { 45 | if (o.hasOwnProperty(p)) { 46 | length++; 47 | } 48 | } 49 | return length; 50 | } 51 | 52 | function prepend(parent, el) { 53 | if (parent.children.length) { 54 | parent.insertBefore(el, parent.children[0]); 55 | } else { 56 | parent.appendChild(el); 57 | } 58 | } 59 | 60 | var IconLayers = L.Control.extend({ 61 | 62 | includes: L.Evented ? L.Evented.prototype : L.Mixin.Events, 63 | _getActiveLayer: function() { 64 | if (this._activeLayerId) { 65 | return this._layers[this._activeLayerId]; 66 | } else if (length(this._layers)) { 67 | return first(this._layers); 68 | } else { 69 | return null; 70 | } 71 | }, 72 | _getPreviousLayer: function() { 73 | var activeLayer = this._getActiveLayer(); 74 | if (!activeLayer) { 75 | return null; 76 | } else if (this._previousLayerId) { 77 | return this._layers[this._previousLayerId]; 78 | } else { 79 | return find(this._layers, function(l) { 80 | return l.id !== activeLayer.id; 81 | }.bind(this)) || null; 82 | } 83 | }, 84 | _getInactiveLayers: function() { 85 | var ar = []; 86 | var activeLayerId = this._getActiveLayer() ? this._getActiveLayer().id : null; 87 | var previousLayerId = this._getPreviousLayer() ? this._getPreviousLayer().id : null; 88 | each(this._layers, function(l) { 89 | if ((l.id !== activeLayerId) && (l.id !== previousLayerId)) { 90 | ar.push(l); 91 | } 92 | }); 93 | return ar; 94 | }, 95 | _arrangeLayers: function() { 96 | var behaviors = {}; 97 | behaviors.previous = function() { 98 | var layers = this._getInactiveLayers(); 99 | if (this._getActiveLayer()) { 100 | layers.unshift(this._getActiveLayer()); 101 | } 102 | if (this._getPreviousLayer()) { 103 | layers.unshift(this._getPreviousLayer()); 104 | } 105 | return layers; 106 | }; 107 | return behaviors[this.options.behavior].apply(this, arguments); 108 | }, 109 | _getLayerCellByLayerId: function(id) { 110 | var els = this._container.getElementsByClassName('leaflet-iconLayers-layerCell'); 111 | for (var i = 0; i < els.length; i++) { 112 | if (els[i].getAttribute('data-layerid') == id) { 113 | return els[i]; 114 | } 115 | } 116 | }, 117 | _createLayerElement: function(layerObj) { 118 | var el = L.DomUtil.create('div', 'leaflet-iconLayers-layer'); 119 | if (layerObj.title) { 120 | var titleContainerEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerTitleContainer'); 121 | var titleEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerTitle'); 122 | var checkIconEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerCheckIcon'); 123 | titleEl.innerHTML = layerObj.title; 124 | titleContainerEl.appendChild(titleEl); 125 | el.appendChild(titleContainerEl); 126 | el.appendChild(checkIconEl); 127 | } 128 | if (layerObj.icon) { 129 | el.setAttribute('style', 'background-image: url(\'' + layerObj.icon + '\')'); 130 | } 131 | return el; 132 | }, 133 | _createLayerElements: function() { 134 | var currentRow, layerCell; 135 | var layers = this._arrangeLayers(); 136 | var activeLayerId = this._getActiveLayer() && this._getActiveLayer().id; 137 | 138 | for (var i = 0; i < layers.length; i++) { 139 | if (i % this.options.maxLayersInRow === 0) { 140 | currentRow = L.DomUtil.create('div', 'leaflet-iconLayers-layersRow'); 141 | if (this.options.position.indexOf('bottom') === -1) { 142 | this._container.appendChild(currentRow); 143 | } else { 144 | prepend(this._container, currentRow); 145 | } 146 | } 147 | layerCell = L.DomUtil.create('div', 'leaflet-iconLayers-layerCell'); 148 | layerCell.setAttribute('data-layerid', layers[i].id); 149 | if (i !== 0) { 150 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_hidden'); 151 | } 152 | if (layers[i].id === activeLayerId) { 153 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_active'); 154 | } 155 | if (this._expandDirection === 'left') { 156 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_expandLeft'); 157 | } else { 158 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_expandRight'); 159 | } 160 | layerCell.appendChild(this._createLayerElement(layers[i])); 161 | 162 | if (this.options.position.indexOf('right') === -1) { 163 | currentRow.appendChild(layerCell); 164 | } else { 165 | prepend(currentRow, layerCell); 166 | } 167 | } 168 | }, 169 | _onLayerClick: function(e) { 170 | e.stopPropagation(); 171 | var layerId = e.currentTarget.getAttribute('data-layerid'); 172 | var layer = this._layers[layerId]; 173 | this.setActiveLayer(layer.layer); 174 | this.expand(); 175 | }, 176 | _attachEvents: function() { 177 | each(this._layers, function(l) { 178 | var e = this._getLayerCellByLayerId(l.id); 179 | if (e) { 180 | e.addEventListener('click', this._onLayerClick.bind(this)); 181 | } 182 | }.bind(this)); 183 | var layersRowCollection = this._container.getElementsByClassName('leaflet-iconLayers-layersRow'); 184 | 185 | var onMouseEnter = function(e) { 186 | e.stopPropagation(); 187 | this.expand(); 188 | }.bind(this); 189 | 190 | var onMouseLeave = function(e) { 191 | e.stopPropagation(); 192 | this.collapse(); 193 | }.bind(this); 194 | 195 | var stopPropagation = function(e) { 196 | e.stopPropagation(); 197 | }; 198 | 199 | //TODO Don't make functions within a loop. 200 | for (var i = 0; i < layersRowCollection.length; i++) { 201 | var el = layersRowCollection[i]; 202 | el.addEventListener('mouseenter', onMouseEnter); 203 | el.addEventListener('mouseleave', onMouseLeave); 204 | el.addEventListener('mousemove', stopPropagation); 205 | } 206 | }, 207 | _render: function() { 208 | this._container.innerHTML = ''; 209 | this._createLayerElements(); 210 | this._attachEvents(); 211 | }, 212 | _switchMapLayers: function() { 213 | if (!this._map) { 214 | return; 215 | } 216 | var activeLayer = this._getActiveLayer(); 217 | var previousLayer = this._getPreviousLayer(); 218 | if (previousLayer) { 219 | this._map.removeLayer(previousLayer.layer); 220 | } else { 221 | each(this._layers, function(layerObject) { 222 | var layer = layerObject.layer; 223 | this._map.removeLayer(layer); 224 | }.bind(this)); 225 | } 226 | if (activeLayer) { 227 | this._map.addLayer(activeLayer.layer); 228 | } 229 | }, 230 | options: { 231 | position: 'bottomleft', // one of expanding directions depends on this 232 | behavior: 'previous', // may be 'previous', 'expanded' or 'first' 233 | expand: 'horizontal', // or 'vertical' 234 | autoZIndex: true, // from L.Control.Layers 235 | maxLayersInRow: 5, 236 | manageLayers: true 237 | }, 238 | initialize: function(layers, options) { 239 | if (!L.Util.isArray(arguments[0])) { 240 | // first argument is options 241 | options = layers; 242 | layers = []; 243 | } 244 | L.setOptions(this, options); 245 | this._expandDirection = (this.options.position.indexOf('left') != -1) ? 'right' : 'left'; 246 | if (this.options.manageLayers) { 247 | this.on('activelayerchange', this._switchMapLayers, this); 248 | } 249 | this.setLayers(layers); 250 | }, 251 | onAdd: function(map) { 252 | this._container = L.DomUtil.create('div', 'leaflet-iconLayers'); 253 | L.DomUtil.addClass(this._container, 'leaflet-iconLayers_' + this.options.position); 254 | this._render(); 255 | map.on('click', this.collapse, this); 256 | if (this.options.manageLayers) { 257 | this._switchMapLayers(); 258 | } 259 | return this._container; 260 | }, 261 | onRemove: function(map) { 262 | map.off('click', this.collapse, this); 263 | }, 264 | setLayers: function(layers) { 265 | this._layers = {}; 266 | layers.map(function(layer) { 267 | var id = L.stamp(layer.layer); 268 | this._layers[id] = L.extend(layer, { 269 | id: id 270 | }); 271 | }.bind(this)); 272 | if (this._container) { 273 | this._render(); 274 | } 275 | }, 276 | setActiveLayer: function(layer) { 277 | var l = layer && this._layers[L.stamp(layer)]; 278 | if (!l || l.id === this._activeLayerId) { 279 | return; 280 | } 281 | this._previousLayerId = this._activeLayerId; 282 | this._activeLayerId = l.id; 283 | if (this._container) { 284 | this._render(); 285 | } 286 | this.fire('activelayerchange', { 287 | layer: layer 288 | }); 289 | }, 290 | expand: function() { 291 | this._arrangeLayers().slice(1).map(function(l) { 292 | var el = this._getLayerCellByLayerId(l.id); 293 | L.DomUtil.removeClass(el, 'leaflet-iconLayers-layerCell_hidden'); 294 | }.bind(this)); 295 | }, 296 | collapse: function() { 297 | this._arrangeLayers().slice(1).map(function(l) { 298 | var el = this._getLayerCellByLayerId(l.id); 299 | L.DomUtil.addClass(el, 'leaflet-iconLayers-layerCell_hidden'); 300 | }.bind(this)); 301 | } 302 | }); 303 | 304 | var iconLayers = function(layers, options) { 305 | return new IconLayers(layers, options); 306 | }; 307 | 308 | iconLayers.Constructor = IconLayers; 309 | 310 | return iconLayers; 311 | }); -------------------------------------------------------------------------------- /dist/transparent-pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/dist/transparent-pixel.png -------------------------------------------------------------------------------- /examples/browserify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leflet-IconLayers demo 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/browserify.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet'); 2 | var providers = require('./providers'); 3 | var iconLayers = require('../src/iconLayers'); 4 | 5 | window.addEventListener('load', function() { 6 | var map = L.map(document.body).setView([38.14, 19.33], 7); 7 | 8 | var layers = []; 9 | for (var providerId in providers) { 10 | layers.push(providers[providerId]); 11 | } 12 | 13 | layers.push({ 14 | layer: { 15 | onAdd: function() {}, 16 | onRemove: function() {} 17 | }, 18 | title: 'empty' 19 | }) 20 | 21 | var ctrl = iconLayers(layers).addTo(map); 22 | }); -------------------------------------------------------------------------------- /examples/icons/cartodb_positron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/cartodb_positron.png -------------------------------------------------------------------------------- /examples/icons/esri_oceanbasemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/esri_oceanbasemap.png -------------------------------------------------------------------------------- /examples/icons/esri_worldterrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/esri_worldterrain.png -------------------------------------------------------------------------------- /examples/icons/here_normalday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/here_normalday.png -------------------------------------------------------------------------------- /examples/icons/here_normaldaygrey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/here_normaldaygrey.png -------------------------------------------------------------------------------- /examples/icons/here_satelliteday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/here_satelliteday.png -------------------------------------------------------------------------------- /examples/icons/openstreetmap_blackandwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/openstreetmap_blackandwhite.png -------------------------------------------------------------------------------- /examples/icons/openstreetmap_de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/openstreetmap_de.png -------------------------------------------------------------------------------- /examples/icons/openstreetmap_mapnik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/openstreetmap_mapnik.png -------------------------------------------------------------------------------- /examples/icons/stamen_toner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/examples/icons/stamen_toner.png -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Leflet-IconLayers demo 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 30 | 31 | 32 | Fork me on GitHub 33 |
34 | 52 | 53 | -------------------------------------------------------------------------------- /examples/providers.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | if (typeof module !== 'undefined' && module.exports) { 3 | module.exports = factory(require('leaflet')); 4 | } else { 5 | window.providers = factory(window.L); 6 | } 7 | })(function(L) { 8 | var providers = {}; 9 | 10 | providers['OpenStreetMap_Mapnik'] = { 11 | title: 'osm', 12 | icon: 'icons/openstreetmap_mapnik.png', 13 | layer: L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 14 | maxZoom: 19, 15 | attribution: '© OpenStreetMap' 16 | }) 17 | }; 18 | 19 | providers['OpenStreetMap_BlackAndWhite'] = { 20 | title: 'osm bw', 21 | icon: 'icons/openstreetmap_blackandwhite.png', 22 | layer: L.tileLayer('http://{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png', { 23 | maxZoom: 18, 24 | attribution: '© OpenStreetMap' 25 | }) 26 | }; 27 | 28 | providers['OpenStreetMap_DE'] = { 29 | title: 'osm de', 30 | icon: 'icons/openstreetmap_de.png', 31 | layer: L.tileLayer('http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { 32 | maxZoom: 18, 33 | attribution: '© OpenStreetMap' 34 | }) 35 | } 36 | 37 | providers['Stamen_Toner'] = { 38 | title: 'toner', 39 | icon: 'icons/stamen_toner.png', 40 | layer: L.tileLayer('http://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', { 41 | attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap', 42 | subdomains: 'abcd', 43 | minZoom: 0, 44 | maxZoom: 20, 45 | ext: 'png' 46 | }) 47 | }; 48 | 49 | providers['Esri_OceanBasemap'] = { 50 | title: 'esri ocean', 51 | icon: 'icons/esri_oceanbasemap.png', 52 | layer: L.tileLayer('http://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}', { 53 | attribution: 'Tiles © Esri — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri', 54 | maxZoom: 13 55 | }) 56 | }; 57 | 58 | providers['HERE_normalDay'] = { 59 | title: 'normalday', 60 | icon: 'icons/here_normalday.png', 61 | layer: L.tileLayer('http://{s}.{base}.maps.cit.api.here.com/maptile/2.1/maptile/{mapID}/normal.day/{z}/{x}/{y}/256/png8?app_id={app_id}&app_code={app_code}', { 62 | attribution: 'Map © 1987-2014 HERE', 63 | subdomains: '1234', 64 | mapID: 'newest', 65 | app_id: 'Y8m9dK2brESDPGJPdrvs', 66 | app_code: 'dq2MYIvjAotR8tHvY8Q_Dg', 67 | base: 'base', 68 | maxZoom: 20 69 | }) 70 | }; 71 | 72 | providers['HERE_normalDayGrey'] = { 73 | title: 'normalday grey', 74 | icon: 'icons/here_normaldaygrey.png', 75 | layer: L.tileLayer('http://{s}.{base}.maps.cit.api.here.com/maptile/2.1/maptile/{mapID}/normal.day.grey/{z}/{x}/{y}/256/png8?app_id={app_id}&app_code={app_code}', { 76 | attribution: 'Map © 1987-2014 HERE', 77 | subdomains: '1234', 78 | mapID: 'newest', 79 | app_id: 'Y8m9dK2brESDPGJPdrvs', 80 | app_code: 'dq2MYIvjAotR8tHvY8Q_Dg', 81 | base: 'base', 82 | maxZoom: 20 83 | }) 84 | }; 85 | 86 | providers['HERE_satelliteDay'] = { 87 | title: 'satellite', 88 | icon: 'icons/here_satelliteday.png', 89 | layer: L.tileLayer('http://{s}.{base}.maps.cit.api.here.com/maptile/2.1/maptile/{mapID}/satellite.day/{z}/{x}/{y}/256/png8?app_id={app_id}&app_code={app_code}', { 90 | attribution: 'Map © 1987-2014 HERE', 91 | subdomains: '1234', 92 | mapID: 'newest', 93 | app_id: 'Y8m9dK2brESDPGJPdrvs', 94 | app_code: 'dq2MYIvjAotR8tHvY8Q_Dg', 95 | base: 'aerial', 96 | maxZoom: 20 97 | }) 98 | }; 99 | 100 | providers['CartoDB_Positron'] = { 101 | title: 'positron', 102 | icon: 'icons/cartodb_positron.png', 103 | layer: L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { 104 | attribution: '© OpenStreetMap © CartoDB', 105 | subdomains: 'abcd', 106 | maxZoom: 19 107 | }) 108 | }; 109 | 110 | return providers; 111 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var eslint = require('gulp-eslint'); 3 | 4 | gulp.task('lint', function() { 5 | return gulp.src(['src/*.js']) 6 | .pipe(eslint()) 7 | .pipe(eslint.format()) 8 | .pipe(eslint.failOnError()); 9 | }); 10 | 11 | gulp.task('copy', ['lint'], function() { 12 | return gulp.src([ 13 | 'src/check.png', 14 | 'src/iconLayers.js', 15 | 'src/iconLayers.css', 16 | 'src/transparent-pixel.png' 17 | ]).pipe(gulp.dest('dist')); 18 | }); 19 | 20 | gulp.task('default', ['lint', 'copy']); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-iconlayers", 3 | "version": "0.2.0", 4 | "description": "Leaflet control that displays base layers as small icons.", 5 | "main": "dist/iconLayers.js", 6 | "style": "dist/iconLayers.css", 7 | "directories": { 8 | "example": "examples" 9 | }, 10 | "scripts": { 11 | "examples": "browserify -v examples/browserify.js > examples/browserify_bundle.js", 12 | "prepublish": "gulp && npm run examples" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ScanEx/Leaflet-IconLayers.git" 17 | }, 18 | "keywords": [ 19 | "leaflet" 20 | ], 21 | "author": { 22 | "name": "Alexander Zverev", 23 | "email": "alexander.zverev@gmail.com", 24 | "url": "https://github.com/zverev" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/ScanEx/Leaflet-IconLayers/issues" 29 | }, 30 | "homepage": "https://github.com/ScanEx/Leaflet-IconLayers", 31 | "dependencies": { 32 | "leaflet": "^0.7.3" 33 | }, 34 | "devDependencies": { 35 | "browserify": "^12.0.1", 36 | "gulp": "*", 37 | "gulp-eslint": "^1.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/src/check.png -------------------------------------------------------------------------------- /src/iconLayers.css: -------------------------------------------------------------------------------- 1 | .leaflet-iconLayers { 2 | pointer-events: none; 3 | } 4 | 5 | .leaflet-iconLayers-layersRow { display: table; pointer-events: auto; } 6 | .leaflet-iconLayers-layerCell { display: table-cell; background-image: url('transparent-pixel.png'); /* ie9 fix */ } 7 | 8 | .leaflet-iconLayers_topleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomleft .leaflet-iconLayers-layerCell { padding-right: 5px; } 9 | .leaflet-iconLayers_topright .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomright .leaflet-iconLayers-layerCell { padding-left: 5px; } 10 | 11 | .leaflet-iconLayers_topleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_topright .leaflet-iconLayers-layerCell { padding-bottom: 5px; } 12 | .leaflet-iconLayers_bottomleft .leaflet-iconLayers-layerCell, .leaflet-iconLayers_bottomright .leaflet-iconLayers-layerCell { padding-top: 5px; } 13 | 14 | .leaflet-iconLayers-layer { 15 | cursor: pointer; 16 | position: relative; 17 | width: 80px; 18 | height: 80px; 19 | background-color: #fff; 20 | background-repeat: no-repeat; 21 | background-size: cover; 22 | text-align: center; 23 | box-sizing: border-box; 24 | box-shadow: 0 0 5px #000; 25 | } 26 | 27 | .leaflet-iconLayers-layerTitleContainer { 28 | display: table; 29 | width: 100%; 30 | background: rgba(255,255,255,0.6); 31 | height: 25%; 32 | padding: 0; 33 | border: 0; 34 | position: absolute; 35 | bottom: 0%; 36 | transition: bottom .35s ease; 37 | } 38 | 39 | .leaflet-iconLayers-layerCheckIcon { 40 | display: none; 41 | position: absolute; 42 | top: 3px; 43 | right: 3px; 44 | width: 18px; 45 | height: 18px; 46 | background: url('check.png'); 47 | background-color: #fff; 48 | background-repeat: no-repeat; 49 | background-position: 4px 4px; 50 | border-radius: 10px; 51 | box-sizing: border-box; 52 | border: 1px solid rgba(0,0,0,0.6); 53 | } 54 | 55 | .leaflet-iconLayers-layerTitle { 56 | display: table-cell; 57 | vertical-align: middle; 58 | } 59 | 60 | .leaflet-iconLayers-layerCell_hidden { display: none; } 61 | .leaflet-iconLayers-layerCell_active .leaflet-iconLayers-layer { cursor: default; } 62 | .leaflet-iconLayers-layerCell_active .leaflet-iconLayers-layerCheckIcon { display: block; } -------------------------------------------------------------------------------- /src/iconLayers.js: -------------------------------------------------------------------------------- 1 | /*eslint-env commonjs, browser */ 2 | (function(factory) { 3 | if (typeof module !== 'undefined' && module.exports) { 4 | module.exports = factory(require('leaflet')); 5 | } else { 6 | window.L.control.iconLayers = factory(window.L); 7 | window.L.Control.IconLayers = window.L.control.iconLayers.Constructor; 8 | } 9 | })(function(L) { 10 | function each(o, cb) { 11 | for (var p in o) { 12 | if (o.hasOwnProperty(p)) { 13 | cb(o[p], p, o); 14 | } 15 | } 16 | } 17 | 18 | function find(ar, cb) { 19 | if (ar.length) { 20 | for (var i = 0; i < ar.length; i++) { 21 | if (cb(ar[i])) { 22 | return ar[i]; 23 | } 24 | } 25 | } else { 26 | for (var p in ar) { 27 | if (ar.hasOwnProperty(p) && cb(ar[p])) { 28 | return ar[p]; 29 | } 30 | } 31 | } 32 | } 33 | 34 | function first(o) { 35 | for (var p in o) { 36 | if (o.hasOwnProperty(p)) { 37 | return o[p]; 38 | } 39 | } 40 | } 41 | 42 | function length(o) { 43 | var length = 0; 44 | for (var p in o) { 45 | if (o.hasOwnProperty(p)) { 46 | length++; 47 | } 48 | } 49 | return length; 50 | } 51 | 52 | function prepend(parent, el) { 53 | if (parent.children.length) { 54 | parent.insertBefore(el, parent.children[0]); 55 | } else { 56 | parent.appendChild(el); 57 | } 58 | } 59 | 60 | var IconLayers = L.Control.extend({ 61 | 62 | includes: L.Evented ? L.Evented.prototype : L.Mixin.Events, 63 | _getActiveLayer: function() { 64 | if (this._activeLayerId) { 65 | return this._layers[this._activeLayerId]; 66 | } else if (length(this._layers)) { 67 | return first(this._layers); 68 | } else { 69 | return null; 70 | } 71 | }, 72 | _getPreviousLayer: function() { 73 | var activeLayer = this._getActiveLayer(); 74 | if (!activeLayer) { 75 | return null; 76 | } else if (this._previousLayerId) { 77 | return this._layers[this._previousLayerId]; 78 | } else { 79 | return find(this._layers, function(l) { 80 | return l.id !== activeLayer.id; 81 | }.bind(this)) || null; 82 | } 83 | }, 84 | _getInactiveLayers: function() { 85 | var ar = []; 86 | var activeLayerId = this._getActiveLayer() ? this._getActiveLayer().id : null; 87 | var previousLayerId = this._getPreviousLayer() ? this._getPreviousLayer().id : null; 88 | each(this._layers, function(l) { 89 | if ((l.id !== activeLayerId) && (l.id !== previousLayerId)) { 90 | ar.push(l); 91 | } 92 | }); 93 | return ar; 94 | }, 95 | _arrangeLayers: function() { 96 | var behaviors = {}; 97 | behaviors.previous = function() { 98 | var layers = this._getInactiveLayers(); 99 | if (this._getActiveLayer()) { 100 | layers.unshift(this._getActiveLayer()); 101 | } 102 | if (this._getPreviousLayer()) { 103 | layers.unshift(this._getPreviousLayer()); 104 | } 105 | return layers; 106 | }; 107 | return behaviors[this.options.behavior].apply(this, arguments); 108 | }, 109 | _getLayerCellByLayerId: function(id) { 110 | var els = this._container.getElementsByClassName('leaflet-iconLayers-layerCell'); 111 | for (var i = 0; i < els.length; i++) { 112 | if (els[i].getAttribute('data-layerid') == id) { 113 | return els[i]; 114 | } 115 | } 116 | }, 117 | _createLayerElement: function(layerObj) { 118 | var el = L.DomUtil.create('div', 'leaflet-iconLayers-layer'); 119 | if (layerObj.title) { 120 | var titleContainerEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerTitleContainer'); 121 | var titleEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerTitle'); 122 | var checkIconEl = L.DomUtil.create('div', 'leaflet-iconLayers-layerCheckIcon'); 123 | titleEl.innerHTML = layerObj.title; 124 | titleContainerEl.appendChild(titleEl); 125 | el.appendChild(titleContainerEl); 126 | el.appendChild(checkIconEl); 127 | } 128 | if (layerObj.icon) { 129 | el.setAttribute('style', 'background-image: url(\'' + layerObj.icon + '\')'); 130 | } 131 | return el; 132 | }, 133 | _createLayerElements: function() { 134 | var currentRow, layerCell; 135 | var layers = this._arrangeLayers(); 136 | var activeLayerId = this._getActiveLayer() && this._getActiveLayer().id; 137 | 138 | for (var i = 0; i < layers.length; i++) { 139 | if (i % this.options.maxLayersInRow === 0) { 140 | currentRow = L.DomUtil.create('div', 'leaflet-iconLayers-layersRow'); 141 | if (this.options.position.indexOf('bottom') === -1) { 142 | this._container.appendChild(currentRow); 143 | } else { 144 | prepend(this._container, currentRow); 145 | } 146 | } 147 | layerCell = L.DomUtil.create('div', 'leaflet-iconLayers-layerCell'); 148 | layerCell.setAttribute('data-layerid', layers[i].id); 149 | if (i !== 0) { 150 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_hidden'); 151 | } 152 | if (layers[i].id === activeLayerId) { 153 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_active'); 154 | } 155 | if (this._expandDirection === 'left') { 156 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_expandLeft'); 157 | } else { 158 | L.DomUtil.addClass(layerCell, 'leaflet-iconLayers-layerCell_expandRight'); 159 | } 160 | layerCell.appendChild(this._createLayerElement(layers[i])); 161 | 162 | if (this.options.position.indexOf('right') === -1) { 163 | currentRow.appendChild(layerCell); 164 | } else { 165 | prepend(currentRow, layerCell); 166 | } 167 | } 168 | }, 169 | _onLayerClick: function(e) { 170 | e.stopPropagation(); 171 | var layerId = e.currentTarget.getAttribute('data-layerid'); 172 | var layer = this._layers[layerId]; 173 | this.setActiveLayer(layer.layer); 174 | this.expand(); 175 | }, 176 | _attachEvents: function() { 177 | each(this._layers, function(l) { 178 | var e = this._getLayerCellByLayerId(l.id); 179 | if (e) { 180 | e.addEventListener('click', this._onLayerClick.bind(this)); 181 | } 182 | }.bind(this)); 183 | var layersRowCollection = this._container.getElementsByClassName('leaflet-iconLayers-layersRow'); 184 | 185 | var onMouseEnter = function(e) { 186 | e.stopPropagation(); 187 | this.expand(); 188 | }.bind(this); 189 | 190 | var onMouseLeave = function(e) { 191 | e.stopPropagation(); 192 | this.collapse(); 193 | }.bind(this); 194 | 195 | var stopPropagation = function(e) { 196 | e.stopPropagation(); 197 | }; 198 | 199 | //TODO Don't make functions within a loop. 200 | for (var i = 0; i < layersRowCollection.length; i++) { 201 | var el = layersRowCollection[i]; 202 | el.addEventListener('mouseenter', onMouseEnter); 203 | el.addEventListener('mouseleave', onMouseLeave); 204 | el.addEventListener('mousemove', stopPropagation); 205 | } 206 | }, 207 | _render: function() { 208 | this._container.innerHTML = ''; 209 | this._createLayerElements(); 210 | this._attachEvents(); 211 | }, 212 | _switchMapLayers: function() { 213 | if (!this._map) { 214 | return; 215 | } 216 | var activeLayer = this._getActiveLayer(); 217 | var previousLayer = this._getPreviousLayer(); 218 | if (previousLayer) { 219 | this._map.removeLayer(previousLayer.layer); 220 | } else { 221 | each(this._layers, function(layerObject) { 222 | var layer = layerObject.layer; 223 | this._map.removeLayer(layer); 224 | }.bind(this)); 225 | } 226 | if (activeLayer) { 227 | this._map.addLayer(activeLayer.layer); 228 | } 229 | }, 230 | options: { 231 | position: 'bottomleft', // one of expanding directions depends on this 232 | behavior: 'previous', // may be 'previous', 'expanded' or 'first' 233 | expand: 'horizontal', // or 'vertical' 234 | autoZIndex: true, // from L.Control.Layers 235 | maxLayersInRow: 5, 236 | manageLayers: true 237 | }, 238 | initialize: function(layers, options) { 239 | if (!L.Util.isArray(arguments[0])) { 240 | // first argument is options 241 | options = layers; 242 | layers = []; 243 | } 244 | L.setOptions(this, options); 245 | this._expandDirection = (this.options.position.indexOf('left') != -1) ? 'right' : 'left'; 246 | if (this.options.manageLayers) { 247 | this.on('activelayerchange', this._switchMapLayers, this); 248 | } 249 | this.setLayers(layers); 250 | }, 251 | onAdd: function(map) { 252 | this._container = L.DomUtil.create('div', 'leaflet-iconLayers'); 253 | L.DomUtil.addClass(this._container, 'leaflet-iconLayers_' + this.options.position); 254 | this._render(); 255 | map.on('click', this.collapse, this); 256 | if (this.options.manageLayers) { 257 | this._switchMapLayers(); 258 | } 259 | return this._container; 260 | }, 261 | onRemove: function(map) { 262 | map.off('click', this.collapse, this); 263 | }, 264 | setLayers: function(layers) { 265 | this._layers = {}; 266 | layers.map(function(layer) { 267 | var id = L.stamp(layer.layer); 268 | this._layers[id] = L.extend(layer, { 269 | id: id 270 | }); 271 | }.bind(this)); 272 | if (this._container) { 273 | this._render(); 274 | } 275 | }, 276 | setActiveLayer: function(layer) { 277 | var l = layer && this._layers[L.stamp(layer)]; 278 | if (!l || l.id === this._activeLayerId) { 279 | return; 280 | } 281 | this._previousLayerId = this._activeLayerId; 282 | this._activeLayerId = l.id; 283 | if (this._container) { 284 | this._render(); 285 | } 286 | this.fire('activelayerchange', { 287 | layer: layer 288 | }); 289 | }, 290 | expand: function() { 291 | this._arrangeLayers().slice(1).map(function(l) { 292 | var el = this._getLayerCellByLayerId(l.id); 293 | L.DomUtil.removeClass(el, 'leaflet-iconLayers-layerCell_hidden'); 294 | }.bind(this)); 295 | }, 296 | collapse: function() { 297 | this._arrangeLayers().slice(1).map(function(l) { 298 | var el = this._getLayerCellByLayerId(l.id); 299 | L.DomUtil.addClass(el, 'leaflet-iconLayers-layerCell_hidden'); 300 | }.bind(this)); 301 | } 302 | }); 303 | 304 | var iconLayers = function(layers, options) { 305 | return new IconLayers(layers, options); 306 | }; 307 | 308 | iconLayers.Constructor = IconLayers; 309 | 310 | return iconLayers; 311 | }); -------------------------------------------------------------------------------- /src/transparent-pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScanEx/Leaflet-IconLayers/ea9af7695d730f5ac3f46edd9d789093e4b3bfc4/src/transparent-pixel.png --------------------------------------------------------------------------------