├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.html ├── index.js ├── layout.css ├── leaflet-side-by-side.js ├── leaflet-side-by-side.min.js ├── package.json ├── range-icon.png ├── range.css └── screencast.gif /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | leaflet-side-by-side.js 2 | leaflet-side-by-side.min.js 3 | screencast.gif 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## Unreleased 7 | 8 | - ADDED: Allows Leaflet version 0.7.7 through 1.x 9 | 10 | ## [v2.0.0] - 2015-12-08 11 | 12 | - ADDED: Add `setLeftLayers()` and `setRightLayers()` methods 13 | - ADDED: `options.padding` 14 | - ADDED: `getPosition()` returns the x coordinate (relative to the map container) of the divider 15 | - FIXED: **[BREAKING]** Export factory function on `L.control` not `L.Control` 16 | - FIXED: Slider drag was not working on touch devices 17 | 18 | ## [v1.1.1] - 2015-12-03 19 | 20 | - FIXED: fix package.json settings for npm distribution 21 | 22 | ## [v1.1.0] - 2015-12-03 23 | 24 | - ADDED: Events 25 | - FIXED: Fix initial divider position in Firefox, should start in middle of map 26 | 27 | ## v1.0.2 - 2015-12-02 28 | 29 | Initial release 30 | 31 | [Unreleased]: https://github.com/digidem/leaflet-side-by-side/compare/v2.0.0...HEAD 32 | [Unreleased]: https://github.com/digidem/leaflet-side-by-side/compare/v1.1.1...v2.0.0 33 | [v1.1.1]: https://github.com/digidem/leaflet-side-by-side/compare/v1.1.0...v1.1.1 34 | [v1.1.0]: https://github.com/digidem/leaflet-side-by-side/compare/v1.0.2...v1.1.0 35 | 36 | ## v1.0.3 - 2022-8-22 37 | 38 | Update to support leaflet 1.8 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Gregor MacLennan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaflet-side-by-side 2 | 3 | A Leaflet control to add a split screen to compare two map overlays. 4 | 5 | ![screencast example](screencast.gif) 6 | 7 | ### L.control.sideBySide(_leftLayer[s]_, _rightLayer[s]_) 8 | 9 | Creates a new Leaflet Control for comparing two layers or collections of layers. It does not add the layers to the map - you need to do that manually. Extends `L.Control` but `setPosition()` and `getPosition` are `noop` because the position is always the same - it does not make sense for this control to be in the corner like other Leaflet controls. 10 | 11 | ### Parameters 12 | 13 | | parameter | type | description | 14 | | ---------- | -------------- | ------------- | 15 | | `leftLayers` | L.Layer\|array | A Leaflet Layer or array of layers to show on the left side of the map. Any layer added to the map that is in this array will be shown on the left | 16 | | `rightLayers` | L.Layer\|array | A Leaflet Layer or array of layers to show on the right side of the map. Any layer added to the map that is in this array will be shown on the right. These *should not be* the same as any layers in `leftLayers` | 17 | | `options` | Object | Options | 18 | | `options.padding` | Number | Padding between slider min/max and the edge of the screen in pixels. Defaults to `44` - the width of the slider thumb | 19 | 20 | ### Events 21 | 22 | Subscribe to events using [these methods](http://leafletjs.com/reference.html#events) 23 | 24 | | Event | Data | Description | 25 | | ---------- | -------------- | ------------- | 26 | | `leftlayeradd` | [LayerEvent](http://leafletjs.com/reference.html#layer-event) | Fired when a layer is added to the left-hand-side pane | 27 | | `leftlayerremove` | [LayerEvent](http://leafletjs.com/reference.html#layer-event) | Fired when a layer is removed from the left-hand-side pane | 28 | | `rightlayeradd` | [LayerEvent](http://leafletjs.com/reference.html#layer-event) | Fired when a layer is added to the right-hand-side pane | 29 | | `rightlayerremove` | [LayerEvent](http://leafletjs.com/reference.html#layer-event) | You guessed it... fired when a layer is removed from the right-hand-side pane | 30 | | `dividermove` | {x: Number} | Fired when the divider is moved. Returns an event object with the property `x` = the pixels of the divider from the left side of the map container. | 31 | 32 | ### Methods 33 | 34 | | Method | Returns | Description | 35 | | ---------- | -------------- | ------------- | 36 | | `setLeftLayers` | `this` | Set the layer(s) for the left side | 37 | | `setRightLayers` | `this` | Set the layer(s) for the right side | 38 | 39 | ### Usage 40 | 41 | Add the script to the top of your page (css is included in the javascript): 42 | 43 | ```html 44 | 45 | ``` 46 | 47 | Or if you are using browserify: 48 | 49 | ```js 50 | var sideBySide = require('leaflet-side-by-side') 51 | ``` 52 | 53 | Then create a map, add two layers to it, and create the SideBySide control and add it to the map: 54 | 55 | ```js 56 | var map = L.map('map').setView([51.505, -0.09], 13); 57 | 58 | var myLayer1 = L.tileLayer(...).addTo(map); 59 | 60 | var myLayer2 = L.tileLayer(...).addTo(map) 61 | 62 | L.control.sideBySide(myLayer1, myLayer2).addTo(map); 63 | ``` 64 | 65 | ### Example 66 | 67 | [Live Example](http://lab.digital-democracy.org/leaflet-side-by-side/) see [source](index.html) 68 | 69 | ### Limitations 70 | 71 | - The divider is not movable with IE. 72 | - Probably won't work in IE8, but what does? 73 | 74 | ### License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Leaflet Side-by-side 7 | 8 | 9 | 10 | 22 | 23 | 24 | 25 |
26 | 27 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet') 2 | require('./layout.css') 3 | require('./range.css') 4 | 5 | var mapWasDragEnabled 6 | var mapWasTapEnabled 7 | 8 | // Leaflet v0.7 backwards compatibility 9 | function on (el, types, fn, context) { 10 | types.split(' ').forEach(function (type) { 11 | L.DomEvent.on(el, type, fn, context) 12 | }) 13 | } 14 | 15 | // Leaflet v0.7 backwards compatibility 16 | function off (el, types, fn, context) { 17 | types.split(' ').forEach(function (type) { 18 | L.DomEvent.off(el, type, fn, context) 19 | }) 20 | } 21 | 22 | function getRangeEvent (rangeInput) { 23 | return 'oninput' in rangeInput ? 'input' : 'change' 24 | } 25 | 26 | function cancelMapDrag () { 27 | mapWasDragEnabled = this._map.dragging.enabled() 28 | mapWasTapEnabled = this._map.tap && this._map.tap.enabled() 29 | this._map.dragging.disable() 30 | this._map.tap && this._map.tap.disable() 31 | } 32 | 33 | function uncancelMapDrag (e) { 34 | this._refocusOnMap(e) 35 | if (mapWasDragEnabled) { 36 | this._map.dragging.enable() 37 | } 38 | if (mapWasTapEnabled) { 39 | this._map.tap.enable() 40 | } 41 | } 42 | 43 | // convert arg to an array - returns empty array if arg is undefined 44 | function asArray (arg) { 45 | return (arg === 'undefined') ? [] : Array.isArray(arg) ? arg : [arg] 46 | } 47 | 48 | function noop () {} 49 | 50 | L.Control.SideBySide = L.Control.extend({ 51 | options: { 52 | thumbSize: 42, 53 | padding: 0 54 | }, 55 | 56 | initialize: function (leftLayers, rightLayers, options) { 57 | this.setLeftLayers(leftLayers) 58 | this.setRightLayers(rightLayers) 59 | L.setOptions(this, options) 60 | }, 61 | 62 | getPosition: function () { 63 | var rangeValue = this._range.value 64 | var offset = (0.5 - rangeValue) * (2 * this.options.padding + this.options.thumbSize) 65 | return this._map.getSize().x * rangeValue + offset 66 | }, 67 | 68 | setPosition: noop, 69 | 70 | includes: L.Evented.prototype || L.Mixin.Events, 71 | 72 | addTo: function (map) { 73 | this.remove() 74 | this._map = map 75 | 76 | var container = this._container = L.DomUtil.create('div', 'leaflet-sbs', map._controlContainer) 77 | 78 | this._divider = L.DomUtil.create('div', 'leaflet-sbs-divider', container) 79 | var range = this._range = L.DomUtil.create('input', 'leaflet-sbs-range', container) 80 | range.type = 'range' 81 | range.min = 0 82 | range.max = 1 83 | range.step = 'any' 84 | range.value = 0.5 85 | range.style.paddingLeft = range.style.paddingRight = this.options.padding + 'px' 86 | this._addEvents() 87 | this._updateLayers() 88 | return this 89 | }, 90 | 91 | remove: function () { 92 | if (!this._map) { 93 | return this 94 | } 95 | if (this._leftLayer) { 96 | this._leftLayer.getContainer().style.clip = '' 97 | } 98 | if (this._rightLayer) { 99 | this._rightLayer.getContainer().style.clip = '' 100 | } 101 | this._removeEvents() 102 | L.DomUtil.remove(this._container) 103 | 104 | this._map = null 105 | 106 | return this 107 | }, 108 | 109 | setLeftLayers: function (leftLayers) { 110 | this._leftLayers = asArray(leftLayers) 111 | this._updateLayers() 112 | return this 113 | }, 114 | 115 | setRightLayers: function (rightLayers) { 116 | this._rightLayers = asArray(rightLayers) 117 | this._updateLayers() 118 | return this 119 | }, 120 | 121 | _updateClip: function () { 122 | var map = this._map 123 | var nw = map.containerPointToLayerPoint([0, 0]) 124 | var se = map.containerPointToLayerPoint(map.getSize()) 125 | var clipX = nw.x + this.getPosition() 126 | var dividerX = this.getPosition() 127 | 128 | this._divider.style.left = dividerX + 'px' 129 | this.fire('dividermove', {x: dividerX}) 130 | var clipLeft = 'rect(' + [nw.y, clipX, se.y, nw.x].join('px,') + 'px)' 131 | var clipRight = 'rect(' + [nw.y, se.x, se.y, clipX].join('px,') + 'px)' 132 | if (this._leftLayer) { 133 | this._leftLayer.getContainer().style.clip = clipLeft 134 | } 135 | if (this._rightLayer) { 136 | this._rightLayer.getContainer().style.clip = clipRight 137 | } 138 | }, 139 | 140 | _updateLayers: function () { 141 | if (!this._map) { 142 | return this 143 | } 144 | var prevLeft = this._leftLayer 145 | var prevRight = this._rightLayer 146 | this._leftLayer = this._rightLayer = null 147 | this._leftLayers.forEach(function (layer) { 148 | if (this._map.hasLayer(layer)) { 149 | this._leftLayer = layer 150 | } 151 | }, this) 152 | this._rightLayers.forEach(function (layer) { 153 | if (this._map.hasLayer(layer)) { 154 | this._rightLayer = layer 155 | } 156 | }, this) 157 | if (prevLeft !== this._leftLayer) { 158 | prevLeft && this.fire('leftlayerremove', {layer: prevLeft}) 159 | this._leftLayer && this.fire('leftlayeradd', {layer: this._leftLayer}) 160 | } 161 | if (prevRight !== this._rightLayer) { 162 | prevRight && this.fire('rightlayerremove', {layer: prevRight}) 163 | this._rightLayer && this.fire('rightlayeradd', {layer: this._rightLayer}) 164 | } 165 | this._updateClip() 166 | }, 167 | 168 | _addEvents: function () { 169 | var range = this._range; 170 | var map = this._map; 171 | if (!map || !range) return; 172 | map.on("move", this._updateClip, this); 173 | map.on("layeradd layerremove", this._updateLayers, this); 174 | L.DomEvent.on(range, getRangeEvent(range), this._updateClip, this); 175 | L.DomEvent.on(range, "touchstart", cancelMapDrag, this); 176 | L.DomEvent.on(range, "touchend", uncancelMapDrag, this); 177 | L.DomEvent.on(range, "mousedown", cancelMapDrag, this); 178 | L.DomEvent.on(range, "mouseup", uncancelMapDrag, this); 179 | }, 180 | 181 | _removeEvents: function () { 182 | var range = this._range; 183 | var map = this._map; 184 | if (range) { 185 | L.DomEvent.off(range, getRangeEvent(range), this._updateClip, this); 186 | L.DomEvent.off(range, "touchstart", cancelMapDrag, this); 187 | L.DomEvent.off(range, "touchend", uncancelMapDrag, this); 188 | L.DomEvent.off(range, "mousedown", cancelMapDrag, this); 189 | L.DomEvent.off(range, "mouseup", uncancelMapDrag, this); 190 | } 191 | if (map) { 192 | map.off("layeradd layerremove", this._updateLayers, this); 193 | map.off("move", this._updateClip, this); 194 | } 195 | }, 196 | }) 197 | 198 | L.control.sideBySide = function (leftLayers, rightLayers, options) { 199 | return new L.Control.SideBySide(leftLayers, rightLayers, options) 200 | } 201 | 202 | module.exports = L.Control.SideBySide 203 | -------------------------------------------------------------------------------- /layout.css: -------------------------------------------------------------------------------- 1 | .leaflet-sbs-range { 2 | position: absolute; 3 | top: 50%; 4 | width: 100%; 5 | z-index: 999; 6 | } 7 | .leaflet-sbs-divider { 8 | position: absolute; 9 | top: 0; 10 | bottom: 0; 11 | left: 50%; 12 | margin-left: -2px; 13 | width: 4px; 14 | background-color: #fff; 15 | pointer-events: none; 16 | z-index: 999; 17 | } 18 | -------------------------------------------------------------------------------- /leaflet-side-by-side.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i leaflet-side-by-side.js", 18 | "postbuild": "uglifyjs leaflet-side-by-side.js -cm -o leaflet-side-by-side.min.js", 19 | "preversion": "npm test && npm run build", 20 | "lint": "standard index.js", 21 | "start": "budo index.js:leaflet-side-by-side.js --live", 22 | "test": "npm run lint" 23 | }, 24 | "keywords": [ 25 | "leaflet" 26 | ], 27 | "author": "Gregor MacLennan / Digital Democracy", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "browserify": "^16.2.2", 31 | "budo": "^11.2.0", 32 | "standard": "^11.0.1", 33 | "uglify-js": "^3.3.24" 34 | }, 35 | "dependencies": { 36 | "browserify-shim": "^3.8.14", 37 | "css-img-datauri-stream": "^0.1.5", 38 | "cssify": "^1.0.3", 39 | "leaflet": ">=0.7.7 <2.0.0" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/digidem/leaflet-side-by-side.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/digidem/leaflet-side-by-side/issues" 47 | }, 48 | "homepage": "https://github.com/digidem/leaflet-side-by-side#readme" 49 | } 50 | -------------------------------------------------------------------------------- /range-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digidem/leaflet-side-by-side/cd74619044c9f0533efa090161c335f2ccea13e7/range-icon.png -------------------------------------------------------------------------------- /range.css: -------------------------------------------------------------------------------- 1 | .leaflet-sbs-range { 2 | -webkit-appearance: none; 3 | display: inline-block!important; 4 | vertical-align: middle; 5 | height: 0; 6 | padding: 0; 7 | margin: 0; 8 | border: 0; 9 | background: rgba(0, 0, 0, 0.25); 10 | min-width: 100px; 11 | cursor: pointer; 12 | pointer-events: none; 13 | z-index: 999; 14 | } 15 | .leaflet-sbs-range::-ms-fill-upper { 16 | background: transparent; 17 | } 18 | .leaflet-sbs-range::-ms-fill-lower { 19 | background: rgba(255, 255, 255, 0.25); 20 | } 21 | /* Browser thingies */ 22 | 23 | .leaflet-sbs-range::-moz-range-track { 24 | opacity: 0; 25 | } 26 | .leaflet-sbs-range::-ms-track { 27 | opacity: 0; 28 | } 29 | .leaflet-sbs-range::-ms-tooltip { 30 | display: none; 31 | } 32 | /* For whatever reason, these need to be defined 33 | * on their own so dont group them */ 34 | 35 | .leaflet-sbs-range::-webkit-slider-thumb { 36 | -webkit-appearance: none; 37 | margin: 0; 38 | padding: 0; 39 | background: #fff; 40 | height: 40px; 41 | width: 40px; 42 | border-radius: 20px; 43 | cursor: ew-resize; 44 | pointer-events: auto; 45 | border: 1px solid #ddd; 46 | background-image: url(range-icon.png); 47 | background-position: 50% 50%; 48 | background-repeat: no-repeat; 49 | background-size: 40px 40px; 50 | } 51 | .leaflet-sbs-range::-ms-thumb { 52 | margin: 0; 53 | padding: 0; 54 | background: #fff; 55 | height: 40px; 56 | width: 40px; 57 | border-radius: 20px; 58 | cursor: ew-resize; 59 | pointer-events: auto; 60 | border: 1px solid #ddd; 61 | background-image: url(range-icon.png); 62 | background-position: 50% 50%; 63 | background-repeat: no-repeat; 64 | background-size: 40px 40px; 65 | } 66 | .leaflet-sbs-range::-moz-range-thumb { 67 | padding: 0; 68 | right: 0 ; 69 | background: #fff; 70 | height: 40px; 71 | width: 40px; 72 | border-radius: 20px; 73 | cursor: ew-resize; 74 | pointer-events: auto; 75 | border: 1px solid #ddd; 76 | background-image: url(range-icon.png); 77 | background-position: 50% 50%; 78 | background-repeat: no-repeat; 79 | background-size: 40px 40px; 80 | } 81 | .leaflet-sbs-range:disabled::-moz-range-thumb { 82 | cursor: default; 83 | } 84 | .leaflet-sbs-range:disabled::-ms-thumb { 85 | cursor: default; 86 | } 87 | .leaflet-sbs-range:disabled::-webkit-slider-thumb { 88 | cursor: default; 89 | } 90 | .leaflet-sbs-range:disabled { 91 | cursor: default; 92 | } 93 | .leaflet-sbs-range:focus { 94 | outline: none!important; 95 | } 96 | .leaflet-sbs-range::-moz-focus-outer { 97 | border: 0; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digidem/leaflet-side-by-side/cd74619044c9f0533efa090161c335f2ccea13e7/screencast.gif --------------------------------------------------------------------------------