├── 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 |
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 |
--------------------------------------------------------------------------------