├── README.md ├── lib ├── Control.OSMGeocoder.css ├── Bing.js ├── leaflet-hash.js └── Control.OSMGeocoder.js ├── index.html └── Bing.IODB.js /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/leaflet-bing-iodb/gh-pages/README.md -------------------------------------------------------------------------------- /lib/Control.OSMGeocoder.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-geocoder a { 2 | background-position: 50% 50%; 3 | background-repeat: no-repeat; 4 | display: block; 5 | } 6 | 7 | .leaflet-control-geocoder { 8 | box-shadow: 0 1px 7px #999; 9 | background: #f8f8f9; 10 | -moz-border-radius: 8px; 11 | -webkit-border-radius: 8px; 12 | border-radius: 8px; 13 | } 14 | 15 | .leaflet-control-geocoder a { 16 | background-image: url(images/geocoder.png); 17 | width: 36px; 18 | height: 36px; 19 | } 20 | 21 | .leaflet-touch .leaflet-control-geocoder a { 22 | width: 44px; 23 | height: 44px; 24 | } 25 | 26 | .leaflet-control-geocoder .leaflet-control-geocoder-form, 27 | .leaflet-control-geocoder-expanded .leaflet-control-geocoder-toggle { 28 | display: none; 29 | } 30 | 31 | .leaflet-control-geocoder-expanded .leaflet-control-geocoder-form { 32 | display: block; 33 | position: relative; 34 | } 35 | 36 | .leaflet-control-geocoder-expanded .leaflet-control-geocoder-form { 37 | padding: 5px; 38 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bing IODB TileLayer Test 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 31 | 32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /Bing.IODB.js: -------------------------------------------------------------------------------- 1 | // Written by Ilya Zverev, licensed WTFPL 2 | 3 | L.BingLayer = L.BingLayer.extend({ 4 | options: { 5 | offsetServer: 'http://offsets.textual.ru/', 6 | offsetUpdateDistance: 1000, 7 | offsetEnabled: true 8 | }, 9 | 10 | setOffsetEnabled: function(e) { 11 | if( e != this.options.offsetEnabled ) { 12 | this.options.offsetEnabled = e; 13 | this._updateContainerOffset(); 14 | } 15 | }, 16 | 17 | _update: function() { 18 | if (this._url == null || !this._map) return; 19 | this._update_attribution(); 20 | if( !this._offset0 ) this._offset0 = new L.Point(0, 0); 21 | this._refreshOffset(); 22 | this._updateContainerOffset(); 23 | L.TileLayer.prototype._update.apply(this, []); 24 | }, 25 | 26 | _refreshOffset: function() { 27 | if( this._map.getZoom() < 15 ) 28 | return; 29 | var currentPos = this._map.getCenter(); 30 | if( this._lastOffsetPos && this._lastOffsetPos.distanceTo(currentPos) < this.options.offsetUpdateDistance ) 31 | return; 32 | this._lastOffsetPos = currentPos; 33 | this._queryIODB(currentPos, this, function(ctx, data) { 34 | var found = null; 35 | for( var i = 0; i < data.length; i++ ) { 36 | if( data[i]['type'] == 'offset' && !data[i]['deprecated'] ) { 37 | found = data[i]; 38 | break; 39 | } 40 | } 41 | var distance = 0; 42 | if( found ) { 43 | var from = new L.LatLng(found['lat'], found['lon']); 44 | var to = new L.LatLng(found['imlat'], found['imlon']); 45 | distance = from.distanceTo(to); 46 | ctx._offset = ctx._map.project(to, 22).subtract(ctx._map.project(from, 22)); 47 | } else 48 | ctx._offset = ctx._offset0; 49 | ctx._updateContainerOffset(); 50 | ctx.fire('offset', { distance: distance, found: found }); 51 | }); 52 | }, 53 | 54 | _updateContainerOffset: function() { 55 | if( !this._offset ) return; 56 | var style = this._container.style, 57 | zoom = this._map.getZoom(), 58 | offset = zoom < 15 || !this.options.offsetEnabled ? this._offset0 : this._offset, 59 | zoomDiff = Math.pow(2, 22 - zoom); 60 | style.left = Math.round(-offset.x / zoomDiff) + 'px'; 61 | style.top = Math.round(-offset.y / zoomDiff) + 'px'; 62 | }, 63 | 64 | _queryIODB: function (latlng, context, callback) { 65 | var url = this.options.offsetServer + 'get?format=json&imagery=bing&lat=' + latlng.lat + '&lon=' + latlng.lng; 66 | var http = null; 67 | if (window.XMLHttpRequest) { 68 | http = new XMLHttpRequest(); 69 | } else if (window.ActiveXObject) { // Older IE. 70 | http = new ActiveXObject("MSXML2.XMLHTTP.3.0"); 71 | } 72 | http.onreadystatechange = function() { 73 | if( http.readyState != 4 || http.status != 200 ) return; 74 | var result = eval(http.responseText); 75 | callback(context, result); 76 | }; 77 | http.open('GET', url, true); 78 | http.send(null); 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /lib/Bing.js: -------------------------------------------------------------------------------- 1 | L.BingLayer = L.TileLayer.extend({ 2 | options: { 3 | subdomains: [0, 1, 2, 3], 4 | type: 'Aerial', 5 | attribution: 'Bing', 6 | culture: '' 7 | }, 8 | 9 | initialize: function(key, options) { 10 | L.Util.setOptions(this, options); 11 | 12 | this._key = key; 13 | this._url = null; 14 | this.meta = {}; 15 | this.loadMetadata(); 16 | }, 17 | 18 | tile2quad: function(x, y, z) { 19 | var quad = ''; 20 | for (var i = z; i > 0; i--) { 21 | var digit = 0; 22 | var mask = 1 << (i - 1); 23 | if ((x & mask) != 0) digit += 1; 24 | if ((y & mask) != 0) digit += 2; 25 | quad = quad + digit; 26 | } 27 | return quad; 28 | }, 29 | 30 | getTileUrl: function(p, z) { 31 | var z = this._getZoomForUrl(); 32 | var subdomains = this.options.subdomains, 33 | s = this.options.subdomains[Math.abs((p.x + p.y) % subdomains.length)]; 34 | return this._url.replace('{subdomain}', s) 35 | .replace('{quadkey}', this.tile2quad(p.x, p.y, z)) 36 | .replace('http:', document.location.protocol) 37 | .replace('{culture}', this.options.culture); 38 | }, 39 | 40 | loadMetadata: function() { 41 | var _this = this; 42 | var cbid = '_bing_metadata_' + L.Util.stamp(this); 43 | window[cbid] = function (meta) { 44 | _this.meta = meta; 45 | window[cbid] = undefined; 46 | var e = document.getElementById(cbid); 47 | e.parentNode.removeChild(e); 48 | if (meta.errorDetails) { 49 | alert("Got metadata" + meta.errorDetails); 50 | return; 51 | } 52 | _this.initMetadata(); 53 | }; 54 | var url = document.location.protocol + "//dev.virtualearth.net/REST/v1/Imagery/Metadata/" + this.options.type + "?include=ImageryProviders&jsonp=" + cbid + "&key=" + this._key; 55 | var script = document.createElement("script"); 56 | script.type = "text/javascript"; 57 | script.src = url; 58 | script.id = cbid; 59 | document.getElementsByTagName("head")[0].appendChild(script); 60 | }, 61 | 62 | initMetadata: function() { 63 | var r = this.meta.resourceSets[0].resources[0]; 64 | this.options.subdomains = r.imageUrlSubdomains; 65 | this._url = r.imageUrl; 66 | this._providers = []; 67 | for (var i = 0; i < r.imageryProviders.length; i++) { 68 | var p = r.imageryProviders[i]; 69 | for (var j = 0; j < p.coverageAreas.length; j++) { 70 | var c = p.coverageAreas[j]; 71 | var coverage = {zoomMin: c.zoomMin, zoomMax: c.zoomMax, active: false}; 72 | var bounds = new L.LatLngBounds( 73 | new L.LatLng(c.bbox[0]+0.01, c.bbox[1]+0.01), 74 | new L.LatLng(c.bbox[2]-0.01, c.bbox[3]-0.01) 75 | ); 76 | coverage.bounds = bounds; 77 | coverage.attrib = p.attribution; 78 | this._providers.push(coverage); 79 | } 80 | } 81 | this._update(); 82 | }, 83 | 84 | _update: function() { 85 | if (this._url == null || !this._map) return; 86 | this._update_attribution(); 87 | L.TileLayer.prototype._update.apply(this, []); 88 | }, 89 | 90 | _update_attribution: function() { 91 | var bounds = this._map.getBounds(); 92 | var zoom = this._map.getZoom(); 93 | for (var i = 0; i < this._providers.length; i++) { 94 | var p = this._providers[i]; 95 | if ((zoom <= p.zoomMax && zoom >= p.zoomMin) && 96 | bounds.intersects(p.bounds)) { 97 | if (!p.active && this._map.attributionControl) 98 | this._map.attributionControl.addAttribution(p.attrib); 99 | p.active = true; 100 | } else { 101 | if (p.active && this._map.attributionControl) 102 | this._map.attributionControl.removeAttribution(p.attrib); 103 | p.active = false; 104 | } 105 | } 106 | }, 107 | 108 | onRemove: function(map) { 109 | for (var i = 0; i < this._providers.length; i++) { 110 | var p = this._providers[i]; 111 | if (p.active && this._map.attributionControl) { 112 | this._map.attributionControl.removeAttribution(p.attrib); 113 | p.active = false; 114 | } 115 | } 116 | L.TileLayer.prototype.onRemove.apply(this, [map]); 117 | } 118 | }); 119 | 120 | L.bingLayer = function (key, options) { 121 | return new L.BingLayer(key, options); 122 | }; 123 | -------------------------------------------------------------------------------- /lib/leaflet-hash.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | var HAS_HASHCHANGE = (function() { 3 | var doc_mode = window.documentMode; 4 | return ('onhashchange' in window) && 5 | (doc_mode === undefined || doc_mode > 7); 6 | })(); 7 | 8 | L.Hash = function(map) { 9 | this.onHashChange = L.Util.bind(this.onHashChange, this); 10 | 11 | if (map) { 12 | this.init(map); 13 | } 14 | }; 15 | 16 | L.Hash.parseHash = function(hash) { 17 | if(hash.indexOf('#') === 0) { 18 | hash = hash.substr(1); 19 | } 20 | var args = hash.split("/"); 21 | if (args.length == 3) { 22 | var zoom = parseInt(args[0], 10), 23 | lat = parseFloat(args[1]), 24 | lon = parseFloat(args[2]); 25 | if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { 26 | return false; 27 | } else { 28 | return { 29 | center: new L.LatLng(lat, lon), 30 | zoom: zoom 31 | }; 32 | } 33 | } else { 34 | return false; 35 | } 36 | }; 37 | 38 | L.Hash.formatHash = function(map) { 39 | var center = map.getCenter(), 40 | zoom = map.getZoom(), 41 | precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); 42 | 43 | return "#" + [zoom, 44 | center.lat.toFixed(precision), 45 | center.lng.toFixed(precision) 46 | ].join("/"); 47 | }, 48 | 49 | L.Hash.prototype = { 50 | map: null, 51 | lastHash: null, 52 | 53 | parseHash: L.Hash.parseHash, 54 | formatHash: L.Hash.formatHash, 55 | 56 | init: function(map) { 57 | this.map = map; 58 | 59 | // reset the hash 60 | this.lastHash = null; 61 | this.onHashChange(); 62 | 63 | if (!this.isListening) { 64 | this.startListening(); 65 | } 66 | }, 67 | 68 | remove: function() { 69 | if (this.changeTimeout) { 70 | clearTimeout(this.changeTimeout); 71 | } 72 | 73 | if (this.isListening) { 74 | this.stopListening(); 75 | } 76 | 77 | this.map = null; 78 | }, 79 | 80 | onMapMove: function() { 81 | // bail if we're moving the map (updating from a hash), 82 | // or if the map is not yet loaded 83 | 84 | if (this.movingMap || !this.map._loaded) { 85 | return false; 86 | } 87 | 88 | var hash = this.formatHash(this.map); 89 | if (this.lastHash != hash) { 90 | location.replace(hash); 91 | this.lastHash = hash; 92 | } 93 | }, 94 | 95 | movingMap: false, 96 | update: function() { 97 | var hash = location.hash; 98 | if (hash === this.lastHash) { 99 | return; 100 | } 101 | var parsed = this.parseHash(hash); 102 | if (parsed) { 103 | this.movingMap = true; 104 | 105 | this.map.setView(parsed.center, parsed.zoom); 106 | 107 | this.movingMap = false; 108 | } else { 109 | this.onMapMove(this.map); 110 | } 111 | }, 112 | 113 | // defer hash change updates every 100ms 114 | changeDefer: 100, 115 | changeTimeout: null, 116 | onHashChange: function() { 117 | // throttle calls to update() so that they only happen every 118 | // `changeDefer` ms 119 | if (!this.changeTimeout) { 120 | var that = this; 121 | this.changeTimeout = setTimeout(function() { 122 | that.update(); 123 | that.changeTimeout = null; 124 | }, this.changeDefer); 125 | } 126 | }, 127 | 128 | isListening: false, 129 | hashChangeInterval: null, 130 | startListening: function() { 131 | this.map.on("moveend", this.onMapMove, this); 132 | 133 | if (HAS_HASHCHANGE) { 134 | L.DomEvent.addListener(window, "hashchange", this.onHashChange); 135 | } else { 136 | clearInterval(this.hashChangeInterval); 137 | this.hashChangeInterval = setInterval(this.onHashChange, 50); 138 | } 139 | this.isListening = true; 140 | }, 141 | 142 | stopListening: function() { 143 | this.map.off("moveend", this.onMapMove, this); 144 | 145 | if (HAS_HASHCHANGE) { 146 | L.DomEvent.removeListener(window, "hashchange", this.onHashChange); 147 | } else { 148 | clearInterval(this.hashChangeInterval); 149 | } 150 | this.isListening = false; 151 | } 152 | }; 153 | L.hash = function(map) { 154 | return new L.Hash(map); 155 | }; 156 | L.Map.prototype.addHash = function() { 157 | this._hash = L.hash(this); 158 | }; 159 | L.Map.prototype.removeHash = function() { 160 | this._hash.remove(); 161 | }; 162 | })(window); 163 | -------------------------------------------------------------------------------- /lib/Control.OSMGeocoder.js: -------------------------------------------------------------------------------- 1 | if (typeof console == "undefined") { 2 | this.console = { log: function (msg) { /* do nothing since it would otherwise break IE */} }; 3 | } 4 | 5 | 6 | L.Control.OSMGeocoder = L.Control.extend({ 7 | options: { 8 | collapsed: true, 9 | position: 'topright', 10 | text: 'Locate', 11 | bounds: null, // L.LatLngBounds 12 | email: null, // String 13 | callback: function (results) { 14 | if (results.length == 0) { 15 | console.log("ERROR: didn't find a result"); 16 | return; 17 | } 18 | var bbox = results[0].boundingbox, 19 | first = new L.LatLng(bbox[0], bbox[2]), 20 | second = new L.LatLng(bbox[1], bbox[3]), 21 | bounds = new L.LatLngBounds([first, second]); 22 | this._map.fitBounds(bounds); 23 | } 24 | }, 25 | 26 | _callbackId: 0, 27 | 28 | initialize: function (options) { 29 | L.Util.setOptions(this, options); 30 | }, 31 | 32 | onAdd: function (map) { 33 | this._map = map; 34 | 35 | var className = 'leaflet-control-geocoder', 36 | container = this._container = L.DomUtil.create('div', className); 37 | 38 | L.DomEvent.disableClickPropagation(container); 39 | 40 | var form = this._form = L.DomUtil.create('form', className + '-form'); 41 | 42 | var input = this._input = document.createElement('input'); 43 | input.type = "text"; 44 | 45 | var submit = document.createElement('input'); 46 | submit.type = "submit"; 47 | submit.value = this.options.text; 48 | 49 | form.appendChild(input); 50 | form.appendChild(submit); 51 | 52 | L.DomEvent.addListener(form, 'submit', this._geocode, this); 53 | 54 | if (this.options.collapsed) { 55 | L.DomEvent.addListener(container, 'mouseover', this._expand, this); 56 | L.DomEvent.addListener(container, 'mouseout', this._collapse, this); 57 | 58 | var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container); 59 | link.href = '#'; 60 | link.title = 'Nominatim Geocoder'; 61 | 62 | L.DomEvent.addListener(link, L.Browser.touch ? 'click' : 'focus', this._expand, this); 63 | 64 | this._map.on('movestart', this._collapse, this); 65 | } else { 66 | this._expand(); 67 | } 68 | 69 | container.appendChild(form); 70 | 71 | return container; 72 | }, 73 | 74 | /* helper functions for cordinate extraction */ 75 | _createSearchResult : function(lat, lon) { 76 | //creates an position description similar to the result of a Nominatim search 77 | var diff = 0.005; 78 | var result = []; 79 | result[0] = {}; 80 | result[0]["boundingbox"] = [parseFloat(lat)-diff,parseFloat(lat)+diff,parseFloat(lon)-diff,parseFloat(lon)+diff]; 81 | result[0]["class"]="boundary"; 82 | result[0]["display_name"]="Position: "+lat+" "+lon; 83 | result[0]["lat"] = lat; 84 | result[0]["lon"] = lon; 85 | return result; 86 | }, 87 | _isLatLon : function (q) { 88 | //"lon lat" => xx.xxx x.xxxxx 89 | var re = /(-?\d+\.\d+)\s(-?\d+\.\d+)/; 90 | var m = re.exec(q); 91 | if (m != undefined) return m; 92 | 93 | //lat...xx.xxx...lon...x.xxxxx 94 | re = /lat\D*(-?\d+\.\d+)\D*lon\D*(-?\d+\.\d+)/; 95 | m = re.exec(q); 96 | //showRegExpResult(m); 97 | if (m != undefined) return m; 98 | else return null; 99 | }, 100 | _isLatLon_decMin : function (q) { 101 | // console.log("is LatLon?: "+q); 102 | //N 53° 13.785' E 010° 23.887' 103 | //re = /[NS]\s*(\d+)\D*(\d+\.\d+).?\s*[EW]\s*(\d+)\D*(\d+\.\d+)\D*/; 104 | re = /([ns])\s*(\d+)\D*(\d+\.\d+).?\s*([ew])\s*(\d+)\D*(\d+\.\d+)/i; 105 | m = re.exec(q.toLowerCase()); 106 | //showRegExpResult(m); 107 | if ((m != undefined)) return m; 108 | else return null; 109 | // +- dec min +- dec min 110 | }, 111 | 112 | _geocode : function (event) { 113 | L.DomEvent.preventDefault(event); 114 | var q = this._input.value; 115 | //try to find corrdinates 116 | if (this._isLatLon(q) != null) 117 | { 118 | var m = this._isLatLon(q); 119 | // console.log("LatLon: "+m[1]+" "+m[2]); 120 | //m = {lon, lat} 121 | this.options.callback.call(this, this._createSearchResult(m[1],m[2])); 122 | return; 123 | } 124 | else if (this._isLatLon_decMin(q) != null) 125 | { 126 | var m = this._isLatLon_decMin(q); 127 | //m: [ns, lat dec, lat min, ew, lon dec, lon min] 128 | var temp = new Array(); 129 | temp['n'] = 1; 130 | temp['s'] = -1; 131 | temp['e'] = 1; 132 | temp['w'] = -1; 133 | this.options.callback.call(this,this._createSearchResult( 134 | temp[m[1]]*(Number(m[2]) + m[3]/60), 135 | temp[m[4]]*(Number(m[5]) + m[6]/60) 136 | )); 137 | return; 138 | } 139 | 140 | //and now Nominatim 141 | //http://wiki.openstreetmap.org/wiki/Nominatim 142 | this._callbackId = "_l_osmgeocoder_" + (this._callbackId++); 143 | window[this._callbackId] = L.Util.bind(this.options.callback, this); 144 | 145 | 146 | /* Set up params to send to Nominatim */ 147 | var params = { 148 | // Defaults 149 | q: this._input.value, 150 | json_callback : this._callbackId, 151 | format: 'json' 152 | }; 153 | 154 | if (this.options.bounds && this.options.bounds != null) { 155 | if( this.options.bounds instanceof L.LatLngBounds ) { 156 | params.viewbox = this.options.bounds.toBBoxString(); 157 | params.bounded = 1; 158 | } 159 | else { 160 | console.log('bounds must be of type L.LatLngBounds'); 161 | return; 162 | } 163 | } 164 | 165 | if (this.options.email && this.options.email != null) { 166 | if (typeof this.options.email == 'string') { 167 | params.email = this.options.email; 168 | } 169 | else{ 170 | console.log('email must be a string'); 171 | } 172 | } 173 | 174 | var url = " http://nominatim.openstreetmap.org/search" + L.Util.getParamString(params), 175 | script = document.createElement("script"); 176 | 177 | 178 | 179 | 180 | script.type = "text/javascript"; 181 | script.src = url; 182 | script.id = this._callbackId; 183 | document.getElementsByTagName("head")[0].appendChild(script); 184 | }, 185 | 186 | _expand: function () { 187 | L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded'); 188 | }, 189 | 190 | _collapse: function () { 191 | this._container.className = this._container.className.replace(' leaflet-control-geocoder-expanded', ''); 192 | } 193 | }); 194 | --------------------------------------------------------------------------------