├── img ├── geohash.png └── quadtree.png ├── style.css ├── README.md ├── map.js ├── leaflet.label.css ├── index.html ├── L.Control.Button.js ├── leaflet.label.js ├── plugin.js └── geohash.js /img/geohash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missinglink/leaflet-spatial-prefix-tree/HEAD/img/geohash.png -------------------------------------------------------------------------------- /img/quadtree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missinglink/leaflet-spatial-prefix-tree/HEAD/img/quadtree.png -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | 2 | .leaflet-label, 3 | .leaflet-label:before { 4 | border:none; 5 | font-size: 10px; 6 | } 7 | 8 | .my-label { 9 | color: white; 10 | background-color: rgba( 255, 0, 0, 0.5 ); 11 | } 12 | 13 | .my-label2 { 14 | font-size: 30px; 15 | color: rgba( 255, 0, 0, 0.5 ); 16 | background-color: transparent; 17 | } 18 | 19 | #buttons { 20 | font-size: 20px; 21 | font-family: sans-serif; 22 | font-weight: bold; 23 | text-transform: uppercase; 24 | position: absolute; 25 | top: 10px; 26 | right: 10px; 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Leaflet plugin for visualizing spatial prefix trees, quadtree and geohash 3 | 4 | ### Demo 5 | 6 | http://missinglink.github.io/leaflet-spatial-prefix-tree/ 7 | 8 | #### Changing Algorithms 9 | 10 | You can toggle the algorithm with the button located at the top-right. 11 | 12 | #### Quadtree 13 | 14 | ![quadtree](https://raw.githubusercontent.com/missinglink/leaflet-spatial-prefix-tree/master/img/quadtree.png) 15 | 16 | #### Geohash 17 | 18 | ![geohash](https://raw.githubusercontent.com/missinglink/leaflet-spatial-prefix-tree/master/img/geohash.png) 19 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | 2 | var map = L.map('map'); 3 | 4 | // create the tile layer with correct attribution 5 | // var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; 6 | // var osmAttrib = 'Map data © OpenStreetMap contributors'; 7 | // var osm = new L.TileLayer( osmUrl ); 8 | 9 | L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', { 10 | attribution: 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL.', 11 | maxZoom: 18 12 | }).addTo(map); 13 | 14 | L.control.geocoder('search-fljxAAA').addTo(map); 15 | 16 | // start the map in South-East England 17 | map.setView( new L.LatLng( 51.5072, 0.1275 ), 0 ); 18 | 19 | $(document).ready(function() { 20 | $('#buttons button').on('click', function(event) { 21 | $('#buttons button').removeClass('active'); 22 | $(event.target).addClass('active'); 23 | changeHashFunction( event.target.id ); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /leaflet.label.css: -------------------------------------------------------------------------------- 1 | .leaflet-label { 2 | background: rgb(235, 235, 235); 3 | background: rgba(235, 235, 235, 0.81); 4 | background-clip: padding-box; 5 | border-color: #777; 6 | border-color: rgba(0,0,0,0.25); 7 | border-radius: 4px; 8 | border-style: solid; 9 | border-width: 4px; 10 | color: #111; 11 | display: block; 12 | font: 12px/20px "Helvetica Neue", Arial, Helvetica, sans-serif; 13 | font-weight: bold; 14 | padding: 1px 6px; 15 | position: absolute; 16 | -webkit-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; 20 | pointer-events: none; 21 | white-space: nowrap; 22 | z-index: 6; 23 | } 24 | 25 | .leaflet-label.leaflet-clickable { 26 | cursor: pointer; 27 | } 28 | 29 | .leaflet-label:before, 30 | .leaflet-label:after { 31 | border-top: 6px solid transparent; 32 | border-bottom: 6px solid transparent; 33 | content: none; 34 | position: absolute; 35 | top: 5px; 36 | } 37 | 38 | .leaflet-label:before { 39 | border-right: 6px solid black; 40 | border-right-color: inherit; 41 | left: -10px; 42 | } 43 | 44 | .leaflet-label:after { 45 | border-left: 6px solid black; 46 | border-left-color: inherit; 47 | right: -10px; 48 | } 49 | 50 | .leaflet-label-right:before, 51 | .leaflet-label-left:after { 52 | content: ""; 53 | } 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /L.Control.Button.js: -------------------------------------------------------------------------------- 1 | // Author: Jerroyd Moore 2 | 3 | L.Control.Button = L.Control.extend({ 4 | includes: L.Mixin.Events, 5 | options: { 6 | position: 'topright', 7 | }, 8 | initialize: function (label, options) { 9 | L.setOptions(this, options); 10 | var button = null; 11 | 12 | if (label instanceof HTMLElement) { 13 | button = label; 14 | try { 15 | button.parentNode.removeChild(button); 16 | } catch (e) { } 17 | } else if (typeof label === "string") { 18 | button = L.DomUtil.create('button', this.options.className) 19 | button.innerHTML = label; 20 | } else { 21 | throw new Error('L.Control.Button: failed to initialize, label must either be text or a dom element'); 22 | } 23 | 24 | L.DomUtil.addClass(button, this.options.position); 25 | 26 | this._container = button; 27 | 28 | return this; 29 | }, 30 | isToggled: function () { 31 | return L.DomUtil.hasClass(this._container, this.options.toggleButton); 32 | }, 33 | _fireClick: function (e) { 34 | this.fire('click'); 35 | 36 | if (this.options.toggleButton) { 37 | var btn = this._container; 38 | if (this.isToggled()) { 39 | L.DomUtil.removeClass(this._container, this.options.toggleButton); 40 | } else { 41 | L.DomUtil.addClass(this._container, this.options.toggleButton); 42 | } 43 | } 44 | }, 45 | onAdd: function (map) { 46 | if (this._container) { 47 | L.DomEvent.on(this._container, 'click', this._fireClick, this); 48 | var stop = L.DomEvent.stopPropagation; 49 | L.DomEvent.on(this._container, 'mousedown', stop) 50 | .on(this._container, 'touchstart', stop) 51 | .on(this._container, 'dblclick', stop) 52 | .on(this._container, 'mousewheel', stop) 53 | .on(this._container, 'MozMozMousePixelScroll', stop) 54 | this.fire('load'); 55 | 56 | this._map = map; 57 | } 58 | 59 | return this._container; 60 | }, 61 | onRemove: function (map) { 62 | if (this._container && this._map) { 63 | L.DomEvent.off(this._container, 'click', this._fireClick, this); 64 | L.DomEvent.off(this._container, 'mousedown', stop) 65 | .off(this._container, 'touchstart', stop) 66 | .off(this._container, 'dblclick', stop) 67 | .off(this._container, 'mousewheel', stop) 68 | .off(this._container, 'MozMozMousePixelScroll', stop) 69 | 70 | this.fire('unload'); 71 | this._map = null; 72 | } 73 | 74 | return this; 75 | } 76 | }); 77 | 78 | L.control.button = function (label, options) { 79 | return new L.Control.Button(label, options); 80 | }; -------------------------------------------------------------------------------- /leaflet.label.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.label, a plugin that adds labels to markers and vectors for Leaflet powered maps. 3 | (c) 2012-2013, Jacob Toye, Smartrak 4 | 5 | https://github.com/Leaflet/Leaflet.label 6 | http://leafletjs.com 7 | https://github.com/jacobtoye 8 | */ 9 | (function(){L.labelVersion="0.2.2-dev",L.Label=L.Class.extend({includes:L.Mixin.Events,options:{className:"",clickable:!1,direction:"right",noHide:!1,offset:[12,-15],opacity:1,zoomAnimation:!0},initialize:function(t,e){L.setOptions(this,t),this._source=e,this._animated=L.Browser.any3d&&this.options.zoomAnimation,this._isOpen=!1},onAdd:function(t){this._map=t,this._pane=this._source instanceof L.Marker?t._panes.markerPane:t._panes.popupPane,this._container||this._initLayout(),this._pane.appendChild(this._container),this._initInteraction(),this._update(),this.setOpacity(this.options.opacity),t.on("moveend",this._onMoveEnd,this).on("viewreset",this._onViewReset,this),this._animated&&t.on("zoomanim",this._zoomAnimation,this),L.Browser.touch&&!this.options.noHide&&L.DomEvent.on(this._container,"click",this.close,this)},onRemove:function(t){this._pane.removeChild(this._container),t.off({zoomanim:this._zoomAnimation,moveend:this._onMoveEnd,viewreset:this._onViewReset},this),this._removeInteraction(),this._map=null},setLatLng:function(t){return this._latlng=L.latLng(t),this._map&&this._updatePosition(),this},setContent:function(t){return this._previousContent=this._content,this._content=t,this._updateContent(),this},close:function(){var t=this._map;t&&(L.Browser.touch&&!this.options.noHide&&L.DomEvent.off(this._container,"click",this.close),t.removeLayer(this))},updateZIndex:function(t){this._zIndex=t,this._container&&this._zIndex&&(this._container.style.zIndex=t)},setOpacity:function(t){this.options.opacity=t,this._container&&L.DomUtil.setOpacity(this._container,t)},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-label "+this.options.className+" leaflet-zoom-animated"),this.updateZIndex(this._zIndex)},_update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updatePosition(),this._container.style.visibility="")},_updateContent:function(){this._content&&this._map&&this._prevContent!==this._content&&"string"==typeof this._content&&(this._container.innerHTML=this._content,this._prevContent=this._content,this._labelWidth=this._container.offsetWidth)},_updatePosition:function(){var t=this._map.latLngToLayerPoint(this._latlng);this._setPosition(t)},_setPosition:function(t){var e=this._map,i=this._container,n=e.latLngToContainerPoint(e.getCenter()),o=e.layerPointToContainerPoint(t),s=this.options.direction,a=this._labelWidth,l=L.point(this.options.offset);"right"===s||"auto"===s&&o.xi;i++)L.DomEvent.on(t,e[i],this._fireMouseEvent,this)}},_removeInteraction:function(){if(this.options.clickable){var t=this._container,e=["dblclick","mousedown","mouseover","mouseout","contextmenu"];L.DomUtil.removeClass(t,"leaflet-clickable"),L.DomEvent.off(t,"click",this._onMouseClick,this);for(var i=0;e.length>i;i++)L.DomEvent.off(t,e[i],this._fireMouseEvent,this)}},_onMouseClick:function(t){this.hasEventListeners(t.type)&&L.DomEvent.stopPropagation(t),this.fire(t.type,{originalEvent:t})},_fireMouseEvent:function(t){this.fire(t.type,{originalEvent:t}),"contextmenu"===t.type&&this.hasEventListeners(t.type)&&L.DomEvent.preventDefault(t),"mousedown"!==t.type?L.DomEvent.stopPropagation(t):L.DomEvent.preventDefault(t)}}),L.BaseMarkerMethods={showLabel:function(){return this.label&&this._map&&(this.label.setLatLng(this._latlng),this._map.showLabel(this.label)),this},hideLabel:function(){return this.label&&this.label.close(),this},setLabelNoHide:function(t){this._labelNoHide!==t&&(this._labelNoHide=t,t?(this._removeLabelRevealHandlers(),this.showLabel()):(this._addLabelRevealHandlers(),this.hideLabel()))},bindLabel:function(t,e){var i=this.options.icon?this.options.icon.options.labelAnchor:this.options.labelAnchor,n=L.point(i)||L.point(0,0);return n=n.add(L.Label.prototype.options.offset),e&&e.offset&&(n=n.add(e.offset)),e=L.Util.extend({offset:n},e),this._labelNoHide=e.noHide,this.label||(this._labelNoHide||this._addLabelRevealHandlers(),this.on("remove",this.hideLabel,this).on("move",this._moveLabel,this).on("add",this._onMarkerAdd,this),this._hasLabelHandlers=!0),this.label=new L.Label(e,this).setContent(t),this},unbindLabel:function(){return this.label&&(this.hideLabel(),this.label=null,this._hasLabelHandlers&&(this._labelNoHide||this._removeLabelRevealHandlers(),this.off("remove",this.hideLabel,this).off("move",this._moveLabel,this).off("add",this._onMarkerAdd,this)),this._hasLabelHandlers=!1),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},getLabel:function(){return this.label},_onMarkerAdd:function(){this._labelNoHide&&this.showLabel()},_addLabelRevealHandlers:function(){this.on("mouseover",this.showLabel,this).on("mouseout",this.hideLabel,this),L.Browser.touch&&this.on("click",this.showLabel,this)},_removeLabelRevealHandlers:function(){this.off("mouseover",this.showLabel,this).off("mouseout",this.hideLabel,this),L.Browser.touch&&this.off("click",this.showLabel,this)},_moveLabel:function(t){this.label.setLatLng(t.latlng)}},L.Icon.Default.mergeOptions({labelAnchor:new L.Point(9,-20)}),L.Marker.mergeOptions({icon:new L.Icon.Default}),L.Marker.include(L.BaseMarkerMethods),L.Marker.include({_originalUpdateZIndex:L.Marker.prototype._updateZIndex,_updateZIndex:function(t){var e=this._zIndex+t;this._originalUpdateZIndex(t),this.label&&this.label.updateZIndex(e)},_originalSetOpacity:L.Marker.prototype.setOpacity,setOpacity:function(t,e){this.options.labelHasSemiTransparency=e,this._originalSetOpacity(t)},_originalUpdateOpacity:L.Marker.prototype._updateOpacity,_updateOpacity:function(){var t=0===this.options.opacity?0:1;this._originalUpdateOpacity(),this.label&&this.label.setOpacity(this.options.labelHasSemiTransparency?this.options.opacity:t)},_originalSetLatLng:L.Marker.prototype.setLatLng,setLatLng:function(t){return this.label&&!this._labelNoHide&&this.hideLabel(),this._originalSetLatLng(t)}}),L.CircleMarker.mergeOptions({labelAnchor:new L.Point(0,0)}),L.CircleMarker.include(L.BaseMarkerMethods),L.Path.include({bindLabel:function(t,e){return this.label&&this.label.options===e||(this.label=new L.Label(e,this)),this.label.setContent(t),this._showLabelAdded||(this.on("mouseover",this._showLabel,this).on("mousemove",this._moveLabel,this).on("mouseout remove",this._hideLabel,this),L.Browser.touch&&this.on("click",this._showLabel,this),this._showLabelAdded=!0),this},unbindLabel:function(){return this.label&&(this._hideLabel(),this.label=null,this._showLabelAdded=!1,this.off("mouseover",this._showLabel,this).off("mousemove",this._moveLabel,this).off("mouseout remove",this._hideLabel,this)),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},_showLabel:function(t){this.label.setLatLng(t.latlng),this._map.showLabel(this.label)},_moveLabel:function(t){this.label.setLatLng(t.latlng)},_hideLabel:function(){this.label.close()}}),L.Map.include({showLabel:function(t){return this.addLayer(t)}}),L.FeatureGroup.include({clearLayers:function(){return this.unbindLabel(),this.eachLayer(this.removeLayer,this),this},bindLabel:function(t,e){return this.invoke("bindLabel",t,e)},unbindLabel:function(){return this.invoke("unbindLabel")},updateLabelContent:function(t){this.invoke("updateLabelContent",t)}})})(this,document); -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 2 | var labelConfig = { 3 | noHide: true, 4 | className: "my-label", 5 | direction: 'right', 6 | offset: [5, 5], 7 | zoomAnimation: true 8 | }; 9 | 10 | var labelConfig2 = { 11 | noHide: true, 12 | className: "my-label2", 13 | direction: 'right', 14 | offset: [-15, -10], 15 | zoomAnimation: true 16 | }; 17 | 18 | var rectStyle = { 19 | color: "#ff0000", 20 | weight: 1, 21 | opacity: 0.3, 22 | fillOpacity: 0, 23 | lineCap: 'butt' 24 | }; 25 | 26 | var layerGroup = L.layerGroup(); 27 | map.addLayer( layerGroup ); 28 | 29 | var quadAdapter = { 30 | range: ['0','1','2','3'], 31 | encode: function( centroid, precision ){ 32 | var zoom = precision-1; 33 | var tile = getTileXYZ( centroid.lat, centroid.lng, zoom ); 34 | return SlippyToQuad( tile.x, tile.y, tile.z ); 35 | }, 36 | bbox: function( hash ){ 37 | 38 | var tileSize = 256; 39 | var tile = QuadToSlippy(hash); 40 | 41 | // get NorthWest and SouthEast points 42 | var nwTilePoint = new L.Point( tile.x * tileSize, tile.y * tileSize ); 43 | var seTilePoint = new L.Point( tile.x * tileSize, tile.y * tileSize ); 44 | seTilePoint.x += tileSize; 45 | seTilePoint.y += tileSize; 46 | 47 | var nwLatLon = map.unproject( nwTilePoint, tile.z ); 48 | var seLatLon = map.unproject( seTilePoint, tile.z ); 49 | 50 | return { 51 | minlng: nwLatLon.lng, 52 | minlat: seLatLon.lat, 53 | maxlng: seLatLon.lng, 54 | maxlat: nwLatLon.lat 55 | }; 56 | }, 57 | layers: function( currentHash, zoom ){ 58 | var layers = {}; 59 | // if( zoom > 4 ) layers[ currentHash.substr( 0, zoom -4 ) ] = true; 60 | // if( zoom > 3 ) layers[ currentHash.substr( 0, zoom -3 ) ] = true; 61 | if( zoom > 2 ) layers[ currentHash.substr( 0, zoom -2 ) ] = true; 62 | if( zoom > 1 ) layers[ currentHash.substr( 0, zoom -1 ) ] = true; 63 | layers[ currentHash.substr( 0, zoom ) ] = true; 64 | return layers; 65 | }, 66 | labels: function( hash ){ 67 | return { 68 | long: hash, 69 | short: hash.substr(-1, 1) 70 | }; 71 | } 72 | }; 73 | 74 | var slippyAdapter = { 75 | range: quadAdapter.range, 76 | encode: quadAdapter.encode, 77 | bbox: quadAdapter.bbox, 78 | layers: quadAdapter.layers, 79 | labels: function( hash ){ 80 | var tile = QuadToSlippy( hash ); 81 | return { 82 | long: [ tile.z, tile.x, tile.y ].join('/'), 83 | short: '' 84 | }; 85 | } 86 | }; 87 | 88 | /** Converts numeric degrees to radians */ 89 | if (typeof(Number.prototype.toRad) === "undefined") { 90 | Number.prototype.toRad = function() { 91 | return this * Math.PI / 180; 92 | }; 93 | } 94 | 95 | function getTileXYZ(lat, lon, zoom) { 96 | var xtile = parseInt(Math.floor( (lon + 180) / 360 * (1< 0; i--) { 124 | var digit = '0'; 125 | var mask = 1 << (i - 1); 126 | if( (x & mask) !== 0 ){ 127 | digit++; 128 | } 129 | if( (y & mask) !== 0 ){ 130 | digit++; 131 | digit++; 132 | } 133 | quadKey.push(digit); 134 | } 135 | return quadKey.join(''); 136 | } 137 | 138 | // function long2tile(lon,zoom) { return (Math.floor((lon+180)/360*Math.pow(2,zoom))); } 139 | // function lat2tile(lat,zoom) { return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom))); } 140 | // function tile2long(x,z) { 141 | // return (x/Math.pow(2,z)*360-180); 142 | // } 143 | // function tile2lat(y,z) { 144 | // var n=Math.PI-2*Math.PI*y/Math.pow(2,z); 145 | // return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)))); 146 | // } 147 | 148 | var hashAdapter = { 149 | range: Object.keys( BASE32_CODES_DICT ), 150 | encode: function( centroid, precision ){ 151 | return '' + geohash.encode( centroid.lat, centroid.lng, precision ); 152 | }, 153 | bbox: function( str ){ 154 | var box = geohash.decode_bbox( '' + str ); 155 | return { minlat: box[0], minlng: box[1], maxlat: box[2], maxlng: box[3] }; 156 | }, 157 | layers: function( currentHash, zoom ){ 158 | var layers = {}; 159 | layers[ '' ] = true; 160 | for( var x=1; x<7; x++ ){ 161 | if( zoom >= (x*3) && zoom < ((x+2)*3) ){ 162 | layers[ '' + currentHash.substr( 0, x ) ] = true; 163 | } 164 | } 165 | return layers; 166 | }, 167 | labels: function( hash ){ 168 | return { 169 | long: hash, 170 | short: hash.substr(-1, 1) 171 | }; 172 | } 173 | }; 174 | 175 | var currentHash; 176 | var adapter = quadAdapter; 177 | // var adapter = hashAdapter; 178 | 179 | var mousePositionEvent = null; 180 | 181 | var generateCurrentHash = function( precision ){ 182 | 183 | var center = map.getCenter(); 184 | 185 | if( mousePositionEvent ){ 186 | center = mousePositionEvent.latlng; 187 | // console.log( center ); 188 | } 189 | 190 | return adapter.encode( center, precision ); 191 | }; 192 | 193 | var prevHash = 'NOTAHASH'; 194 | var changeHashFunction = function( algorithm ){ 195 | if( algorithm == 'geohash' ) adapter = hashAdapter; 196 | else if( algorithm == 'slippy' ) adapter = slippyAdapter; 197 | else adapter = quadAdapter; 198 | prevHash = 'NOTAHASH'; // force hash to regenerate 199 | updateLayer(); 200 | }; 201 | 202 | // 0 : 1 char 203 | // 3 : 2 chars 204 | // 6 : 3 chars 205 | var zoomToHashChars = function( zoom ){ 206 | return 1 + Math.floor( zoom / 3 ); 207 | }; 208 | 209 | function updateLayer(){ 210 | 211 | var zoom = map.getZoom(); 212 | var hashLength = zoom+1; 213 | 214 | // update current hash 215 | currentHash = generateCurrentHash( hashLength ); 216 | 217 | if( adapter === hashAdapter ){ 218 | hashLength = zoomToHashChars( zoom ); 219 | } 220 | 221 | var hashPrefix = currentHash.substr( 0, hashLength ); 222 | 223 | // console.log( 'zoom', zoom ); 224 | // console.log( 'prevHash', prevHash ); 225 | // console.log( 'hashPrefix', hashPrefix ); 226 | 227 | // performance tweak 228 | // @todo: not that performant? 229 | if( prevHash != hashPrefix ){ 230 | // console.log( 'zoom', zoom ); 231 | layerGroup.clearLayers(); 232 | 233 | var layers = adapter.layers( currentHash, zoom ); 234 | for( var attr in layers ){ 235 | drawLayer( attr, layers[attr] ); 236 | } 237 | } 238 | 239 | prevHash = hashPrefix; 240 | } 241 | 242 | function drawRect( bounds, hash, showDigit ){ 243 | 244 | // console.log('draw'); 245 | 246 | // http://leafletjs.com/reference.html#path-options 247 | var poly = L.rectangle( bounds, rectStyle ); 248 | poly.addTo( layerGroup ); 249 | 250 | // generate labels 251 | var labels = adapter.labels( hash ); 252 | 253 | // full (long) hash marker 254 | if( labels.long.length > 1 ){ 255 | var marker = new L.marker( poly.getBounds().getNorthWest(), { opacity: 0.0001 }); 256 | marker.bindLabel( labels.long, labelConfig ); 257 | marker.addTo( layerGroup ); 258 | } 259 | 260 | // large single digit marker 261 | if( showDigit ){ 262 | var marker2 = new L.marker( poly.getBounds().getCenter(), { opacity: 0.0001 }); 263 | marker2.bindLabel( labels.short, labelConfig2 ); 264 | marker2.addTo( layerGroup ); 265 | } 266 | } 267 | 268 | function drawLayer( prefix, showDigit ){ 269 | adapter.range.forEach( function( n ){ 270 | 271 | var hash = '' + prefix + n; 272 | var bbox = adapter.bbox( hash ); 273 | 274 | var bounds = L.latLngBounds( 275 | L.latLng( bbox.maxlat, bbox.minlng ), 276 | L.latLng( bbox.minlat, bbox.maxlng ) 277 | ); 278 | 279 | // console.log( hash ); 280 | // console.log( bbox ); 281 | // console.log( bounds ); 282 | 283 | drawRect( bounds, hash, showDigit ); 284 | }); 285 | } 286 | 287 | // update on changes 288 | map.on('zoomend', updateLayer); 289 | map.on('moveend', updateLayer); 290 | 291 | // init 292 | changeHashFunction( 'quadtree' ); 293 | // updateLayer(); 294 | 295 | map.on('mousemove', function( e ){ 296 | mousePositionEvent = e; 297 | updateLayer(); 298 | }); 299 | -------------------------------------------------------------------------------- /geohash.js: -------------------------------------------------------------------------------- 1 | /** 2 | src: https://github.com/sunng87/node-geohash 3 | **/ 4 | 5 | /** 6 | * Copyright (c) 2011, Sun Ning. 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, copy, 12 | * modify, merge, publish, distribute, sublicense, and/or sell copies 13 | * of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | */ 29 | 30 | var BASE32_CODES = "0123456789bcdefghjkmnpqrstuvwxyz"; 31 | var BASE32_CODES_DICT = {}; 32 | for (var i = 0; i < BASE32_CODES.length; i++) { 33 | BASE32_CODES_DICT[BASE32_CODES.charAt(i)] = i; 34 | } 35 | 36 | var ENCODE_AUTO = 'auto'; 37 | /** 38 | * Significant Figure Hash Length 39 | * 40 | * This is a quick and dirty lookup to figure out how long our hash 41 | * should be in order to guarantee a certain amount of trailing 42 | * significant figures. This was calculated by determining the error: 43 | * 45/2^(n-1) where n is the number of bits for a latitude or 44 | * longitude. Key is # of desired sig figs, value is minimum length of 45 | * the geohash. 46 | * @type Array 47 | */ 48 | // Desired sig figs: 0 1 2 3 4 5 6 7 8 9 10 49 | var SIGFIG_HASH_LENGTH = [0, 5, 7, 8, 11, 12, 13, 15, 16, 17, 18]; 50 | /** 51 | * Encode 52 | * 53 | * Create a Geohash out of a latitude and longitude that is 54 | * `numberOfChars` long. 55 | * 56 | * @param {Number|String} latitude 57 | * @param {Number|String} longitude 58 | * @param {Number} numberOfChars 59 | * @returns {String} 60 | */ 61 | var encode = function (latitude, longitude, numberOfChars) { 62 | if (numberOfChars === ENCODE_AUTO) { 63 | if (typeof(latitude) === 'number' || typeof(longitude) === 'number') { 64 | throw new Error('string notation required for auto precision.'); 65 | } 66 | var decSigFigsLat = latitude.split('.')[1].length; 67 | var decSigFigsLong = longitude.split('.')[1].length; 68 | var numberOfSigFigs = Math.max(decSigFigsLat, decSigFigsLong); 69 | numberOfChars = SIGFIG_HASH_LENGTH[numberOfSigFigs]; 70 | } else if (numberOfChars === undefined) { 71 | numberOfChars = 9; 72 | } 73 | 74 | var chars = [], 75 | bits = 0, 76 | bitsTotal = 0, 77 | hash_value = 0, 78 | maxLat = 90, 79 | minLat = -90, 80 | maxLon = 180, 81 | minLon = -180, 82 | mid; 83 | while (chars.length < numberOfChars) { 84 | if (bitsTotal % 2 === 0) { 85 | mid = (maxLon + minLon) / 2; 86 | if (longitude > mid) { 87 | hash_value = (hash_value << 1) + 1; 88 | minLon = mid; 89 | } else { 90 | hash_value = (hash_value << 1) + 0; 91 | maxLon = mid; 92 | } 93 | } else { 94 | mid = (maxLat + minLat) / 2; 95 | if (latitude > mid) { 96 | hash_value = (hash_value << 1) + 1; 97 | minLat = mid; 98 | } else { 99 | hash_value = (hash_value << 1) + 0; 100 | maxLat = mid; 101 | } 102 | } 103 | 104 | bits++; 105 | bitsTotal++; 106 | if (bits === 5) { 107 | var code = BASE32_CODES[hash_value]; 108 | chars.push(code); 109 | bits = 0; 110 | hash_value = 0; 111 | } 112 | } 113 | return chars.join(''); 114 | }; 115 | 116 | /** 117 | * Encode Integer 118 | * 119 | * Create a Geohash out of a latitude and longitude that is of 'bitDepth'. 120 | * 121 | * @param {Number} latitude 122 | * @param {Number} longitude 123 | * @param {Number} bitDepth 124 | * @returns {Number} 125 | */ 126 | var encode_int = function (latitude, longitude, bitDepth) { 127 | 128 | bitDepth = bitDepth || 52; 129 | 130 | var bitsTotal = 0, 131 | maxLat = 90, 132 | minLat = -90, 133 | maxLon = 180, 134 | minLon = -180, 135 | mid, 136 | combinedBits = 0; 137 | 138 | while (bitsTotal < bitDepth) { 139 | combinedBits *= 2; 140 | if (bitsTotal % 2 === 0) { 141 | mid = (maxLon + minLon) / 2; 142 | if (longitude > mid) { 143 | combinedBits += 1; 144 | minLon = mid; 145 | } else { 146 | maxLon = mid; 147 | } 148 | } else { 149 | mid = (maxLat + minLat) / 2; 150 | if (latitude > mid) { 151 | combinedBits += 1; 152 | minLat = mid; 153 | } else { 154 | maxLat = mid; 155 | } 156 | } 157 | bitsTotal++; 158 | } 159 | return combinedBits; 160 | }; 161 | 162 | /** 163 | * Decode Bounding Box 164 | * 165 | * Decode hashString into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon] 166 | * @param {String} hash_string 167 | * @returns {Array} 168 | */ 169 | var decode_bbox = function (hash_string) { 170 | var isLon = true, 171 | maxLat = 90, 172 | minLat = -90, 173 | maxLon = 180, 174 | minLon = -180, 175 | mid; 176 | 177 | var hashValue = 0; 178 | for (var i = 0, l = hash_string.length; i < l; i++) { 179 | var code = hash_string[i].toLowerCase(); 180 | hashValue = BASE32_CODES_DICT[code]; 181 | 182 | for (var bits = 4; bits >= 0; bits--) { 183 | var bit = (hashValue >> bits) & 1; 184 | if (isLon) { 185 | mid = (maxLon + minLon) / 2; 186 | if (bit === 1) { 187 | minLon = mid; 188 | } else { 189 | maxLon = mid; 190 | } 191 | } else { 192 | mid = (maxLat + minLat) / 2; 193 | if (bit === 1) { 194 | minLat = mid; 195 | } else { 196 | maxLat = mid; 197 | } 198 | } 199 | isLon = !isLon; 200 | } 201 | } 202 | return [minLat, minLon, maxLat, maxLon]; 203 | }; 204 | 205 | /** 206 | * Decode Bounding Box Integer 207 | * 208 | * Decode hash number into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon] 209 | * @param {Number} hashInt 210 | * @param {Number} bitDepth 211 | * @returns {Array} 212 | */ 213 | var decode_bbox_int = function (hashInt, bitDepth) { 214 | 215 | bitDepth = bitDepth || 52; 216 | 217 | var maxLat = 90, 218 | minLat = -90, 219 | maxLon = 180, 220 | minLon = -180; 221 | 222 | var latBit = 0, lonBit = 0; 223 | var step = bitDepth / 2; 224 | 225 | for (var i = 0; i < step; i++) { 226 | 227 | lonBit = get_bit(hashInt, ((step - i) * 2) - 1); 228 | latBit = get_bit(hashInt, ((step - i) * 2) - 2); 229 | 230 | if (latBit === 0) { 231 | maxLat = (maxLat + minLat) / 2; 232 | } 233 | else { 234 | minLat = (maxLat + minLat) / 2; 235 | } 236 | 237 | if (lonBit === 0) { 238 | maxLon = (maxLon + minLon) / 2; 239 | } 240 | else { 241 | minLon = (maxLon + minLon) / 2; 242 | } 243 | } 244 | return [minLat, minLon, maxLat, maxLon]; 245 | }; 246 | 247 | function get_bit(bits, position) { 248 | return (bits / Math.pow(2, position)) & 0x01; 249 | } 250 | 251 | /** 252 | * Decode 253 | * 254 | * Decode a hash string into pair of latitude and longitude. A javascript object is returned with keys `latitude`, 255 | * `longitude` and `error`. 256 | * @param {String} hashString 257 | * @returns {Object} 258 | */ 259 | var decode = function (hashString) { 260 | var bbox = decode_bbox(hashString); 261 | var lat = (bbox[0] + bbox[2]) / 2; 262 | var lon = (bbox[1] + bbox[3]) / 2; 263 | var latErr = bbox[2] - lat; 264 | var lonErr = bbox[3] - lon; 265 | return {latitude: lat, longitude: lon, 266 | error: {latitude: latErr, longitude: lonErr}}; 267 | }; 268 | 269 | /** 270 | * Decode Integer 271 | * 272 | * Decode a hash number into pair of latitude and longitude. A javascript object is returned with keys `latitude`, 273 | * `longitude` and `error`. 274 | * @param {Number} hash_int 275 | * @param {Number} bitDepth 276 | * @returns {Object} 277 | */ 278 | var decode_int = function (hash_int, bitDepth) { 279 | var bbox = decode_bbox_int(hash_int, bitDepth); 280 | var lat = (bbox[0] + bbox[2]) / 2; 281 | var lon = (bbox[1] + bbox[3]) / 2; 282 | var latErr = bbox[2] - lat; 283 | var lonErr = bbox[3] - lon; 284 | return {latitude: lat, longitude: lon, 285 | error: {latitude: latErr, longitude: lonErr}}; 286 | }; 287 | 288 | /** 289 | * Neighbor 290 | * 291 | * Find neighbor of a geohash string in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest. 292 | * direction [lat, lon], i.e. 293 | * [1,0] - north 294 | * [1,1] - northeast 295 | * ... 296 | * @param {String} hashString 297 | * @param {Array} Direction as a 2D normalized vector. 298 | * @returns {String} 299 | */ 300 | var neighbor = function (hashString, direction) { 301 | var lonLat = decode(hashString); 302 | var neighborLat = lonLat.latitude 303 | + direction[0] * lonLat.error.latitude * 2; 304 | var neighborLon = lonLat.longitude 305 | + direction[1] * lonLat.error.longitude * 2; 306 | return encode(neighborLat, neighborLon, hashString.length); 307 | }; 308 | 309 | /** 310 | * Neighbor Integer 311 | * 312 | * Find neighbor of a geohash integer in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest. 313 | * direction [lat, lon], i.e. 314 | * [1,0] - north 315 | * [1,1] - northeast 316 | * ... 317 | * @param {String} hash_string 318 | * @returns {Array} 319 | */ 320 | var neighbor_int = function(hash_int, direction, bitDepth) { 321 | bitDepth = bitDepth || 52; 322 | var lonlat = decode_int(hash_int, bitDepth); 323 | var neighbor_lat = lonlat.latitude + direction[0] * lonlat.error.latitude * 2; 324 | var neighbor_lon = lonlat.longitude + direction[1] * lonlat.error.longitude * 2; 325 | return encode_int(neighbor_lat, neighbor_lon, bitDepth); 326 | }; 327 | 328 | /** 329 | * Neighbors 330 | * 331 | * Returns all neighbors' hashstrings clockwise from north around to northwest 332 | * 7 0 1 333 | * 6 x 2 334 | * 5 4 3 335 | * @param {String} hash_string 336 | * @returns {encoded neighborHashList|Array} 337 | */ 338 | var neighbors = function(hash_string){ 339 | 340 | var hashstringLength = hash_string.length; 341 | 342 | var lonlat = decode(hash_string); 343 | var lat = lonlat.latitude; 344 | var lon = lonlat.longitude; 345 | var latErr = lonlat.error.latitude * 2; 346 | var lonErr = lonlat.error.longitude * 2; 347 | 348 | var neighbor_lat, 349 | neighbor_lon; 350 | 351 | var neighborHashList = [ 352 | encodeNeighbor(1,0), 353 | encodeNeighbor(1,1), 354 | encodeNeighbor(0,1), 355 | encodeNeighbor(-1,1), 356 | encodeNeighbor(-1,0), 357 | encodeNeighbor(-1,-1), 358 | encodeNeighbor(0,-1), 359 | encodeNeighbor(1,-1) 360 | ]; 361 | 362 | function encodeNeighbor(neighborLatDir, neighborLonDir){ 363 | neighbor_lat = lat + neighborLatDir * latErr; 364 | neighbor_lon = lon + neighborLonDir * lonErr; 365 | return encode(neighbor_lat, neighbor_lon, hashstringLength); 366 | } 367 | 368 | return neighborHashList; 369 | }; 370 | 371 | /** 372 | * Neighbors Integer 373 | * 374 | * Returns all neighbors' hash integers clockwise from north around to northwest 375 | * 7 0 1 376 | * 6 x 2 377 | * 5 4 3 378 | * @param {Number} hash_int 379 | * @param {Number} bitDepth 380 | * @returns {encode_int'd neighborHashIntList|Array} 381 | */ 382 | var neighbors_int = function(hash_int, bitDepth){ 383 | 384 | bitDepth = bitDepth || 52; 385 | 386 | var lonlat = decode_int(hash_int, bitDepth); 387 | var lat = lonlat.latitude; 388 | var lon = lonlat.longitude; 389 | var latErr = lonlat.error.latitude * 2; 390 | var lonErr = lonlat.error.longitude * 2; 391 | 392 | var neighbor_lat, 393 | neighbor_lon; 394 | 395 | var neighborHashIntList = [ 396 | encodeNeighbor_int(1,0), 397 | encodeNeighbor_int(1,1), 398 | encodeNeighbor_int(0,1), 399 | encodeNeighbor_int(-1,1), 400 | encodeNeighbor_int(-1,0), 401 | encodeNeighbor_int(-1,-1), 402 | encodeNeighbor_int(0,-1), 403 | encodeNeighbor_int(1,-1) 404 | ]; 405 | 406 | function encodeNeighbor_int(neighborLatDir, neighborLonDir){ 407 | neighbor_lat = lat + neighborLatDir * latErr; 408 | neighbor_lon = lon + neighborLonDir * lonErr; 409 | return encode_int(neighbor_lat, neighbor_lon, bitDepth); 410 | } 411 | 412 | return neighborHashIntList; 413 | }; 414 | 415 | 416 | /** 417 | * Bounding Boxes 418 | * 419 | * Return all the hashString between minLat, minLon, maxLat, maxLon in numberOfChars 420 | * @param {Number} minLat 421 | * @param {Number} minLon 422 | * @param {Number} maxLat 423 | * @param {Number} maxLon 424 | * @param {Number} numberOfChars 425 | * @returns {bboxes.hashList|Array} 426 | */ 427 | var bboxes = function (minLat, minLon, maxLat, maxLon, numberOfChars) { 428 | numberOfChars = numberOfChars || 9; 429 | 430 | var hashSouthWest = encode(minLat, minLon, numberOfChars); 431 | var hashNorthEast = encode(maxLat, maxLon, numberOfChars); 432 | 433 | var latLon = decode(hashSouthWest); 434 | 435 | var perLat = latLon.error.latitude * 2; 436 | var perLon = latLon.error.longitude * 2; 437 | 438 | var boxSouthWest = decode_bbox(hashSouthWest); 439 | var boxNorthEast = decode_bbox(hashNorthEast); 440 | 441 | var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0]) / perLat); 442 | var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1]) / perLon); 443 | 444 | var hashList = []; 445 | 446 | for (var lat = 0; lat <= latStep; lat++) { 447 | for (var lon = 0; lon <= lonStep; lon++) { 448 | hashList.push(neighbor(hashSouthWest, [lat, lon])); 449 | } 450 | } 451 | 452 | return hashList; 453 | }; 454 | 455 | /** 456 | * Bounding Boxes Integer 457 | * 458 | * Return all the hash integers between minLat, minLon, maxLat, maxLon in bitDepth 459 | * @param {Number} minLat 460 | * @param {Number} minLon 461 | * @param {Number} maxLat 462 | * @param {Number} maxLon 463 | * @param {Number} bitDepth 464 | * @returns {bboxes_int.hashList|Array} 465 | */ 466 | var bboxes_int = function(minLat, minLon, maxLat, maxLon, bitDepth){ 467 | bitDepth = bitDepth || 52; 468 | 469 | var hashSouthWest = encode_int(minLat, minLon, bitDepth); 470 | var hashNorthEast = encode_int(maxLat, maxLon, bitDepth); 471 | 472 | var latlon = decode_int(hashSouthWest, bitDepth); 473 | 474 | var perLat = latlon.error.latitude * 2; 475 | var perLon = latlon.error.longitude * 2; 476 | 477 | var boxSouthWest = decode_bbox_int(hashSouthWest, bitDepth); 478 | var boxNorthEast = decode_bbox_int(hashNorthEast, bitDepth); 479 | 480 | var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0])/perLat); 481 | var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1])/perLon); 482 | 483 | var hashList = []; 484 | 485 | for(var lat = 0; lat <= latStep; lat++){ 486 | for(var lon = 0; lon <= lonStep; lon++){ 487 | hashList.push(neighbor_int(hashSouthWest,[lat, lon], bitDepth)); 488 | } 489 | } 490 | 491 | return hashList; 492 | }; 493 | 494 | var geohash = { 495 | 'ENCODE_AUTO': ENCODE_AUTO, 496 | 'encode': encode, 497 | 'encode_uint64': encode_int, // keeping for backwards compatibility, will deprecate 498 | 'encode_int': encode_int, 499 | 'decode': decode, 500 | 'decode_int': decode_int, 501 | 'decode_uint64': decode_int, // keeping for backwards compatibility, will deprecate 502 | 'decode_bbox': decode_bbox, 503 | 'decode_bbox_uint64': decode_bbox_int, // keeping for backwards compatibility, will deprecate 504 | 'decode_bbox_int': decode_bbox_int, 505 | 'neighbor': neighbor, 506 | 'neighbor_int': neighbor_int, 507 | 'neighbors': neighbors, 508 | 'neighbors_int': neighbors_int, 509 | 'bboxes': bboxes, 510 | 'bboxes_int': bboxes_int 511 | }; 512 | 513 | window.geohash = geohash; --------------------------------------------------------------------------------