├── www ├── robots.txt ├── lib │ ├── images │ │ ├── flag.png │ │ ├── calibrat.png │ │ ├── ioffset.png │ │ ├── ioffsetd.png │ │ ├── layers.png │ │ ├── calibratd.png │ │ ├── layers-2x.png │ │ ├── marker-icon.png │ │ ├── marker-shadow.png │ │ ├── marker-icon-2x.png │ │ └── marker-icon@2x.png │ ├── MarkerCluster.css │ ├── MarkerCluster.Default.ie.css │ ├── MarkerCluster.Default.css │ ├── TileLayer.Grayscale.js │ ├── Permalink.js │ ├── heatmap-leaflet.js │ ├── leaflet-heatmap.js │ ├── leaflet.css │ ├── leaflet.markercluster.js │ └── heatmap.js ├── download │ ├── .htaccess │ └── HEADER.html ├── .htaccess ├── config.php.sample ├── map.html ├── iodb-web.php └── iodb.php ├── .gitignore ├── scripts ├── prune_archive.sh └── iodb_backup.sh ├── composer.json ├── .htaccess ├── LICENSE ├── README.md └── composer.lock /www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /download/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | www/config.php 3 | vendor/ 4 | www/download/*.gz 5 | -------------------------------------------------------------------------------- /www/lib/images/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/flag.png -------------------------------------------------------------------------------- /www/lib/images/calibrat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/calibrat.png -------------------------------------------------------------------------------- /www/lib/images/ioffset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/ioffset.png -------------------------------------------------------------------------------- /www/lib/images/ioffsetd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/ioffsetd.png -------------------------------------------------------------------------------- /www/lib/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/layers.png -------------------------------------------------------------------------------- /www/lib/images/calibratd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/calibratd.png -------------------------------------------------------------------------------- /www/lib/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/layers-2x.png -------------------------------------------------------------------------------- /www/lib/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/marker-icon.png -------------------------------------------------------------------------------- /www/lib/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/marker-shadow.png -------------------------------------------------------------------------------- /www/lib/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/marker-icon-2x.png -------------------------------------------------------------------------------- /www/lib/images/marker-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/iodb/master/www/lib/images/marker-icon@2x.png -------------------------------------------------------------------------------- /www/download/.htaccess: -------------------------------------------------------------------------------- 1 | Options +Indexes 2 | IndexOrderDefault Descending Date 3 | IndexOptions +SuppressColumnSorting 4 | HeaderName HEADER.html 5 | 6 | -------------------------------------------------------------------------------- /scripts/prune_archive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | find "$(dirname "$0")/download" -name 'iodb*' ! -name '*01.*' ! -name '*-1303*' ! -name "*-$(date +%y%m)*" ! -name '*latest*' -delete 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zverik/iodb", 3 | "description": "Imagery Offset Database Backend", 4 | "type": "project", 5 | "require": { 6 | "jbelien/oauth2-openstreetmap": "^0.1.2" 7 | }, 8 | "license": "WTFPL" 9 | } 10 | -------------------------------------------------------------------------------- /www/download/HEADER.html: -------------------------------------------------------------------------------- 1 |

Imagery Offset Database Archives

2 |

Those are archives of the Imagery Offset Database. All files are distributed under a PDDL license. The latest extracts in XML and JSON formats are updated hourly.

3 | -------------------------------------------------------------------------------- /www/lib/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.2s ease-out, opacity 0.2s ease-in; 3 | -moz-transition: -moz-transform 0.2s ease-out, opacity 0.2s ease-in; 4 | -o-transition: -o-transform 0.2s ease-out, opacity 0.2s ease-in; 5 | transition: transform 0.2s ease-out, opacity 0.2s ease-in; 6 | } 7 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | DirectoryIndex iodb.php 2 | 3 | Options -Indexes -MultiViews +FollowSymLinks 4 | 5 | 6 | RewriteEngine On 7 | 8 | RewriteCond %{REQUEST_FILENAME} -f 9 | RewriteRule ^ - [L] 10 | RewriteRule ^(download|lib) - [L] 11 | RewriteRule ^iodb\.php$ - [L] 12 | RewriteRule ^map map.html [L] 13 | RewriteRule ^$ iodb.php [QSA,L] 14 | RewriteRule .* iodb.php?action=$0 [QSA,L] 15 | 16 | 17 | 18 | CharsetRecodeMultipartForms Off 19 | CharsetSourceEnc utf-8 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /www/.htaccess: -------------------------------------------------------------------------------- 1 | DirectoryIndex iodb.php 2 | 3 | Options -Indexes -MultiViews 4 | 5 | 6 | RewriteEngine On 7 | 8 | RewriteCond %{REQUEST_FILENAME} -f 9 | RewriteRule ^ - [L] 10 | RewriteRule ^(download|lib) - [L] 11 | RewriteRule ^iodb\.php$ - [L] 12 | RewriteRule ^map map.html [L] 13 | RewriteRule ^$ iodb.php [QSA,L] 14 | RewriteRule .* iodb.php?action=$0 [QSA,L,UnsafeAllow3F] 15 | 16 | 17 | 18 | CharsetRecodeMultipartForms Off 19 | CharsetSourceEnc utf-8 20 | 21 | 22 | -------------------------------------------------------------------------------- /www/lib/MarkerCluster.Default.ie.css: -------------------------------------------------------------------------------- 1 | /* IE 6-8 fallback colors */ 2 | .marker-cluster-small { 3 | background-color: rgb(181, 226, 140); 4 | } 5 | .marker-cluster-small div { 6 | background-color: rgb(110, 204, 57); 7 | } 8 | 9 | .marker-cluster-medium { 10 | background-color: rgb(241, 211, 87); 11 | } 12 | .marker-cluster-medium div { 13 | background-color: rgb(240, 194, 12); 14 | } 15 | 16 | .marker-cluster-large { 17 | background-color: rgb(253, 156, 115); 18 | } 19 | .marker-cluster-large div { 20 | background-color: rgb(241, 128, 23); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /scripts/iodb_backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | IODB=/var/www/sites/offsets.textual.ru/iodb.php 3 | PHP=/usr/bin/php 4 | GZIP=/usr/bin/gzip 5 | TARGET=/var/www/sites/offsets.textual.ru/download 6 | TMPDIR=/tmp 7 | DATE=`date +%y%m%d` 8 | 9 | $PHP $IODB xml 2> /dev/null | $GZIP > $TMPDIR/iodb-latest.xml.gz 10 | $PHP $IODB json 2> /dev/null | $GZIP > $TMPDIR/iodb-latest.json.gz 11 | 12 | if [ ! -f $TARGET/iodb-$DATE.xml.gz ]; then 13 | cp $TMPDIR/iodb-latest.xml.gz $TARGET/iodb-$DATE.xml.gz 14 | cp $TMPDIR/iodb-latest.json.gz $TARGET/iodb-$DATE.json.gz 15 | fi 16 | 17 | mv $TMPDIR/iodb-latest.xml.gz $TARGET 18 | mv $TMPDIR/iodb-latest.json.gz $TARGET 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imagery Offset Database 2 | 3 | This is a backend and web interface for imagery offset database. 4 | 5 | ## Installation 6 | 7 | Copy everything except `scripts` to a www document root. Edit `config.php`, in which 8 | you need to include OSM API key and database credentials. Then open the web interface 9 | and click "Create database". 10 | 11 | To publish regular backups of the database, use `iodb_backup` script. 12 | 13 | ## Author and License 14 | 15 | Written by Ilya Zverev, licensed WTFPL. 16 | 17 | Web interface uses [Leaflet](http://leafletjs.com) library and some plugins for it: 18 | heatmap.js, Leaflet.markercluster, Permalink and TileLayer.Grayscale. Those and other 19 | plugins can be downloaded from Leaflet's official website. 20 | -------------------------------------------------------------------------------- /www/config.php.sample: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /www/lib/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | .marker-cluster { 23 | background-clip: padding-box; 24 | border-radius: 20px; 25 | } 26 | .marker-cluster div { 27 | width: 30px; 28 | height: 30px; 29 | margin-left: 5px; 30 | margin-top: 5px; 31 | 32 | text-align: center; 33 | border-radius: 15px; 34 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 35 | } 36 | .marker-cluster span { 37 | line-height: 30px; 38 | } -------------------------------------------------------------------------------- /www/lib/TileLayer.Grayscale.js: -------------------------------------------------------------------------------- 1 | /* 2 | * L.TileLayer.Grayscale is a regular tilelayer with grayscale makeover. 3 | */ 4 | 5 | L.TileLayer.Grayscale = L.TileLayer.extend({ 6 | options: { 7 | quotaRed: 21, 8 | quotaGreen: 71, 9 | quotaBlue: 8, 10 | quotaDividerTune: 0, 11 | quotaDivider: function() { 12 | return this.quotaRed + this.quotaGreen + this.quotaBlue + this.quotaDividerTune; 13 | } 14 | }, 15 | 16 | initialize: function (url, options) { 17 | options = options || {} 18 | options.crossOrigin = true; 19 | L.TileLayer.prototype.initialize.call(this, url, options); 20 | 21 | this.on('tileload', function(e) { 22 | this._makeGrayscale(e.tile); 23 | }); 24 | }, 25 | 26 | _createTile: function () { 27 | var tile = L.TileLayer.prototype._createTile.call(this); 28 | tile.crossOrigin = "Anonymous"; 29 | return tile; 30 | }, 31 | 32 | _makeGrayscale: function (img) { 33 | if (img.getAttribute('data-grayscaled')) 34 | return; 35 | 36 | img.crossOrigin = ''; 37 | var canvas = document.createElement("canvas"); 38 | canvas.width = img.width; 39 | canvas.height = img.height; 40 | var ctx = canvas.getContext("2d"); 41 | ctx.drawImage(img, 0, 0); 42 | 43 | var imgd = ctx.getImageData(0, 0, canvas.width, canvas.height); 44 | var pix = imgd.data; 45 | for (var i = 0, n = pix.length; i < n; i += 4) { 46 | pix[i] = pix[i + 1] = pix[i + 2] = (this.options.quotaRed * pix[i] + this.options.quotaGreen * pix[i + 1] + this.options.quotaBlue * pix[i + 2]) / this.options.quotaDivider(); 47 | } 48 | ctx.putImageData(imgd, 0, 0); 49 | img.setAttribute('data-grayscaled', true); 50 | img.src = canvas.toDataURL(); 51 | } 52 | }); 53 | 54 | L.tileLayer.grayscale = function (url, options) { 55 | return new L.TileLayer.Grayscale(url, options); 56 | }; 57 | -------------------------------------------------------------------------------- /www/lib/Permalink.js: -------------------------------------------------------------------------------- 1 | L.Control.Permalink = L.Control.extend({ 2 | includes: L.Mixin.Events, 3 | 4 | options: { 5 | position: "bottomleft", 6 | useAnchor: true, 7 | useLocation: false, 8 | text: "Permalink" 9 | }, 10 | 11 | initialize: function(options) { 12 | L.Util.setOptions(this, options); 13 | this._params = {}; 14 | this._set_urlvars(); 15 | this.on("update", this._set_center, this); 16 | for (var i in this) { 17 | if (typeof(i) === "string" && i.indexOf('initialize_') == 0) 18 | this[i](); 19 | } 20 | }, 21 | 22 | onAdd: function(map) { 23 | this._container = L.DomUtil.create('div', 'leaflet-control-attribution leaflet-control-permalink'); 24 | L.DomEvent.disableClickPropagation(this._container); 25 | this._map = map; 26 | this._href = L.DomUtil.create('a', null, this._container); 27 | this._href.innerHTML = this.options.text 28 | 29 | map.on('moveend', this._update_center, this); 30 | this.fire("update", {params: this._params}) 31 | this._update_center(); 32 | 33 | if (this.options.useAnchor && 'onhashchange' in window) { 34 | var _this = this, fn = window.onhashchange; 35 | window.onhashchange = function() { 36 | _this._set_urlvars(); 37 | if (fn) return fn(); 38 | } 39 | } 40 | 41 | this.fire('add', {map: map}); 42 | 43 | return this._container; 44 | }, 45 | 46 | _update_center: function() { 47 | if (!this._map) return; 48 | 49 | var center = this._round_point(this._map.getCenter()); 50 | this._update({zoom: this._map.getZoom(), lat: center.lat, lon: center.lng}); 51 | }, 52 | 53 | _update_href: function() { 54 | var params = L.Util.getParamString(this._params); 55 | var sep = '?'; 56 | if (this.options.useAnchor) sep = '#'; 57 | var url = this._url_base + sep + params.slice(1); 58 | if (this._href) this._href.setAttribute('href', url); 59 | if (this.options.useLocation) 60 | location.replace('#' + params.slice(1)); 61 | return url; 62 | }, 63 | 64 | _round_point : function(point) { 65 | var bounds = this._map.getBounds(), size = this._map.getSize(); 66 | var ne = bounds.getNorthEast(), sw = bounds.getSouthWest(); 67 | 68 | var round = function (x, p) { 69 | if (p == 0) return x; 70 | shift = 1; 71 | while (p < 1 && p > -1) { 72 | x *= 10; 73 | p *= 10; 74 | shift *= 10; 75 | } 76 | return Math.floor(x)/shift; 77 | } 78 | point.lat = round(point.lat, (ne.lat - sw.lat) / size.y); 79 | point.lng = round(point.lng, (ne.lng - sw.lng) / size.x); 80 | return point; 81 | }, 82 | 83 | _update: function(obj, source) { 84 | //console.info("Update", obj, this._params); 85 | for(var i in obj) { 86 | if (!obj.hasOwnProperty(i)) continue; 87 | if (obj[i] != null && obj[i] != undefined) 88 | this._params[i] = obj[i] 89 | else 90 | delete this._params[i]; 91 | } 92 | 93 | this._update_href(); 94 | }, 95 | 96 | _set_urlvars: function() 97 | { 98 | this._url_base = window.location.href.split('#')[0]; 99 | 100 | var p; 101 | if (this.options.useAnchor) 102 | p = L.UrlUtil.queryParse(L.UrlUtil.hash()); 103 | else 104 | p = L.UrlUtil.queryParse(L.UrlUtil.query()); 105 | 106 | function eq(x, y) { 107 | for(var i in x) 108 | if (x.hasOwnProperty(i) && x[i] != y[i]) 109 | return false; 110 | return true; 111 | } 112 | 113 | if (eq(p, this._params) && eq(this._params, p)) 114 | return; 115 | this._params = p; 116 | this._update_href(); 117 | this.fire("update", {params: this._params}) 118 | }, 119 | 120 | _set_center: function(e) 121 | { 122 | //console.info("Update center", e); 123 | var params = e.params; 124 | if (params.zoom == undefined || 125 | params.lat == undefined || 126 | params.lon == undefined) return; 127 | this._map.setView(new L.LatLng(params.lat, params.lon), params.zoom); 128 | } 129 | }); 130 | 131 | L.UrlUtil = { 132 | queryParse: function(s) { 133 | var p = {}; 134 | var sep = "&"; 135 | if (s.search("&") != -1) 136 | sep = "&"; 137 | var params = s.split(sep); 138 | for(var i = 0; i < params.length; i++) { 139 | var tmp = params[i].split('='); 140 | if (tmp.length != 2) continue; 141 | p[tmp[0]] = tmp[1]; 142 | } 143 | return p; 144 | }, 145 | 146 | query: function() { 147 | var href = window.location.href.split('#')[0], idx = href.indexOf('?'); 148 | if (idx < 0) 149 | return ''; 150 | return href.slice(idx+1); 151 | }, 152 | 153 | hash: function() { return window.location.hash.slice(1) }, 154 | 155 | updateParamString: function (q, obj) { 156 | var p = L.UrlUtil.queryParse(q); 157 | for (var i in obj) { 158 | if (obj.hasOwnProperty(i)) 159 | p[i] = obj[i]; 160 | } 161 | return L.Util.getParamString(p).slice(1); 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /www/lib/heatmap-leaflet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * heatmap.js 0.2 Leaflet overlay 3 | * 4 | * Copyright (c) 2012, Dominik Moritz 5 | * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 6 | * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license. 7 | * 8 | * Attribution for snippets: https://gist.github.com/2566567 9 | */ 10 | 11 | L.TileLayer.HeatMap = L.TileLayer.Canvas.extend({ 12 | options: { 13 | // calculate the maximum value on a per view basis instead of global 14 | // this creates issues when moving the map 15 | maxPerView: false, 16 | debug: false 17 | }, 18 | 19 | initialize: function (options) { 20 | L.Util.setOptions(this, options); 21 | 22 | this._cache = { 23 | max: 0, 24 | bounds: {} 25 | }; 26 | 27 | this._data = []; 28 | 29 | this.drawTile = function (tile, tilePoint, zoom) { 30 | var ctx = { 31 | tile: tile, 32 | tilePoint: tilePoint, 33 | zoom: zoom, 34 | heatmap: tile.heatmap 35 | }; 36 | 37 | if (this.options.debug) { 38 | this._drawDebugInfo(ctx); 39 | } 40 | this._draw(ctx); 41 | }; 42 | }, 43 | 44 | // Add a dataset to be drawn. You might want to redraw() if you had previeous datasets. 45 | addData: function(dataset) { 46 | this._data = dataset; 47 | this._cache.max = this._calculateMaxValue(dataset); 48 | }, 49 | 50 | _createTileProto: function () { 51 | var proto = this._tileProto = L.DomUtil.create('div', 'leaflet-tile'); 52 | 53 | var tileSize = this.options.tileSize; 54 | proto.style.width = tileSize+"px"; 55 | proto.style.height = tileSize+"px"; 56 | proto.width = tileSize; 57 | proto.height = tileSize; 58 | }, 59 | 60 | _createTile: function () { 61 | var tile = this._tileProto.cloneNode(false); 62 | tile.onselectstart = tile.onmousemove = L.Util.falseFn; 63 | 64 | var options = this.options; 65 | var config = { 66 | "radius": options.radius, 67 | "element": tile, 68 | "visible": true, 69 | "opacity": options.opacity * 100, 70 | "gradient": options.gradient 71 | }; 72 | tile.heatmap = h337.create(config); 73 | 74 | return tile; 75 | }, 76 | 77 | _drawDebugInfo: function (ctx) { 78 | var canvas = L.DomUtil.create('canvas', 'leaflet-tile-debug'); 79 | var tileSize = this.options.tileSize; 80 | canvas.width = tileSize; 81 | canvas.height = tileSize; 82 | ctx['tile'].appendChild(canvas); 83 | ctx['canvas'] = canvas; 84 | 85 | var max = tileSize; 86 | var g = ctx.canvas.getContext('2d'); 87 | g.strokeStyle = '#000000'; 88 | g.fillStyle = '#FFFF00'; 89 | g.strokeRect(0, 0, max, max); 90 | g.font = "12px Arial"; 91 | g.fillRect(0, 0, 5, 5); 92 | g.fillRect(0, max - 5, 5, 5); 93 | g.fillRect(max - 5, 0, 5, 5); 94 | g.fillRect(max - 5, max - 5, 5, 5); 95 | g.fillRect(max / 2 - 5, max / 2 - 5, 10, 10); 96 | g.strokeText(ctx.tilePoint.x + ' ' + ctx.tilePoint.y + ' ' + ctx.zoom, max / 2 - 30, max / 2 - 10); 97 | 98 | this._drawPoint(ctx, [0,0]) 99 | }, 100 | 101 | _drawPoint: function (ctx, geom) { 102 | var p = this._tilePoint(ctx, geom); 103 | var c = ctx.canvas; 104 | var g = c.getContext('2d'); 105 | g.beginPath(); 106 | g.fillStyle = '#FF0000'; 107 | g.arc(p.x, p.y, 4, 0, Math.PI * 2); 108 | g.closePath(); 109 | g.fill(); 110 | g.restore(); 111 | }, 112 | 113 | _tilePoint: function (ctx, coords) { 114 | // start coords to tile 'space' 115 | var s = ctx.tilePoint.multiplyBy(this.options.tileSize); 116 | 117 | // actual coords to tile 'space' 118 | var p = this._map.project(new L.LatLng(coords[1], coords[0])); 119 | 120 | // point to draw 121 | var x = Math.round(p.x - s.x); 122 | var y = Math.round(p.y - s.y); 123 | return { 124 | x: x, 125 | y: y 126 | }; 127 | }, 128 | 129 | // checks whether the point is inside a tile 130 | _isInTile: function(localXY, padding) { 131 | padding = padding || this.options.radius; 132 | var bounds = this._cache.bounds[padding]; 133 | if (!bounds) { 134 | var tileSize = this.options.tileSize; 135 | var p1 = new L.Point(-padding, -padding); //topLeft 136 | var p2 = new L.Point(padding+tileSize, padding+tileSize); //bottomRight 137 | bounds = this._cache.bounds[padding] = new L.Bounds(p1, p2); 138 | }; 139 | return bounds.contains([localXY.x, localXY.y]); 140 | }, 141 | 142 | // get the max value of the dataset 143 | _getMaxValue: function() { 144 | if (this.options.maxPerView) { 145 | var dataset = []; 146 | var mapBounds = this._map.getBounds(); 147 | this._data.forEach(function(item){ 148 | if (mapBounds.contains([item.lat, item.lon])) { 149 | dataset.push(item); 150 | }; 151 | }); 152 | 153 | return this._calculateMaxValue(dataset) 154 | } else { 155 | return this._cache.max; 156 | } 157 | }, 158 | 159 | _calculateMaxValue: function(dataset) { 160 | array = []; 161 | dataset.forEach(function(item){ 162 | array.push(item.value || item.count); 163 | }); 164 | return Math.max.apply(Math, array); 165 | }, 166 | 167 | _draw: function(ctx) { 168 | 169 | var heatmap = ctx.heatmap 170 | heatmap.clear(); 171 | 172 | var pointsInTile = []; 173 | if (this._data.length > 0) { 174 | for (var i=0, l=this._data.length; i 0) { 186 | var len = pointOrArray.length; 187 | while(len--) { 188 | this.addData(pointOrArray[len]); 189 | } 190 | } else { 191 | var latField = this.cfg.latField || 'lat'; 192 | var lngField = this.cfg.lngField || 'lng'; 193 | var valueField = this.cfg.valueField || 'value'; 194 | var entry = pointOrArray; 195 | var latlng = new L.LatLng(entry[latField], entry[lngField]); 196 | var dataObj = { latlng: latlng }; 197 | 198 | dataObj[valueField] = entry[valueField]; 199 | this._max = Math.max(this._max, dataObj[valueField]); 200 | this._min = Math.min(this._min, dataObj[valueField]); 201 | 202 | if (entry.radius) { 203 | dataObj.radius = entry.radius; 204 | } 205 | this._data.push(dataObj); 206 | this._draw(); 207 | } 208 | }, 209 | _reset: function () { 210 | this._origin = this._map.layerPointToLatLng(new L.Point(0, 0)); 211 | 212 | var size = this._map.getSize(); 213 | if (this._width !== size.x || this._height !== size.y) { 214 | this._width = size.x; 215 | this._height = size.y; 216 | 217 | this._el.style.width = this._width + 'px'; 218 | this._el.style.height = this._height + 'px'; 219 | 220 | this._heatmap._renderer.setDimensions(this._width, this._height); 221 | } 222 | this._draw(); 223 | } 224 | }); 225 | 226 | HeatmapOverlay.CSS_TRANSFORM = (function() { 227 | var div = document.createElement('div'); 228 | var props = [ 229 | 'transform', 230 | 'WebkitTransform', 231 | 'MozTransform', 232 | 'OTransform', 233 | 'msTransform' 234 | ]; 235 | 236 | for (var i = 0; i < props.length; i++) { 237 | var prop = props[i]; 238 | if (div.style[prop] !== undefined) { 239 | return prop; 240 | } 241 | } 242 | return props[0]; 243 | })(); 244 | 245 | return HeatmapOverlay; 246 | }); -------------------------------------------------------------------------------- /www/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Imagery Offsets Map 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 67 | 206 | 207 | 208 |
209 |
210 | Imagery Offset Database Visualization
211 | The data is updated hourly. Return to the list. 212 |
213 |
214 |
Markers
215 |
Heatmap
216 |
Area
217 |
218 |
219 |
All
220 |
Geometries
221 |
Bing
222 |
Other Offsets
223 |
224 | 225 |
226 |
Show Deprecated Offsets
227 |
228 | 229 | 230 | -------------------------------------------------------------------------------- /www/lib/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-container, 8 | .leaflet-pane > svg, 9 | .leaflet-pane > canvas, 10 | .leaflet-zoom-box, 11 | .leaflet-image-layer, 12 | .leaflet-layer { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .leaflet-container { 18 | overflow: hidden; 19 | } 20 | .leaflet-tile, 21 | .leaflet-marker-icon, 22 | .leaflet-marker-shadow { 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | /* Prevents IE11 from highlighting tiles in blue */ 29 | .leaflet-tile::selection { 30 | background: transparent; 31 | } 32 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ 33 | .leaflet-safari .leaflet-tile { 34 | image-rendering: -webkit-optimize-contrast; 35 | } 36 | /* hack that prevents hw layers "stretching" when loading new tiles */ 37 | .leaflet-safari .leaflet-tile-container { 38 | width: 1600px; 39 | height: 1600px; 40 | -webkit-transform-origin: 0 0; 41 | } 42 | .leaflet-marker-icon, 43 | .leaflet-marker-shadow { 44 | display: block; 45 | } 46 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ 47 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ 48 | .leaflet-container .leaflet-overlay-pane svg { 49 | max-width: none !important; 50 | max-height: none !important; 51 | } 52 | .leaflet-container .leaflet-marker-pane img, 53 | .leaflet-container .leaflet-shadow-pane img, 54 | .leaflet-container .leaflet-tile-pane img, 55 | .leaflet-container img.leaflet-image-layer, 56 | .leaflet-container .leaflet-tile { 57 | max-width: none !important; 58 | max-height: none !important; 59 | width: auto; 60 | padding: 0; 61 | } 62 | 63 | .leaflet-container img.leaflet-tile { 64 | /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ 65 | mix-blend-mode: plus-lighter; 66 | } 67 | 68 | .leaflet-container.leaflet-touch-zoom { 69 | -ms-touch-action: pan-x pan-y; 70 | touch-action: pan-x pan-y; 71 | } 72 | .leaflet-container.leaflet-touch-drag { 73 | -ms-touch-action: pinch-zoom; 74 | /* Fallback for FF which doesn't support pinch-zoom */ 75 | touch-action: none; 76 | touch-action: pinch-zoom; 77 | } 78 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { 79 | -ms-touch-action: none; 80 | touch-action: none; 81 | } 82 | .leaflet-container { 83 | -webkit-tap-highlight-color: transparent; 84 | } 85 | .leaflet-container a { 86 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); 87 | } 88 | .leaflet-tile { 89 | filter: inherit; 90 | visibility: hidden; 91 | } 92 | .leaflet-tile-loaded { 93 | visibility: inherit; 94 | } 95 | .leaflet-zoom-box { 96 | width: 0; 97 | height: 0; 98 | -moz-box-sizing: border-box; 99 | box-sizing: border-box; 100 | z-index: 800; 101 | } 102 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 103 | .leaflet-overlay-pane svg { 104 | -moz-user-select: none; 105 | } 106 | 107 | .leaflet-pane { z-index: 400; } 108 | 109 | .leaflet-tile-pane { z-index: 200; } 110 | .leaflet-overlay-pane { z-index: 400; } 111 | .leaflet-shadow-pane { z-index: 500; } 112 | .leaflet-marker-pane { z-index: 600; } 113 | .leaflet-tooltip-pane { z-index: 650; } 114 | .leaflet-popup-pane { z-index: 700; } 115 | 116 | .leaflet-map-pane canvas { z-index: 100; } 117 | .leaflet-map-pane svg { z-index: 200; } 118 | 119 | .leaflet-vml-shape { 120 | width: 1px; 121 | height: 1px; 122 | } 123 | .lvml { 124 | behavior: url(#default#VML); 125 | display: inline-block; 126 | position: absolute; 127 | } 128 | 129 | 130 | /* control positioning */ 131 | 132 | .leaflet-control { 133 | position: relative; 134 | z-index: 800; 135 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 136 | pointer-events: auto; 137 | } 138 | .leaflet-top, 139 | .leaflet-bottom { 140 | position: absolute; 141 | z-index: 1000; 142 | pointer-events: none; 143 | } 144 | .leaflet-top { 145 | top: 0; 146 | } 147 | .leaflet-right { 148 | right: 0; 149 | } 150 | .leaflet-bottom { 151 | bottom: 0; 152 | } 153 | .leaflet-left { 154 | left: 0; 155 | } 156 | .leaflet-control { 157 | float: left; 158 | clear: both; 159 | } 160 | .leaflet-right .leaflet-control { 161 | float: right; 162 | } 163 | .leaflet-top .leaflet-control { 164 | margin-top: 10px; 165 | } 166 | .leaflet-bottom .leaflet-control { 167 | margin-bottom: 10px; 168 | } 169 | .leaflet-left .leaflet-control { 170 | margin-left: 10px; 171 | } 172 | .leaflet-right .leaflet-control { 173 | margin-right: 10px; 174 | } 175 | 176 | 177 | /* zoom and fade animations */ 178 | 179 | .leaflet-fade-anim .leaflet-popup { 180 | opacity: 0; 181 | -webkit-transition: opacity 0.2s linear; 182 | -moz-transition: opacity 0.2s linear; 183 | transition: opacity 0.2s linear; 184 | } 185 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 186 | opacity: 1; 187 | } 188 | .leaflet-zoom-animated { 189 | -webkit-transform-origin: 0 0; 190 | -ms-transform-origin: 0 0; 191 | transform-origin: 0 0; 192 | } 193 | svg.leaflet-zoom-animated { 194 | will-change: transform; 195 | } 196 | 197 | .leaflet-zoom-anim .leaflet-zoom-animated { 198 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 199 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 200 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 201 | } 202 | .leaflet-zoom-anim .leaflet-tile, 203 | .leaflet-pan-anim .leaflet-tile { 204 | -webkit-transition: none; 205 | -moz-transition: none; 206 | transition: none; 207 | } 208 | 209 | .leaflet-zoom-anim .leaflet-zoom-hide { 210 | visibility: hidden; 211 | } 212 | 213 | 214 | /* cursors */ 215 | 216 | .leaflet-interactive { 217 | cursor: pointer; 218 | } 219 | .leaflet-grab { 220 | cursor: -webkit-grab; 221 | cursor: -moz-grab; 222 | cursor: grab; 223 | } 224 | .leaflet-crosshair, 225 | .leaflet-crosshair .leaflet-interactive { 226 | cursor: crosshair; 227 | } 228 | .leaflet-popup-pane, 229 | .leaflet-control { 230 | cursor: auto; 231 | } 232 | .leaflet-dragging .leaflet-grab, 233 | .leaflet-dragging .leaflet-grab .leaflet-interactive, 234 | .leaflet-dragging .leaflet-marker-draggable { 235 | cursor: move; 236 | cursor: -webkit-grabbing; 237 | cursor: -moz-grabbing; 238 | cursor: grabbing; 239 | } 240 | 241 | /* marker & overlays interactivity */ 242 | .leaflet-marker-icon, 243 | .leaflet-marker-shadow, 244 | .leaflet-image-layer, 245 | .leaflet-pane > svg path, 246 | .leaflet-tile-container { 247 | pointer-events: none; 248 | } 249 | 250 | .leaflet-marker-icon.leaflet-interactive, 251 | .leaflet-image-layer.leaflet-interactive, 252 | .leaflet-pane > svg path.leaflet-interactive, 253 | svg.leaflet-image-layer.leaflet-interactive path { 254 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 255 | pointer-events: auto; 256 | } 257 | 258 | /* visual tweaks */ 259 | 260 | .leaflet-container { 261 | background: #ddd; 262 | outline-offset: 1px; 263 | } 264 | .leaflet-container a { 265 | color: #0078A8; 266 | } 267 | .leaflet-zoom-box { 268 | border: 2px dotted #38f; 269 | background: rgba(255,255,255,0.5); 270 | } 271 | 272 | 273 | /* general typography */ 274 | .leaflet-container { 275 | font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; 276 | font-size: 12px; 277 | font-size: 0.75rem; 278 | line-height: 1.5; 279 | } 280 | 281 | 282 | /* general toolbar styles */ 283 | 284 | .leaflet-bar { 285 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 286 | border-radius: 4px; 287 | } 288 | .leaflet-bar a { 289 | background-color: #fff; 290 | border-bottom: 1px solid #ccc; 291 | width: 26px; 292 | height: 26px; 293 | line-height: 26px; 294 | display: block; 295 | text-align: center; 296 | text-decoration: none; 297 | color: black; 298 | } 299 | .leaflet-bar a, 300 | .leaflet-control-layers-toggle { 301 | background-position: 50% 50%; 302 | background-repeat: no-repeat; 303 | display: block; 304 | } 305 | .leaflet-bar a:hover, 306 | .leaflet-bar a:focus { 307 | background-color: #f4f4f4; 308 | } 309 | .leaflet-bar a:first-child { 310 | border-top-left-radius: 4px; 311 | border-top-right-radius: 4px; 312 | } 313 | .leaflet-bar a:last-child { 314 | border-bottom-left-radius: 4px; 315 | border-bottom-right-radius: 4px; 316 | border-bottom: none; 317 | } 318 | .leaflet-bar a.leaflet-disabled { 319 | cursor: default; 320 | background-color: #f4f4f4; 321 | color: #bbb; 322 | } 323 | 324 | .leaflet-touch .leaflet-bar a { 325 | width: 30px; 326 | height: 30px; 327 | line-height: 30px; 328 | } 329 | .leaflet-touch .leaflet-bar a:first-child { 330 | border-top-left-radius: 2px; 331 | border-top-right-radius: 2px; 332 | } 333 | .leaflet-touch .leaflet-bar a:last-child { 334 | border-bottom-left-radius: 2px; 335 | border-bottom-right-radius: 2px; 336 | } 337 | 338 | /* zoom control */ 339 | 340 | .leaflet-control-zoom-in, 341 | .leaflet-control-zoom-out { 342 | font: bold 18px 'Lucida Console', Monaco, monospace; 343 | text-indent: 1px; 344 | } 345 | 346 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { 347 | font-size: 22px; 348 | } 349 | 350 | 351 | /* layers control */ 352 | 353 | .leaflet-control-layers { 354 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 355 | background: #fff; 356 | border-radius: 5px; 357 | } 358 | .leaflet-control-layers-toggle { 359 | background-image: url(images/layers.png); 360 | width: 36px; 361 | height: 36px; 362 | } 363 | .leaflet-retina .leaflet-control-layers-toggle { 364 | background-image: url(images/layers-2x.png); 365 | background-size: 26px 26px; 366 | } 367 | .leaflet-touch .leaflet-control-layers-toggle { 368 | width: 44px; 369 | height: 44px; 370 | } 371 | .leaflet-control-layers .leaflet-control-layers-list, 372 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 373 | display: none; 374 | } 375 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 376 | display: block; 377 | position: relative; 378 | } 379 | .leaflet-control-layers-expanded { 380 | padding: 6px 10px 6px 6px; 381 | color: #333; 382 | background: #fff; 383 | } 384 | .leaflet-control-layers-scrollbar { 385 | overflow-y: scroll; 386 | overflow-x: hidden; 387 | padding-right: 5px; 388 | } 389 | .leaflet-control-layers-selector { 390 | margin-top: 2px; 391 | position: relative; 392 | top: 1px; 393 | } 394 | .leaflet-control-layers label { 395 | display: block; 396 | font-size: 13px; 397 | font-size: 1.08333em; 398 | } 399 | .leaflet-control-layers-separator { 400 | height: 0; 401 | border-top: 1px solid #ddd; 402 | margin: 5px -10px 5px -6px; 403 | } 404 | 405 | /* Default icon URLs */ 406 | .leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ 407 | background-image: url(images/marker-icon.png); 408 | } 409 | 410 | 411 | /* attribution and scale controls */ 412 | 413 | .leaflet-container .leaflet-control-attribution { 414 | background: #fff; 415 | background: rgba(255, 255, 255, 0.8); 416 | margin: 0; 417 | } 418 | .leaflet-control-attribution, 419 | .leaflet-control-scale-line { 420 | padding: 0 5px; 421 | color: #333; 422 | line-height: 1.4; 423 | } 424 | .leaflet-control-attribution a { 425 | text-decoration: none; 426 | } 427 | .leaflet-control-attribution a:hover, 428 | .leaflet-control-attribution a:focus { 429 | text-decoration: underline; 430 | } 431 | .leaflet-attribution-flag { 432 | display: inline !important; 433 | vertical-align: baseline !important; 434 | width: 1em; 435 | height: 0.6669em; 436 | } 437 | .leaflet-left .leaflet-control-scale { 438 | margin-left: 5px; 439 | } 440 | .leaflet-bottom .leaflet-control-scale { 441 | margin-bottom: 5px; 442 | } 443 | .leaflet-control-scale-line { 444 | border: 2px solid #777; 445 | border-top: none; 446 | line-height: 1.1; 447 | padding: 2px 5px 1px; 448 | white-space: nowrap; 449 | -moz-box-sizing: border-box; 450 | box-sizing: border-box; 451 | background: rgba(255, 255, 255, 0.8); 452 | text-shadow: 1px 1px #fff; 453 | } 454 | .leaflet-control-scale-line:not(:first-child) { 455 | border-top: 2px solid #777; 456 | border-bottom: none; 457 | margin-top: -2px; 458 | } 459 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 460 | border-bottom: 2px solid #777; 461 | } 462 | 463 | .leaflet-touch .leaflet-control-attribution, 464 | .leaflet-touch .leaflet-control-layers, 465 | .leaflet-touch .leaflet-bar { 466 | box-shadow: none; 467 | } 468 | .leaflet-touch .leaflet-control-layers, 469 | .leaflet-touch .leaflet-bar { 470 | border: 2px solid rgba(0,0,0,0.2); 471 | background-clip: padding-box; 472 | } 473 | 474 | 475 | /* popup */ 476 | 477 | .leaflet-popup { 478 | position: absolute; 479 | text-align: center; 480 | margin-bottom: 20px; 481 | } 482 | .leaflet-popup-content-wrapper { 483 | padding: 1px; 484 | text-align: left; 485 | border-radius: 12px; 486 | } 487 | .leaflet-popup-content { 488 | margin: 13px 24px 13px 20px; 489 | line-height: 1.3; 490 | font-size: 13px; 491 | font-size: 1.08333em; 492 | min-height: 1px; 493 | } 494 | .leaflet-popup-content p { 495 | margin: 17px 0; 496 | margin: 1.3em 0; 497 | } 498 | .leaflet-popup-tip-container { 499 | width: 40px; 500 | height: 20px; 501 | position: absolute; 502 | left: 50%; 503 | margin-top: -1px; 504 | margin-left: -20px; 505 | overflow: hidden; 506 | pointer-events: none; 507 | } 508 | .leaflet-popup-tip { 509 | width: 17px; 510 | height: 17px; 511 | padding: 1px; 512 | 513 | margin: -10px auto 0; 514 | pointer-events: auto; 515 | 516 | -webkit-transform: rotate(45deg); 517 | -moz-transform: rotate(45deg); 518 | -ms-transform: rotate(45deg); 519 | transform: rotate(45deg); 520 | } 521 | .leaflet-popup-content-wrapper, 522 | .leaflet-popup-tip { 523 | background: white; 524 | color: #333; 525 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 526 | } 527 | .leaflet-container a.leaflet-popup-close-button { 528 | position: absolute; 529 | top: 0; 530 | right: 0; 531 | border: none; 532 | text-align: center; 533 | width: 24px; 534 | height: 24px; 535 | font: 16px/24px Tahoma, Verdana, sans-serif; 536 | color: #757575; 537 | text-decoration: none; 538 | background: transparent; 539 | } 540 | .leaflet-container a.leaflet-popup-close-button:hover, 541 | .leaflet-container a.leaflet-popup-close-button:focus { 542 | color: #585858; 543 | } 544 | .leaflet-popup-scrolled { 545 | overflow: auto; 546 | } 547 | 548 | .leaflet-oldie .leaflet-popup-content-wrapper { 549 | -ms-zoom: 1; 550 | } 551 | .leaflet-oldie .leaflet-popup-tip { 552 | width: 24px; 553 | margin: 0 auto; 554 | 555 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 556 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 557 | } 558 | 559 | .leaflet-oldie .leaflet-control-zoom, 560 | .leaflet-oldie .leaflet-control-layers, 561 | .leaflet-oldie .leaflet-popup-content-wrapper, 562 | .leaflet-oldie .leaflet-popup-tip { 563 | border: 1px solid #999; 564 | } 565 | 566 | 567 | /* div icon */ 568 | 569 | .leaflet-div-icon { 570 | background: #fff; 571 | border: 1px solid #666; 572 | } 573 | 574 | 575 | /* Tooltip */ 576 | /* Base styles for the element that has a tooltip */ 577 | .leaflet-tooltip { 578 | position: absolute; 579 | padding: 6px; 580 | background-color: #fff; 581 | border: 1px solid #fff; 582 | border-radius: 3px; 583 | color: #222; 584 | white-space: nowrap; 585 | -webkit-user-select: none; 586 | -moz-user-select: none; 587 | -ms-user-select: none; 588 | user-select: none; 589 | pointer-events: none; 590 | box-shadow: 0 1px 3px rgba(0,0,0,0.4); 591 | } 592 | .leaflet-tooltip.leaflet-interactive { 593 | cursor: pointer; 594 | pointer-events: auto; 595 | } 596 | .leaflet-tooltip-top:before, 597 | .leaflet-tooltip-bottom:before, 598 | .leaflet-tooltip-left:before, 599 | .leaflet-tooltip-right:before { 600 | position: absolute; 601 | pointer-events: none; 602 | border: 6px solid transparent; 603 | background: transparent; 604 | content: ""; 605 | } 606 | 607 | /* Directions */ 608 | 609 | .leaflet-tooltip-bottom { 610 | margin-top: 6px; 611 | } 612 | .leaflet-tooltip-top { 613 | margin-top: -6px; 614 | } 615 | .leaflet-tooltip-bottom:before, 616 | .leaflet-tooltip-top:before { 617 | left: 50%; 618 | margin-left: -6px; 619 | } 620 | .leaflet-tooltip-top:before { 621 | bottom: 0; 622 | margin-bottom: -12px; 623 | border-top-color: #fff; 624 | } 625 | .leaflet-tooltip-bottom:before { 626 | top: 0; 627 | margin-top: -12px; 628 | margin-left: -6px; 629 | border-bottom-color: #fff; 630 | } 631 | .leaflet-tooltip-left { 632 | margin-left: -6px; 633 | } 634 | .leaflet-tooltip-right { 635 | margin-left: 6px; 636 | } 637 | .leaflet-tooltip-left:before, 638 | .leaflet-tooltip-right:before { 639 | top: 50%; 640 | margin-top: -6px; 641 | } 642 | .leaflet-tooltip-left:before { 643 | right: 0; 644 | margin-right: -12px; 645 | border-left-color: #fff; 646 | } 647 | .leaflet-tooltip-right:before { 648 | left: 0; 649 | margin-left: -12px; 650 | border-right-color: #fff; 651 | } 652 | 653 | /* Printing */ 654 | 655 | @media print { 656 | /* Prevent printers from removing background-images of controls. */ 657 | .leaflet-control { 658 | -webkit-print-color-adjust: exact; 659 | print-color-adjust: exact; 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /www/iodb-web.php: -------------------------------------------------------------------------------- 1 | CLIENT_ID, 9 | 'clientSecret' => CLIENT_SECRET, 10 | 'redirectUri' => $redirect.'oauth', 11 | 'dev' => false 12 | ]); 13 | } 14 | 15 | // Set session parameters. 16 | $session_lifetime = 365 * 24 * 3600; // a year 17 | session_set_cookie_params($session_lifetime); 18 | session_start([ 19 | 'cookie_lifetime' => $session_lifetime, 20 | 'use_only_cookies' => true, 21 | 'use_strict_mode' => true, 22 | ]); 23 | 24 | header('Content-type: text/html; charset=utf-8'); 25 | $html = true; 26 | $user = isset($_SESSION['osm_user']) ? $_SESSION['osm_user'] : DEFAULT_USER; 27 | $is_admin = in_array($user, $administrators); 28 | 29 | $action = req('action', ''); 30 | if( $action == 'login' ) { 31 | $oauth = oauth_make(); 32 | $options = ['scope' => 'read_prefs']; 33 | $auth_url = $oauth->getAuthorizationUrl($options); 34 | $_SESSION['oauth2state'] = $oauth->getState(); 35 | header('Location: '.$auth_url); 36 | exit; 37 | } elseif( $action == 'oauth' ) { 38 | if(empty($_GET['code'])) { 39 | echo "Error: there is no OAuth code."; 40 | } elseif(empty($_SESSION['oauth2state'])) { 41 | echo "Error: there is no OAuth state."; 42 | } elseif(empty($_GET['state']) || $_GET['state'] != $_SESSION['oauth2state']) { 43 | echo "Error: invalid state."; 44 | } else { 45 | unset($_SESSION['oauth2state']); 46 | try { 47 | $oauth = oauth_make(); 48 | $accessToken = $oauth->getAccessToken( 49 | 'authorization_code', ['code' => $_GET['code']] 50 | ); 51 | $resourceOwner = $oauth->getResourceOwner($accessToken); 52 | $_SESSION['osm_user'] = $resourceOwner->getDisplayName(); 53 | 54 | // Переход на станицу успеха 55 | header("Location: $redirect"); 56 | } catch (Exception $e) { 57 | echo("
Exception:\n");
 58 | 			print_r($e);
 59 | 			echo '
'; 60 | } 61 | } 62 | exit; 63 | } elseif( $action == 'logout' ) { 64 | unset($_SESSION['osm_user']); 65 | header("Location: $redirect"); 66 | } elseif( $action == 'createdb' && $is_admin ) { 67 | create_table(); 68 | header("Location: $redirect"); 69 | } elseif( $action == 'web' && $user ) { 70 | $admact = req('webact'); 71 | if( $admact == 'report' ) { 72 | report_impl(req('offsetid'), req('message')); 73 | } elseif( $admact == 'unflag' && $is_admin ) { 74 | report_impl(req('offsetid', $_REQUEST['offsetids'])); 75 | } elseif( $admact == 'deprecate' ) { 76 | deprecate_impl(req('offsetid'), $user, req('message')); 77 | } elseif( $admact == 'undeprecate' && $is_admin ) { 78 | undeprecate(req('offsetid', $_REQUEST['offsetids'])); 79 | } elseif( $admact == 'delete' && $is_admin ) { 80 | error('Deleting is disabled for now. Please contact an admin if you really need to delete something.'); 81 | delete_impl(req('offsetid', $_REQUEST['offsetids'])); 82 | } else 83 | error('Unknown web action: '.$admact); 84 | header("Location: $redirect"); 85 | } elseif( strlen($action) > 0 ) { 86 | header("Location: $redirect"); 87 | } 88 | 89 | ?> 90 | 91 | Imagery Offset Database 92 | 93 | 94 | 95 | 208 | 328 | 329 | query('select 1 from iodb'); 331 | if( $res ) { 332 | $has_table = true; 333 | $res->free(); 334 | } else 335 | $has_table = false; 336 | 337 | $before = req('before', ''); 338 | if( !preg_match('/^\d{4}-\d\d-\d\d$/', $before) ) $before = ''; 339 | $select = '*, X(coord) as lon, Y(coord) as lat, X(im_coord) as imlon, Y(im_coord) as imlat, GeometryType(calibration) as geotype'; 340 | $where = ''; 341 | if( isset($_REQUEST['filter']) ) { 342 | $filter = req('filter'); 343 | if( $is_admin && $filter == 'flagged' ) { 344 | $where = 'flagged is not null'; 345 | } elseif (isset($_REQUEST['fid']) ) { 346 | $fid = req('fid'); 347 | validate_num($fid, 'filter id', false); 348 | $result = $db->query("select $select, HEX(ip) as h_ip, HEX(abandon_ip) as h_aip from iodb where offset_id = $fid"); 349 | if( $result ) { 350 | $offset = $result->fetch_assoc(); 351 | $result->free(); 352 | if( $filter == 'author' || $filter == 'author0' ) { 353 | $author = htmlspecialchars(!isset($offset['abandon_date']) || $filter == 'author0' ? $offset['author'] : $offset['abandon_author']); 354 | $where = "(author = '$author' or abandon_author = '$author')"; 355 | } elseif( ($filter == 'ip' || $filter == 'ip0') && $is_admin ) { 356 | $ip = !isset($offset['abandon_date']) || $filter == 'ip0' ? $offset['h_ip'] : $offset['h_aip']; 357 | $where = "(ip = 0x$ip or abandon_ip = 0x$ip)"; 358 | } elseif( $filter == 'area' ) { 359 | $where = region_where_clause($offset['lat'], $offset['lon'], MAX_RADIUS); 360 | } 361 | } 362 | } 363 | if( strlen($where) > 0 ) 364 | $where = 'and '.$where; 365 | } 366 | ?> 367 | 368 |

0 || strlen($where) > 0 ) { ?>Imagery Offset DatabaseImagery Offset Database

369 | 373 | 382 | 383 |

There seems to be no iodb table in the database. Create it.

384 | 385 | 386 |

This website is in development. Sorry for any missing functionality.

387 | 388 |
389 |
390 | 391 |
392 | 393 | 394 | 395 | 396 | 397 |

Select all, invert. Show flagged. 398 | Things to do with selected entires: 399 | 400 | 401 | 402 |

403 | 404 | query("select $select from iodb where abandon_date is null and offset_date = '$timestr' $where order by offset_id desc limit 500"); 414 | if( !$result ) 415 | error('Error requesting data: '.$db->error); 416 | while( $offset = $result->fetch_assoc() ) { 417 | print_offset($offset); 418 | $cnt++; 419 | } 420 | $result->free(); 421 | 422 | $result = $db->query("select $select from iodb where abandon_date = '$timestr' $where order by offset_id desc"); 423 | if( !$result ) 424 | error('Error requesting data: '.$db->error); 425 | while( $offset = $result->fetch_assoc() ) { 426 | print_offset($offset); 427 | $cnt++; 428 | } 429 | $result->free(); 430 | $time -= 24*3600; 431 | $days++; 432 | } 433 | ?> 434 |
435 | 440 | 441 | 442 | 443 | '.date('jS \o\f F', $time)."\n"; 448 | $time_shown = true; 449 | } 450 | $id = $offset['offset_id']; 451 | $deprecated = isset($offset['abandon_date']); 452 | $class = (isset($offset['imagery']) ? 'offset':'calibration').($deprecated ? ' deprecated':''); 453 | $description = htmlspecialchars($offset[$deprecated ? 'abandon_reason' : 'description']); 454 | $author = htmlspecialchars($offset[$deprecated ? 'abandon_author' : 'author']); 455 | if( !isset($offset['location']) ) { 456 | $country = ''; 457 | } else { 458 | $loc_parts = explode(',', $offset['location']); 459 | $clp = count($loc_parts) - 1; 460 | $country = trim($loc_parts[$clp > 0 && trim($loc_parts[$clp]) == 'European Union' ? $clp - 1 : $clp]); 461 | if( $country ) $country = ' in '.$country; 462 | } 463 | ?> 464 |
465 | 466 | '.$offset['author'].' '; } ?>:   467 |
468 | 0 ? "&before=$before" : ''; 472 | $filters[] = array('author', 'author'); 473 | if( $is_admin ) 474 | $filters[] = array('IP', 'ip'); 475 | if( isset($offset['abandon_date']) ) 476 | $filters[] = array('orig. author', 'author0'); 477 | if( $is_admin && isset($offset['abandon_date']) ) 478 | $filters[] = array('orig. IP', 'ip0'); 479 | $filters[] = array('area', 'area'); 480 | 481 | for( $i = 0; $i < count($filters); $i++ ) 482 | $filters[$i] = ''.$filters[$i][0].''; 483 | 484 | $actions = array(); 485 | if( !isset($offset['flagged']) ) 486 | $actions[] = array('Report', "submitAction($id, 'report', 'Why are you reporting this offset to moderators?', true)"); 487 | if( !isset($offset['abandon_date']) ) 488 | $actions[] = array('Deprecate', "submitAction($id, 'deprecate', 'Please do not forget to upload fresh offset!\\nWhy are you deprecating this one?', true)"); 489 | if( $is_admin ) { 490 | if( isset($offset['flagged']) ) 491 | $actions[] = array('Unflag', "submitAction($id, 'unflag', 'Unflag this offset?', false)"); 492 | if( isset($offset['abandon_date']) ) 493 | $actions[] = array('Undeprecate', "submitAction($id, 'undeprecate', 'You are going to clear deprecation flag on the offset. Proceed?', false)"); 494 | $actions[] = array('Delete', "submitAction($id, 'delete', 'You are going to IRREVERSIBLY delete this offset. Proceed?', false)"); 495 | } 496 | 497 | for( $i = 0; $i < count($actions); $i++ ) 498 | $actions[$i] = ''.$actions[$i][0].''; 499 | $actions[] = 'Show on map'; 500 | $actions[] = 'Open area in JOSM'; 501 | ?> 502 |
Filter by
503 |
504 |

505 |
526 | Imagery: 0 || $offset['max_zoom'] < 29 ) { ?> [z-z]
527 | Created by on
528 | 529 |

530 | Deprecated by on
531 | 532 | 533 |

534 | This entry has been reported. 535 |
536 | 537 |
538 |
539 |
540 |
541 | 544 | -------------------------------------------------------------------------------- /www/iodb.php: -------------------------------------------------------------------------------- 1 | connect_errno ) 8 | die('Cannot connect to database: ('.$db->connect_errno.') '.$db->connect_error); 9 | $db->set_charset('utf8'); 10 | 11 | if( PHP_SAPI == 'cli' ) { 12 | db_dump(); 13 | exit; 14 | } 15 | 16 | if( !isset($_REQUEST['action']) ) { 17 | require('iodb-web.php'); 18 | exit; 19 | } 20 | 21 | $action = $_REQUEST['action']; 22 | 23 | if( $action == 'get' ) { 24 | do_get(); 25 | } elseif( $action == 'store' ) { 26 | post(); 27 | } elseif( $action == 'deprecate' ) { 28 | deprecate(); 29 | } elseif( $action == 'report' ) { 30 | report(); 31 | } elseif( $action == 'all' ) { 32 | db_dump_passthru(); 33 | exit; 34 | } else { 35 | // the rest of actions could be related to web 36 | require('iodb-web.php'); 37 | } 38 | 39 | // ----------------------------------------------------------------------------- 40 | 41 | // Check query parameter and return either it or the default value, or raise error if there's no default. 42 | function req( $param, $default = NULL ) { 43 | if( !isset($_REQUEST[$param]) || strlen($_REQUEST[$param]) == 0 ) { 44 | if( is_null($default) ) 45 | error("Missing required parameter \"$param\"."); 46 | else 47 | return $default; 48 | } 49 | return trim($_REQUEST[$param]); 50 | } 51 | 52 | // Validate float or integer number, and check for min/max. 53 | function validate_num( $f, $name, $float, $min = NULL, $max = NULL ) { 54 | if( !preg_match($float ? '/^-?\d+(?:\.\d+)?$/' : '/^-?\d+$/', $f) ) 55 | error("Parameter \"$name\" should be " 56 | .($float ? 'a floating-point number with a dot as a separator.' : 'an integer number')); 57 | if( (!is_null($min) && $f < $min) || (!is_null($max) && $f > $max) ) 58 | error("Parameter \"$name\" should be a number between $min and $max."); 59 | } 60 | 61 | // Print a message (respecting output format) and shut down. 62 | function print_msg( $type, $msg ) { 63 | $fmt = out_fmt(); 64 | if( $fmt == 'json' ) { 65 | print_json(array($type => $msg)); 66 | } elseif ( $fmt == 'xml' ) { 67 | print xmlheader()."\t<$type>".htmlspecialchars($msg)."\n".xmlfooter(); 68 | } else { // html 69 | print ''.htmlspecialchars($msg).''; 70 | } 71 | exit; 72 | } 73 | 74 | // Prints an error message. 75 | function error( $msg ) { 76 | print_msg('error', $msg); 77 | } 78 | 79 | // Prints a success message. 80 | function message( $msg ) { 81 | print_msg('message', $msg); 82 | } 83 | 84 | // Returns requested outpue format: 'json' or 'xml'. 85 | function out_fmt() { 86 | if( isset($GLOBALS['html']) ) return 'html'; 87 | return isset($_REQUEST['format']) && strtolower($_REQUEST['format']) == 'json' ? 'json' : 'xml'; 88 | } 89 | 90 | // Prints json respecting jsonp parameter. 91 | function print_json( $data ) { 92 | header('Content-type: application/json'); 93 | $str = json_encode($data); 94 | print isset($_REQUEST['jsonp']) ? $_REQUEST['jsonp']."($str);" : $str; 95 | } 96 | 97 | // Prints XML header. 98 | function xmlheader() { 99 | header('Content-type: application/xml'); 100 | return "\n\n"; 101 | } 102 | 103 | // Prints XML footer. 104 | function xmlfooter() { 105 | return ""; 106 | } 107 | 108 | // Checks for input parameters and calls get() or get_json() depending on requested format. 109 | function do_get() { 110 | $lat = req('lat'); $lon = req('lon'); 111 | validate_num($lat, 'lat', true, -90.0, 90.0); 112 | validate_num($lon, 'lon', true, -180.0, 180.0); 113 | $radius = req('radius', DEFAULT_RADIUS); 114 | validate_num($radius, 'radius', false); 115 | 116 | $query = get_query($lat, $lon, min($radius, MAX_RADIUS)); 117 | if( out_fmt() == 'json' ) 118 | get_json($query); 119 | else 120 | get($query); 121 | } 122 | 123 | // Queries the database and prints XML. 124 | function get( $query ) { 125 | global $db; 126 | $result = $db->query($query); 127 | if( !$result ) 128 | error("Database query failed: ".$db->error); 129 | print xmlheader(); 130 | if( isset($_REQUEST['query']) ) 131 | print "\t".htmlspecialchars($query)."\n"; 132 | while( $data = $result->fetch_assoc() ) { 133 | $obsolete = is_null($data['abandon_date']) ? '' : ' deprecated="yes"'; 134 | $flagged = is_null($data['flagged']) ? '' : ' flagged="yes"'; 135 | $tag = !is_null($data['imagery']) ? 'offset' : (!is_null($data['calibration']) ? 'calibration' : 'unknown'); 136 | print "\t<$tag id=\"".$data['offset_id']."\" lat=\"".$data['lat']."\" lon=\"".$data['lon']."\"$obsolete$flagged>\n"; 137 | print "\t\t".htmlspecialchars($data['author'])."\n"; 138 | print "\t\t".htmlspecialchars($data['description'])."\n"; 139 | print "\t\t".$data['offset_date']."\n"; 140 | if( !is_null($data['abandon_date']) ) { 141 | print "\t\t\n"; 142 | print "\t\t\t".htmlspecialchars($data['abandon_author'])."\n"; 143 | print "\t\t\t".htmlspecialchars($data['abandon_reason'])."\n"; 144 | print "\t\t\t".$data['abandon_date']."\n"; 145 | print "\t\t\n"; 146 | } 147 | if( !is_null($data['imagery']) ) { 148 | $minzoom = is_null($data['min_zoom']) ? '' : ' min-zoom="'.$data['min_zoom'].'"'; 149 | $maxzoom = is_null($data['max_zoom']) ? '' : ' max-zoom="'.$data['max_zoom'].'"'; 150 | print "\t\t".htmlspecialchars($data['imagery'])."\n"; 151 | print "\t\t\n"; 152 | } elseif( !is_null($data['calibration']) ) { 153 | $geom = parse_wkt($data['cal_text']); 154 | print "\t\t\n"; 155 | foreach( $geom as $lonlat ) 156 | print "\t\t\t\n"; 157 | print "\t\t\n"; 158 | } 159 | print "\t\n"; 160 | } 161 | print xmlfooter(); 162 | $result->free(); 163 | } 164 | 165 | // Queries the database and prints JSON. 166 | function get_json( $query ) { 167 | global $db; 168 | $result = $db->query($query); 169 | if( !$result ) 170 | error("Database query failed: ".$db->error); 171 | $list = array(); 172 | $list[] = array('type' => 'meta', 'timestamp' => date(DATE_ISO8601)); 173 | while( $data = $result->fetch_assoc() ) { 174 | $item = array(); 175 | $item['type'] = !is_null($data['imagery']) ? 'offset' : (!is_null($data['calibration']) ? 'calibration' : 'unknown'); 176 | $item['id'] = $data['offset_id']; 177 | $item['lat'] = $data['lat']; 178 | $item['lon'] = $data['lon']; 179 | $item['author'] = $data['author']; 180 | $item['description'] = $data['description']; 181 | $item['date'] = $data['offset_date']; 182 | if( !is_null($data['abandon_date']) ) { 183 | $dep = array(); 184 | $dep['author'] = $data['abandon_author']; 185 | $dep['reason'] = $data['abandon_reason']; 186 | $dep['date'] = $data['abandon_date']; 187 | $item['deprecated'] = $dep; 188 | } 189 | if( !is_null($data['flagged']) ) 190 | $item['flagged'] = 1; 191 | if( !is_null($data['imagery']) ) { 192 | if( !is_null($data['min_zoom']) ) 193 | $item['min-zoom'] = $data['min_zoom']; 194 | if( !is_null($data['max_zoom']) ) 195 | $item['max-zoom'] = $data['max_zoom']; 196 | $item['imagery'] = $data['imagery']; 197 | $item['imlat'] = $data['imlat']; 198 | $item['imlon'] = $data['imlon']; 199 | } elseif( !is_null($data['calibration']) ) { 200 | $item['geometry'] = parse_wkt($data['cal_text']); 201 | } 202 | $list[] = $item; 203 | } 204 | $result->free(); 205 | print_json($list); 206 | } 207 | 208 | // Prepares a database query given lat, lon, radius and $_REQUEST['imagery']. 209 | function get_query( $lat, $lon, $radius ) { 210 | global $db; 211 | $coslat = cos($lat * M_PI / 180.0); 212 | $region = region_where_clause($lat, $lon, $radius); 213 | $qimagery = !isset($_REQUEST['imagery']) ? '' 214 | : "and (imagery is null or imagery = '".$db->escape_string(trim($_REQUEST['imagery']))."')"; 215 | $query = "select *, AsText(calibration) as cal_text, " 216 | ."X(coord) as lon, Y(coord) as lat, X(im_coord) as imlon, Y(im_coord) as imlat, " 217 | ."(Y(coord) - $lat)*(Y(coord) - $lat) + (X(coord) - $lon)*(X(coord) - $lon)*$coslat*$coslat as distance " 218 | ."from iodb where $region $qimagery order by distance limit 30"; 219 | return $query; 220 | } 221 | 222 | // Returns where clause for offsets around a point with given radius in km 223 | function region_where_clause( $lat, $lon, $radius ) { 224 | // http://stackoverflow.com/questions/973363/mysql-not-using-my-indexes 225 | // http://en.wikipedia.org/wiki/Latitude#The_length_of_a_degree_of_latitude 226 | // http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates 227 | $basekm = 6371.0; // 111.1; 228 | $coslat = cos($lat * M_PI / 180.0); 229 | $dlat = $radius / $basekm; 230 | $dlon = asin(sin($dlat) / $coslat); // $dlat * $coslat 231 | $bbox = wkt_bbox_around($lat, $lon, $dlat * 180.0 / M_PI, $dlon * 180.0 / M_PI); 232 | return "MBRContains($bbox, coord)"; 233 | } 234 | 235 | // Transforms center point + radiuses to a WKT string. 236 | function wkt_bbox_around( $lat, $lon, $dlat, $dlon ) { 237 | $minlat = $lat - $dlat; 238 | $minlon = $lon - $dlon; 239 | $maxlat = $lat + $dlat; 240 | $maxlon = $lon + $dlon; 241 | return "GeomFromText('POLYGON(($minlon $minlat, $minlon $maxlat, $maxlon $maxlat, $maxlon $minlat, $minlon $minlat))')"; 242 | } 243 | 244 | // Returns an array of coordinate arrays [lon, lat] from WKT. 245 | function parse_wkt( $wkt ) { 246 | $geom = array(); 247 | $type = strtoupper(substr($wkt, strpos($wkt, '('))); // we don't need it actually 248 | $i = strrpos($wkt, '(') + 1; 249 | $points = explode(',', substr($wkt, $i, strpos($wkt, ')') - $i)); 250 | foreach( $points as $point ) { 251 | $geom[] = explode(' ', trim($point)); 252 | } 253 | return $geom; 254 | } 255 | 256 | // Adds an entry to the database. 257 | function post() { 258 | global $db; 259 | $author = $db->escape_string(req('author')); 260 | $message = $db->escape_string(req('description')); 261 | if( mb_strlen($author, 'UTF8') == 0 || mb_strlen($author, 'UTF8') > 100 ) 262 | error('Incorrect author name'); 263 | $messagelen = mb_strlen($message, 'UTF8'); 264 | if( $messagelen < 3 ) 265 | error('Description is too short'); 266 | if( $messagelen > 200 ) 267 | error("Description is too long ($messagelen), 200 chars max"); 268 | $ip = bin2hex(inet_pton($_SERVER['REMOTE_ADDR'])); 269 | validate_ip($ip); 270 | 271 | if( isset($_REQUEST['imagery']) ) { 272 | $imagery = $db->escape_string(req('imagery')); 273 | $lat = req('lat'); $lon = req('lon'); 274 | validate_num($lat, 'lat', true, -90.0, 90.0); 275 | validate_num($lon, 'lon', true, -180.0, 180.0); 276 | // test if there is already something at there coordinates 277 | validate_dup($lat, $lon, $author, true); 278 | 279 | $imlat = req('imlat'); 280 | $imlon = req('imlon'); 281 | $minzoom = req('minzoom', 0); 282 | $maxzoom = req('maxzoom', 30); 283 | 284 | validate_num($imlon, 'imlon', true); 285 | validate_num($imlat, 'imlat', true); 286 | validate_num($minzoom, 'minzoom', false, 0, 30); 287 | validate_num($maxzoom, 'maxzoom', false, 0, 30); 288 | 289 | $query = "insert into iodb (offset_date, coord, author, description, ip, " 290 | ."imagery, im_coord, min_zoom, max_zoom) " 291 | ."values (CURDATE(), POINT($lon, $lat), '$author', '$message', 0x$ip, " 292 | ."'$imagery', POINT($imlon, $imlat), $minzoom, $maxzoom)"; 293 | } elseif( isset($_REQUEST['geometry']) ) { 294 | $geom = parse_geometry(req('geometry')); 295 | validate_dup($geom[1], $geom[0], $author, false); 296 | $query = "insert into iodb (offset_date, coord, author, description, ip, calibration) " 297 | ."values (CURDATE(), POINT($geom[0], $geom[1]), '$author', '$message', 0x$ip, GeomFromText('$geom[2]'))"; 298 | } else { 299 | error("Not enough parameters"); 300 | } 301 | // everything's set, add this! 302 | $result = $db->query($query); 303 | if( !$result ) { 304 | error("Database query failed: ".$db->error); 305 | } 306 | message('Offset was added to the database, thank you.'); 307 | } 308 | 309 | // Request a single value from the database 310 | function request_one($query) { 311 | global $db; 312 | $result = $db->query($query); 313 | if( !$result ) 314 | error('Database query failed: '.$db->error); 315 | if( $result->num_rows > 0 ) { 316 | $tmp = $result->fetch_row(); 317 | $ret = $tmp[0]; 318 | } else { 319 | $ret = null; 320 | } 321 | $result->free(); 322 | return $ret; 323 | } 324 | 325 | // Check that there are no offset of a given type in this point. 326 | function validate_dup($lat, $lon, $author, $is_imagery) { 327 | $is_im_clause = $is_imagery ? 'is not null' : 'is null'; 328 | $query = "select offset_id from iodb where " 329 | ."MBRContains(".wkt_bbox_around($lat, $lon, 1e-6, 1e-6).", coord) " 330 | ."and author = '$author' and imagery $is_im_clause"; 331 | if( request_one($query) != null ) 332 | error("There is already an offset at coordinates ($lat, $lon), sorry."); 333 | } 334 | 335 | // Limit offsets per day for a single IP. 336 | function validate_ip( $ipenc ) { 337 | $query = "select count(1) from iodb where " 338 | ."(ip = 0x$ipenc or abandon_ip = 0x$ipenc) and (offset_date = CURDATE() or abandon_date = CURDATE())"; 339 | $cnt = request_one($query); 340 | if( $cnt > MAX_REQUEST_PER_IP_PER_DAY ) 341 | error('You have made '.MAX_REQUEST_PER_IP_PER_DAY.' requests today. That\'s the limit, sorry.'); 342 | } 343 | 344 | // Parses query parameter 'geometry', returns array of [lon, lat, wkt]. 345 | function parse_geometry( $geom ) { 346 | $points = explode(',', $geom); 347 | $g = array(); 348 | $alat = 0.0; 349 | $alon = 0.0; 350 | foreach( $points as $pt ) { 351 | if( !preg_match('/(-?\d{1,3}(?:\.\d+)?)\s+(-?\d{1,2}(?:\.\d+)?)/', $pt, $matches) ) 352 | error("Incorrect format of a geometry point: $pt"); 353 | $g[] = $matches[1].' '.$matches[2]; 354 | $alon += $matches[1]; 355 | $alat += $matches[2]; 356 | } 357 | 358 | $cnt = count($g); 359 | if( $cnt == 0 ) 360 | error("No points specified in the geometry."); 361 | if( $cnt > 100 ) 362 | error("Too many points (".count($g)." > 100) in the geometry."); 363 | 364 | $wkt_base = implode(',', $g); 365 | if( $cnt == 1 ) 366 | $wkt = "POINT($wkt_base)"; 367 | elseif( $g[0] != $g[$cnt-1] ) 368 | $wkt = "LINESTRING($wkt_base)"; 369 | else 370 | $wkt = "POLYGON(($wkt_base))"; 371 | 372 | return array($alon / $cnt, $alat / $cnt, $wkt); 373 | } 374 | 375 | // Mark a specified offset as deprecated. 376 | function deprecate() { 377 | global $db; 378 | $id = req('id'); 379 | deprecate_impl($id, req('author'), req('reason')); 380 | message("Offset $id was deprecated."); 381 | } 382 | 383 | // Mark a specified offset as deprecated (or not). 384 | function deprecate_impl( $id, $author, $reason ) { 385 | global $db; 386 | validate_num($id, 'id', false); 387 | if( mb_strlen($author, 'UTF8') == 0 || mb_strlen($author, 'UTF8') > 100 ) 388 | error('Incorrect author name'); 389 | $messagelen = mb_strlen($reason, 'UTF8'); 390 | if( $messagelen < 3 ) 391 | error('Reason is too short'); 392 | if( $messagelen > 200 ) 393 | error("Reason is too long ($messagelen), 200 chars max"); 394 | 395 | $ip = bin2hex(inet_pton($_SERVER['REMOTE_ADDR'])); 396 | validate_ip($ip); 397 | 398 | $result = $db->query("select abandon_date from iodb where offset_id = $id"); 399 | if( !$result ) 400 | error("Database query failed: ".$db->error); 401 | if( $result->num_rows < 1 ) 402 | error("No such offset"); 403 | $r = $result->fetch_row(); 404 | $result->free(); 405 | if( !is_null($r[0]) ) 406 | error("Offset $id is already deprecated"); 407 | 408 | $qset = "abandon_date = CURDATE(), abandon_author = '".$db->escape_string($author)."', abandon_reason = '".$db->escape_string($reason)."', abandon_ip = 0x$ip"; 409 | $result = $db->query("update iodb set $qset where offset_id = $id"); 410 | if( !$result ) 411 | error("Database query failed: ".$db->error); 412 | } 413 | 414 | // Flag a specified offset. 415 | function report() { 416 | global $db; 417 | $id = req('id'); 418 | report_impl($id, req('reason')); 419 | message("Offset $id was flagged."); 420 | } 421 | 422 | // Flag/unflag a specified offset. 423 | function report_impl($id, $message = null) { 424 | global $db; 425 | if( is_array($id) ) { 426 | foreach( $id as $tid ) 427 | validate_num($tid, 'id', false); 428 | $qid = 'in ('.implode(',', $id).')'; 429 | } else { 430 | validate_num($id, 'id', false); 431 | $result = $db->query("select flagged from iodb where offset_id = $id"); 432 | if( !$result ) 433 | error("Database query failed: ".$db->error); 434 | if( $result->num_rows < 1 ) 435 | error("No such offset"); 436 | $arr = $result->fetch_row(); 437 | if( !is_null($message) && !is_null($arr[0]) ) 438 | error('The offset is already reported.'); 439 | if( is_null($message) && is_null($arr[0]) ) 440 | error('The offset is not reported yet.'); 441 | $result->free(); 442 | $qid = '= '.$id; 443 | } 444 | 445 | $result = $db->query('update iodb set flagged = '.(is_null($message) ? 'null' : "'".$db->escape_string($message)."'")." where offset_id $qid"); 446 | if( !$result ) 447 | error("Database query failed: ".$db->error); 448 | } 449 | 450 | function undeprecate( $id ) { 451 | global $db; 452 | if( is_array($id) ) { 453 | foreach( $id as $tid ) 454 | validate_num($tid, 'id', false); 455 | $qid = 'in ('.implode(',', $id).')'; 456 | } else { 457 | validate_num($id, 'id', false); 458 | $result = $db->query("select abandon_date from iodb where offset_id = $id"); 459 | if( !$result ) 460 | error("Database query failed: ".$db->error); 461 | if( $result->num_rows < 1 ) 462 | error("No such offset"); 463 | $r = $result->fetch_row(); 464 | if( is_null($r[0]) ) 465 | error("Offset $id is not deprecated"); 466 | $result->free(); 467 | $qid = '= '.$id; 468 | } 469 | 470 | $result = $db->query("update iodb set abandon_date = null where offset_id $qid"); 471 | if( !$result ) 472 | error("Database query failed: ".$db->error); 473 | } 474 | 475 | function delete_impl( $id ) { 476 | global $db; 477 | if( !is_array($id) ) { 478 | $qid = '= '.$id; 479 | validate_num($id, 'id', false); 480 | } else { 481 | foreach( $id as $tid ) 482 | validate_num($tid, 'id', false); 483 | $qid = 'in ('.implode(',', $id).')'; 484 | } 485 | $result = $db->query("delete from iodb where offset_id $qid"); 486 | if( !$result ) 487 | error("Database query failed: ".$db->error); 488 | } 489 | 490 | // Just dump the whole database. Is called only from command line. 491 | function db_dump() { 492 | global $argc, $argv; 493 | $query = 'select *, AsText(calibration) as cal_text, ' 494 | .'X(coord) as lon, Y(coord) as lat, X(im_coord) as imlon, Y(im_coord) as imlat ' 495 | .'from iodb order by offset_id'; 496 | if( $argc > 1 ) { 497 | if( $argv[1] == 'loc' ) 498 | find_locations(); 499 | else if( $argv[1] == 'create' ) 500 | create_table(); 501 | else if( $argv[1] == 'json' ) 502 | get_json($query); 503 | else if( $argv[1] == 'xml' ) 504 | get($query); 505 | else 506 | print "Unknown action\n"; 507 | } else { 508 | print "IODB Command line\nSyntax: php $argv[0] {xml|json|loc|create}\n"; 509 | } 510 | } 511 | 512 | // Pass the database dump file through, respecting requested output format. 513 | function db_dump_passthru() { 514 | header('Content-Encoding: gzip'); 515 | if( out_fmt() == 'json' ) { 516 | header('Content-type: application/json'); 517 | $file = 'iodb-latest.json.gz'; 518 | } else { 519 | header('Content-type: application/xml'); 520 | $file = 'iodb-latest.xml.gz'; 521 | } 522 | header('Content-Length: '.filesize('download/'.$file)); 523 | // header('Content-Disposition: attachment; filename='.$file); 524 | header('Expires: 0'); 525 | header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); 526 | header('Pragma: public'); 527 | readfile('download/'.$file); 528 | } 529 | 530 | // Query nominatim for unset locations in the database 531 | function find_locations() { 532 | global $db; 533 | $result = $db->query("select offset_id, X(coord) as lon, Y(coord) as lat from iodb where location is null order by offset_id limit 3"); 534 | if( !$result ) 535 | error("Database query failed: ".$db->error); 536 | $queue = array(); 537 | while( $line = $result->fetch_assoc() ) 538 | $queue[] = array('id' => $line['offset_id'], 'lat' => $line['lat'], 'lon' => $line['lon']); 539 | $result->free(); 540 | 541 | $lastrow = request_one("select max(offset_id) from iodb where location is not null"); 542 | $result = $db->query("select offset_id, X(coord) as lon, Y(coord) as lat from iodb where offset_id = ".rand(1, $lastrow)); 543 | if( !$result ) 544 | error("Database query failed: ".$db->error); 545 | while( $line = $result->fetch_assoc() ) 546 | $queue[] = array('id' => $line['offset_id'], 'lat' => $line['lat'], 'lon' => $line['lon']); 547 | $result->free(); 548 | 549 | foreach( $queue as $offset ) { 550 | $url = 'https://nominatim.openstreetmap.org/reverse?format=json&zoom=16&addressdetails=0' 551 | .'&email=zverik%40textual.ru&lat='.$offset['lat'].'&lon='.$offset['lon']; 552 | $response = json_decode(file_get_contents($url), true); 553 | if( isset($response) && (isset($response['display_name']) || isset($response['error'])) ) { 554 | $newval = $db->escape_string(isset($response['display_name']) ? $response['display_name'] : ''); 555 | $result = $db->query("update iodb set location='$newval' where offset_id = ".$offset['id']); 556 | if( !$result ) 557 | error("Database query failed: ".$db->error); 558 | } 559 | } 560 | } 561 | 562 | function create_table() { 563 | global $db; 564 | $query = <<query($query); 597 | if( !$result ) { 598 | print('Could not create iodb table: '.$db->error); 599 | exit; 600 | } 601 | } 602 | 603 | ?> 604 | -------------------------------------------------------------------------------- /www/lib/leaflet.markercluster.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Smartrak, David Leaver 3 | Leaflet.markercluster is an open-source JavaScript library for Marker Clustering on leaflet powered maps. 4 | https://github.com/danzel/Leaflet.markercluster 5 | */ 6 | (function(e,t){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),L.FeatureGroup.prototype.initialize.call(this,[]),this._inZoomAnimation=0,this._needsClustering=[],this._currentShownBounds=null},addLayer:function(e){if(e instanceof L.LayerGroup){var t=[];for(var n in e._layers)e._layers.hasOwnProperty(n)&&t.push(e._layers[n]);return this.addLayers(t)}if(!this._map)return this._needsClustering.push(e),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom);var r=e,i=this._map.getZoom();if(e.__parent)while(r.__parent._zoom>=i)r=r.__parent;return this._currentShownBounds.contains(r.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,r):this._animationAddLayerNonAnimated(e,r)),this},removeLayer:function(e){return this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),e._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1)),this):this:(this._arraySplice(this._needsClustering,e),this)},addLayers:function(e){var t,n,r;if(!this._map)return this._needsClustering=this._needsClustering.concat(e),this;for(t=0,n=e.length;t=0;t--)e.extend(this._needsClustering[t].getLatLng());return e},eachLayer:function(e,t){var n=this._needsClustering.slice(),r;this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n);for(r=n.length-1;r>=0;r--)e.call(t,n[r])},hasLayer:function(e){if(this._needsClustering.length>0){var t=this._needsClustering;for(var n=t.length-1;n>=0;n--)if(t[n]===e)return!0}return!!e.__parent&&e.__parent._group===this},zoomToShowLayer:function(e,t){var n=function(){if((e._icon||e.__parent._icon)&&!this._inZoomAnimation){this._map.off("moveend",n,this),this.off("animationend",n,this);if(e._icon)t();else if(e.__parent._icon){var r=function(){this.off("spiderfied",r,this),t()};this.on("spiderfied",r,this),e.__parent.spiderfy()}}};e._icon?t():e.__parent._zoom=0;n--)if(e[n]===t){e.splice(n,1);return}},_removeLayer:function(e,t,n){var r=this._gridClusters,i=this._gridUnclustered,s=this._map;if(t)for(var o=this._maxZoom;o>=0;o--)if(!i[o].removeObject(e,s.project(e.getLatLng(),o)))break;var u=e.__parent,a=u._markers,f;this._arraySplice(a,e);while(u){u._childCount--;if(u._zoom<0)break;t&&u._childCount<=1?(f=u._markers[0]===e?u._markers[1]:u._markers[0],r[u._zoom].removeObject(u,s.project(u._cLatLng,u._zoom)),i[u._zoom].addObject(f,s.project(f.getLatLng(),u._zoom)),this._arraySplice(u.__parent._childClusters,u),u.__parent._markers.push(f),f.__parent=u.__parent,u._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,u),n||L.FeatureGroup.prototype.addLayer.call(this,f))):(u._recalculateBounds(),(!n||!u._icon)&&u._updateIcon()),u=u.__parent}delete e.__parent},_propagateEvent:function(e){e.target instanceof L.MarkerCluster&&(e.type="cluster"+e.type),L.FeatureGroup.prototype._propagateEvent.call(this,e)},_defaultIconCreateFunction:function(e){var t=e.getChildCount(),n=" marker-cluster-";return t<10?n+="small":t<100?n+="medium":n+="large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+n,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=null,t=this._map,n=this.options.spiderfyOnMaxZoom,r=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick;(n||i)&&this.on("clusterclick",function(e){t.getMaxZoom()===t.getZoom()?n&&e.layer.spiderfy():i&&e.layer.zoomToBounds()},this),r&&(this.on("clustermouseover",function(n){if(this._inZoomAnimation)return;e&&t.removeLayer(e),n.layer.getChildCount()>2&&n.layer!==this._spiderfied&&(e=new L.Polygon(n.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(e))},this),this.on("clustermouseout",function(){e&&(t.removeLayer(e),e=null)},this),t.on("zoomend",function(){e&&(t.removeLayer(e),e=null)},this),t.on("layerremove",function(n){e&&n.layer===this&&(t.removeLayer(e),e=null)},this))},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick,r=this._map;(e||n)&&this.off("clusterclick",null,this),t&&(this.off("clustermouseover",null,this),this.off("clustermouseout",null,this),r.off("zoomend",null,this),r.off("layerremove",null,this))},_zoomEnd:function(){if(!this._map)return;this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds()},_moveEnd:function(){if(this._inZoomAnimation)return;var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,e),this._currentShownBounds=e;return},_generateInitialClusters:function(){var e=this._map.getMaxZoom(),t=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var n=e;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(t),this._gridUnclustered[n]=new L.DistanceGrid(t);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(e,t){var n=this._gridClusters,r=this._gridUnclustered,i,s;this.options.singleMarkerMode&&(e.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[e]}}));for(;t>=0;t--){i=this._map.project(e.getLatLng(),t);var o=n[t].getNearObject(i);if(o){o._addChild(e),e.__parent=o;return}o=r[t].getNearObject(i);if(o){var u=o.__parent;u&&this._removeLayer(o,!1);var a=new L.MarkerCluster(this,t,o,e);n[t].addObject(a,this._map.project(a._cLatLng,t)),o.__parent=a,e.__parent=a;var f=a;for(s=t-1;s>u._zoom;s--)f=new L.MarkerCluster(this,s,f),n[s].addObject(f,this._map.project(o.getLatLng(),s));u._addChild(f);for(s=t;s>=0;s--)if(!r[s].removeObject(o,this._map.project(o.getLatLng(),s)))break;return}r[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel;return},_mergeSplitClusters:function(){this._zoomthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var e=this._map,t=e.getBounds(),n=t._southWest,r=t._northEast,i=L.Browser.mobile?0:Math.abs(n.lat-r.lat),s=L.Browser.mobile?0:Math.abs(n.lng-r.lng);return new L.LatLngBounds(new L.LatLng(n.lat-i,n.lng-s,!0),new L.LatLng(r.lat+i,r.lng+s,!0))},_animationAddLayerNonAnimated:function(e,t){if(t===e)L.FeatureGroup.prototype.addLayer.call(this,e);else if(t._childCount===2){t._addToMap();var n=t.getAllChildMarkers();L.FeatureGroup.prototype.removeLayer.call(this,n[0]),L.FeatureGroup.prototype.removeLayer.call(this,n[1])}else t._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(e,t){var n=this,r=this._getExpandedVisibleBounds(),i;this._topClusterLevel._recursively(r,e,0,function(s){var o=s._latlng,u=s._markers,a;s._isSingleParent()&&e+1===t?(L.FeatureGroup.prototype.removeLayer.call(n,s),s._recursivelyAddChildrenToMap(null,t,r)):(s.setOpacity(0),s._recursivelyAddChildrenToMap(o,t,r));for(i=u.length-1;i>=0;i--)a=u[i],r.contains(a._latlng)||L.FeatureGroup.prototype.removeLayer.call(n,a)}),this._forceLayout();var s,o;n._topClusterLevel._recursivelyBecomeVisible(r,t);for(s in n._layers)n._layers.hasOwnProperty(s)&&(o=n._layers[s],!(o instanceof L.MarkerCluster)&&o._icon&&o.setOpacity(1));n._topClusterLevel._recursively(r,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),setTimeout(function(){n._topClusterLevel._recursively(r,e,0,function(e){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1)}),n._animationEnd()},200)},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(e,t,n){var r=this._getExpandedVisibleBounds();e._recursivelyAnimateChildrenInAndAddSelfToMap(r,t+1,n);var i=this;this._forceLayout(),e._recursivelyBecomeVisible(r,n),setTimeout(function(){if(e._childCount===1){var s=e._markers[0];s.setLatLng(s.getLatLng()),s.setOpacity(1);return}e._recursively(r,n,0,function(e){e._recursivelyRemoveChildrenFromMap(r,t+1)}),i._animationEnd()},200)},_animationAddLayer:function(e,t){var n=this;L.FeatureGroup.prototype.addLayer.call(this,e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.setOpacity(0),setTimeout(function(){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1),n._animationEnd()},200)):(this._forceLayout(),n._animationStart(),n._animationZoomOutSingle(t,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationZoomOut:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){this._animationAddLayerNonAnimated(e,t)}}),L.MarkerCluster=L.Marker.extend({initialize:function(e,t,n,r){L.Marker.prototype.initialize.call(this,n?n._cLatLng||n.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,n&&this._addChild(n),r&&this._addChild(r)},getAllChildMarkers:function(e){e=e||[];for(var t=this._childClusters.length-1;t>=0;t--)this._childClusters[t].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(){this._group._map.fitBounds(this._bounds)},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._expandBounds(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_expandBounds:function(e){var t,n=e._wLatLng||e._latlng;e instanceof L.MarkerCluster?(this._bounds.extend(e._bounds),t=e._childCount):(this._bounds.extend(n),t=1),this._cLatLng||(this._cLatLng=e._cLatLng||n);var r=this._childCount+t;this._wLatLng?(this._wLatLng.lat=(n.lat*t+this._wLatLng.lat*this._childCount)/r,this._wLatLng.lng=(n.lng*t+this._wLatLng.lng*this._childCount)/r):this._latlng=this._wLatLng=new L.LatLng(n.lat,n.lng)},_addToMap:function(e){e&&(this._backupLatlng=this._latlng,this.setLatLng(e)),L.FeatureGroup.prototype.addLayer.call(this._group,this)},_recursivelyAnimateChildrenIn:function(e,t,n){this._recursively(e,0,n-1,function(e){var n=e._markers,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))},function(e){var n=e._childClusters,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,n){this._recursively(e,n,0,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),t),r._isSingleParent()&&t-1===n?(r.setOpacity(1),r._recursivelyRemoveChildrenFromMap(e,t)):r.setOpacity(0),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,0,t,null,function(e){e.setOpacity(1)})},_recursivelyAddChildrenToMap:function(e,t,n){this._recursively(n,-1,t,function(r){if(t===r._zoom)return;for(var i=r._markers.length-1;i>=0;i--){var s=r._markers[i];if(!n.contains(s._latlng))continue;e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.setOpacity(0)),L.FeatureGroup.prototype.addLayer.call(r._group,s)}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var n=this._markers[t];n._backupLatlng&&(n.setLatLng(n._backupLatlng),delete n._backupLatlng)}if(e-1===this._zoom)for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._restorePosition();else for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,n){var r,i;this._recursively(e,-1,t-1,function(e){for(i=e._markers.length-1;i>=0;i--){r=e._markers[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}},function(e){for(i=e._childClusters.length-1;i>=0;i--){r=e._childClusters[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}})},_recursively:function(e,t,n,r,i){var s=this._childClusters,o=this._zoom,u,a;if(t>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i);else{r&&r(this),i&&this._zoom===n&&i(this);if(n>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i)}},_recalculateBounds:function(){var e=this._markers,t=this._childClusters,n;this._bounds=new L.LatLngBounds,delete this._wLatLng;for(n=e.length-1;n>=0;n--)this._expandBounds(e[n]);for(n=t.length-1;n>=0;n--)this._expandBounds(t[n])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u=L.Util.stamp(e);this._objectPoint[u]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u,a;delete this._objectPoint[L.Util.stamp(e)];for(u=0,a=o.length;u=0;s--){o=t[s],u=this.getDistant(o,e);if(!(u>0))continue;i.push(o),u>n&&(n=u,r=o)}return{maxPoint:r,newPoints:i}},buildConvexHull:function(e,t){var n=[],r=this.findMostDistantPointFromBaseLine(e,t);return r.maxPoint?(n=n.concat(this.buildConvexHull([e[0],r.maxPoint],r.newPoints)),n=n.concat(this.buildConvexHull([r.maxPoint,e[1]],r.newPoints)),n):[e]},getConvexHull:function(e){var t=!1,n=!1,r=null,i=null,s;for(s=e.length-1;s>=0;s--){var o=e[s];if(t===!1||o.lat>t)r=o,t=o.lat;if(n===!1||o.lat=0;s--)i=e[s].getLatLng(),t.push(i);r=L.QuickHull.getConvexHull(t);for(s=r.length-1;s>=0;s--)n.push(r[s][0]);return n}}),L.MarkerCluster.include({_2PI:Math.PI*2,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied===this||this._group._inZoomAnimation)return;var e=this.getAllChildMarkers(),t=this._group,n=t._map,r=n.latLngToLayerPoint(this._latlng),i;this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?i=this._generatePointsSpiral(e.length,r):(r.y+=10,i=this._generatePointsCircle(e.length,r)),this._animationSpiderfy(e,i)},unspiderfy:function(e){if(this._group._inZoomAnimation)return;this._animationUnspiderfy(e),this._group._spiderfied=null},_generatePointsCircle:function(e,t){var n=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),r=n/this._2PI,i=this._2PI/e,s=[],o,u;s.length=e;for(o=e-1;o>=0;o--)u=this._circleStartAngle+o*i,s[o]=(new L.Point(t.x+r*Math.cos(u),t.y+r*Math.sin(u)))._round();return s},_generatePointsSpiral:function(e,t){var n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,r=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,i=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,s=0,o=[],u;o.length=e;for(u=e-1;u>=0;u--)s+=r/n+u*5e-4,o[u]=(new L.Point(t.x+n*Math.cos(s),t.y+n*Math.sin(s)))._round(),n+=this._2PI*i/s;return o}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return(document.createElementNS("http://www.w3.org/2000/svg","animate")+"").indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(e,t){var n=this,r=this._group,i=r._map,s=i.latLngToLayerPoint(this._latlng),o,u,a,f;for(o=e.length-1;o>=0;o--)u=e[o],u.setZIndexOffset(1e6),u.setOpacity(0),L.FeatureGroup.prototype.addLayer.call(r,u),u._setPos(s);r._forceLayout(),r._animationStart();var l=L.Path.SVG?0:.3,c=L.Path.SVG_NS;for(o=e.length-1;o>=0;o--){f=i.layerPointToLatLng(t[o]),u=e[o],u._preSpiderfyLatlng=u._latlng,u.setLatLng(f),u.setOpacity(1),a=new L.Polyline([n._latlng,f],{weight:1.5,color:"#222",opacity:l}),i.addLayer(a),u._spiderLeg=a;if(!L.Path.SVG||!this.SVG_ANIMATION)continue;var h=a._path.getTotalLength();a._path.setAttribute("stroke-dasharray",h+","+h);var p=document.createElementNS(c,"animate");p.setAttribute("attributeName","stroke-dashoffset"),p.setAttribute("begin","indefinite"),p.setAttribute("from",h),p.setAttribute("to",0),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement(),p=document.createElementNS(c,"animate"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("begin","indefinite"),p.setAttribute("from",0),p.setAttribute("to",.5),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement()}n.setOpacity(.3);if(L.Path.SVG){this._group._forceLayout();for(o=e.length-1;o>=0;o--)u=e[o]._spiderLeg,u.options.opacity=.5,u._path.setAttribute("stroke-opacity",.5)}setTimeout(function(){r._animationEnd(),r.fire("spiderfied")},200)},_animationUnspiderfy:function(e){var t=this._group,n=t._map,r=e?n._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):n.latLngToLayerPoint(this._latlng),i=this.getAllChildMarkers(),s=L.Path.SVG&&this.SVG_ANIMATION,o,u,a;t._animationStart(),this.setOpacity(1);for(u=i.length-1;u>=0;u--){o=i[u];if(!o._preSpiderfyLatlng)continue;o.setLatLng(o._preSpiderfyLatlng),delete o._preSpiderfyLatlng,o._setPos(r),o.setOpacity(0),s&&(a=o._spiderLeg._path.childNodes[0],a.setAttribute("to",a.getAttribute("from")),a.setAttribute("from",0),a.beginElement(),a=o._spiderLeg._path.childNodes[1],a.setAttribute("from",.5),a.setAttribute("to",0),a.setAttribute("stroke-opacity",0),a.beginElement(),o._spiderLeg._path.setAttribute("stroke-opacity",0))}setTimeout(function(){var e=0;for(u=i.length-1;u>=0;u--)o=i[u],o._spiderLeg&&e++;for(u=i.length-1;u>=0;u--){o=i[u];if(!o._spiderLeg)continue;o.setOpacity(1),o.setZIndexOffset(0),e>1&&L.FeatureGroup.prototype.removeLayer.call(t,o),n.removeLayer(o._spiderLeg),delete o._spiderLeg}t._animationEnd()},200)}}:{_animationSpiderfy:function(e,t){var n=this._group,r=n._map,i,s,o,u;for(i=e.length-1;i>=0;i--)u=r.layerPointToLatLng(t[i]),s=e[i],s._preSpiderfyLatlng=s._latlng,s.setLatLng(u),s.setZIndexOffset(1e6),L.FeatureGroup.prototype.addLayer.call(n,s),o=new L.Polyline([this._latlng,u],{weight:1.5,color:"#222"}),r.addLayer(o),s._spiderLeg=o;this.setOpacity(.3),n.fire("spiderfied")},_animationUnspiderfy:function(){var e=this._group,t=e._map,n=this.getAllChildMarkers(),r,i;this.setOpacity(1);for(i=n.length-1;i>=0;i--)r=n[i],L.FeatureGroup.prototype.removeLayer.call(e,r),r.setLatLng(r._preSpiderfyLatlng),delete r._preSpiderfyLatlng,r.setZIndexOffset(0),t.removeLayer(r._spiderLeg),delete r._spiderLeg}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation?this._map.on("zoomstart",this._unspiderfyZoomStart,this):this._map.on("zoomend",this._unspiderfyWrapper,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){if(!this._map)return;this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){if(L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching"))return;this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e)},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_unspiderfyLayer:function(e){e._spiderLeg&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1),e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}})})(this); -------------------------------------------------------------------------------- /www/lib/heatmap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * heatmap.js 1.0 - JavaScript Heatmap Library 3 | * 4 | * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at) 5 | * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 6 | * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license. 7 | */ 8 | 9 | (function(w){ 10 | // the heatmapFactory creates heatmap instances 11 | var heatmapFactory = (function(){ 12 | 13 | // store object constructor 14 | // a heatmap contains a store 15 | // the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added 16 | var store = function store(hmap){ 17 | 18 | var _ = { 19 | // data is a two dimensional array 20 | // a datapoint gets saved as data[point-x-value][point-y-value] 21 | // the value at [point-x-value][point-y-value] is the occurrence of the datapoint 22 | data: [], 23 | // tight coupling of the heatmap object 24 | heatmap: hmap 25 | }; 26 | // the max occurrence - the heatmaps radial gradient alpha transition is based on it 27 | this.max = 1; 28 | 29 | this.get = function(key){ 30 | return _[key]; 31 | }; 32 | this.set = function(key, value){ 33 | _[key] = value; 34 | }; 35 | } 36 | 37 | store.prototype = { 38 | // function for adding datapoints to the store 39 | // datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence 40 | addDataPoint: function(x, y){ 41 | if(x < 0 || y < 0) 42 | return; 43 | 44 | var me = this, 45 | heatmap = me.get("heatmap"), 46 | data = me.get("data"); 47 | 48 | if(!data[x]) 49 | data[x] = []; 50 | 51 | if(!data[x][y]) 52 | data[x][y] = 0; 53 | 54 | // if count parameter is set increment by count otherwise by 1 55 | data[x][y]+=(arguments.length<3)?1:arguments[2]; 56 | 57 | me.set("data", data); 58 | // do we have a new maximum? 59 | if(me.max < data[x][y]){ 60 | // max changed, we need to redraw all existing(lower) datapoints 61 | heatmap.get("actx").clearRect(0,0,heatmap.get("width"),heatmap.get("height")); 62 | me.setDataSet({ max: data[x][y], data: data }, true); 63 | return; 64 | } 65 | heatmap.drawAlpha(x, y, data[x][y], true); 66 | }, 67 | setDataSet: function(obj, internal){ 68 | var me = this, 69 | heatmap = me.get("heatmap"), 70 | data = [], 71 | d = obj.data, 72 | dlen = d.length; 73 | // clear the heatmap before the data set gets drawn 74 | heatmap.clear(); 75 | this.max = obj.max; 76 | // if a legend is set, update it 77 | heatmap.get("legend") && heatmap.get("legend").update(obj.max); 78 | 79 | if(internal != null && internal){ 80 | for(var one in d){ 81 | // jump over undefined indexes 82 | if(one === undefined) 83 | continue; 84 | for(var two in d[one]){ 85 | if(two === undefined) 86 | continue; 87 | // if both indexes are defined, push the values into the array 88 | heatmap.drawAlpha(one, two, d[one][two], false); 89 | } 90 | } 91 | }else{ 92 | while(dlen--){ 93 | var point = d[dlen]; 94 | heatmap.drawAlpha(point.x, point.y, point.count, false); 95 | if(!data[point.x]) 96 | data[point.x] = []; 97 | 98 | if(!data[point.x][point.y]) 99 | data[point.x][point.y] = 0; 100 | 101 | data[point.x][point.y] = point.count; 102 | } 103 | } 104 | heatmap.colorize(); 105 | this.set("data", d); 106 | }, 107 | exportDataSet: function(){ 108 | var me = this, 109 | data = me.get("data"), 110 | exportData = []; 111 | 112 | for(var one in data){ 113 | // jump over undefined indexes 114 | if(one === undefined) 115 | continue; 116 | for(var two in data[one]){ 117 | if(two === undefined) 118 | continue; 119 | // if both indexes are defined, push the values into the array 120 | exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]}); 121 | } 122 | } 123 | 124 | return { max: me.max, data: exportData }; 125 | }, 126 | generateRandomDataSet: function(points){ 127 | var heatmap = this.get("heatmap"), 128 | w = heatmap.get("width"), 129 | h = heatmap.get("height"); 130 | var randomset = {}, 131 | max = Math.floor(Math.random()*1000+1); 132 | randomset.max = max; 133 | var data = []; 134 | while(points--){ 135 | data.push({x: Math.floor(Math.random()*w+1), y: Math.floor(Math.random()*h+1), count: Math.floor(Math.random()*max+1)}); 136 | } 137 | randomset.data = data; 138 | this.setDataSet(randomset); 139 | } 140 | }; 141 | 142 | var legend = function legend(config){ 143 | this.config = config; 144 | 145 | var _ = { 146 | element: null, 147 | labelsEl: null, 148 | gradientCfg: null, 149 | ctx: null 150 | }; 151 | this.get = function(key){ 152 | return _[key]; 153 | }; 154 | this.set = function(key, value){ 155 | _[key] = value; 156 | }; 157 | this.init(); 158 | }; 159 | legend.prototype = { 160 | init: function(){ 161 | var me = this, 162 | config = me.config, 163 | title = config.title || "Legend", 164 | position = config.position, 165 | offset = config.offset || 10, 166 | gconfig = config.gradient, 167 | labelsEl = document.createElement("ul"), 168 | labelsHtml = "", 169 | grad, element, gradient, positionCss = ""; 170 | 171 | me.processGradientObject(); 172 | 173 | // Positioning 174 | 175 | // top or bottom 176 | if(position.indexOf('t') > -1){ 177 | positionCss += 'top:'+offset+'px;'; 178 | }else{ 179 | positionCss += 'bottom:'+offset+'px;'; 180 | } 181 | 182 | // left or right 183 | if(position.indexOf('l') > -1){ 184 | positionCss += 'left:'+offset+'px;'; 185 | }else{ 186 | positionCss += 'right:'+offset+'px;'; 187 | } 188 | 189 | element = document.createElement("div"); 190 | element.style.cssText = "border-radius:5px;position:absolute;"+positionCss+"font-family:Helvetica; width:256px;z-index:10000000000; background:rgba(255,255,255,1);padding:10px;border:1px solid black;margin:0;"; 191 | element.innerHTML = "

"+title+"

"; 192 | // create gradient in canvas 193 | labelsEl.style.cssText = "position:relative;font-size:12px;display:block;list-style:none;list-style-type:none;margin:0;height:15px;"; 194 | 195 | 196 | // create gradient element 197 | gradient = document.createElement("div"); 198 | gradient.style.cssText = ["position:relative;display:block;width:256px;height:15px;border-bottom:1px solid black; background-image:url(",me.createGradientImage(),");"].join(""); 199 | 200 | element.appendChild(labelsEl); 201 | element.appendChild(gradient); 202 | 203 | me.set("element", element); 204 | me.set("labelsEl", labelsEl); 205 | 206 | me.update(1); 207 | }, 208 | processGradientObject: function(){ 209 | // create array and sort it 210 | var me = this, 211 | gradientConfig = this.config.gradient, 212 | gradientArr = []; 213 | 214 | for(var key in gradientConfig){ 215 | if(gradientConfig.hasOwnProperty(key)){ 216 | gradientArr.push({ stop: key, value: gradientConfig[key] }); 217 | } 218 | } 219 | gradientArr.sort(function(a, b){ 220 | return (a.stop - b.stop); 221 | }); 222 | gradientArr.unshift({ stop: 0, value: 'rgba(0,0,0,0)' }); 223 | 224 | me.set("gradientArr", gradientArr); 225 | }, 226 | createGradientImage: function(){ 227 | var me = this, 228 | gradArr = me.get("gradientArr"), 229 | length = gradArr.length, 230 | canvas = document.createElement("canvas"), 231 | ctx = canvas.getContext("2d"), 232 | grad; 233 | // the gradient in the legend including the ticks will be 256x15px 234 | canvas.width = "256"; 235 | canvas.height = "15"; 236 | 237 | grad = ctx.createLinearGradient(0,5,256,10); 238 | 239 | for(var i = 0; i < length; i++){ 240 | grad.addColorStop(1/(length-1) * i, gradArr[i].value); 241 | } 242 | 243 | ctx.fillStyle = grad; 244 | ctx.fillRect(0,5,256,10); 245 | ctx.strokeStyle = "black"; 246 | ctx.beginPath(); 247 | 248 | for(var i = 0; i < length; i++){ 249 | ctx.moveTo(((1/(length-1)*i*256) >> 0)+.5, 0); 250 | ctx.lineTo(((1/(length-1)*i*256) >> 0)+.5, (i==0)?15:5); 251 | } 252 | ctx.moveTo(255.5, 0); 253 | ctx.lineTo(255.5, 15); 254 | ctx.moveTo(255.5, 4.5); 255 | ctx.lineTo(0, 4.5); 256 | 257 | ctx.stroke(); 258 | 259 | // we re-use the context for measuring the legends label widths 260 | me.set("ctx", ctx); 261 | 262 | return canvas.toDataURL(); 263 | }, 264 | getElement: function(){ 265 | return this.get("element"); 266 | }, 267 | update: function(max){ 268 | var me = this, 269 | gradient = me.get("gradientArr"), 270 | ctx = me.get("ctx"), 271 | labels = me.get("labelsEl"), 272 | labelText, labelsHtml = "", offset; 273 | 274 | for(var i = 0; i < gradient.length; i++){ 275 | 276 | labelText = max*gradient[i].stop >> 0; 277 | offset = (ctx.measureText(labelText).width/2) >> 0; 278 | 279 | if(i == 0){ 280 | offset = 0; 281 | } 282 | if(i == gradient.length-1){ 283 | offset *= 2; 284 | } 285 | labelsHtml += '
  • '+labelText+'
  • '; 286 | } 287 | labels.innerHTML = labelsHtml; 288 | } 289 | }; 290 | 291 | // heatmap object constructor 292 | var heatmap = function heatmap(config){ 293 | // private variables 294 | var _ = { 295 | radius : 40, 296 | element : {}, 297 | canvas : {}, 298 | acanvas: {}, 299 | ctx : {}, 300 | actx : {}, 301 | legend: null, 302 | visible : true, 303 | width : 0, 304 | height : 0, 305 | max : false, 306 | gradient : false, 307 | opacity: 180, 308 | premultiplyAlpha: false, 309 | bounds: { 310 | l: 1000, 311 | r: 0, 312 | t: 1000, 313 | b: 0 314 | }, 315 | debug: false 316 | }; 317 | // heatmap store containing the datapoints and information about the maximum 318 | // accessible via instance.store 319 | this.store = new store(this); 320 | 321 | this.get = function(key){ 322 | return _[key]; 323 | }; 324 | this.set = function(key, value){ 325 | _[key] = value; 326 | }; 327 | // configure the heatmap when an instance gets created 328 | this.configure(config); 329 | // and initialize it 330 | this.init(); 331 | }; 332 | 333 | // public functions 334 | heatmap.prototype = { 335 | configure: function(config){ 336 | var me = this, 337 | rout, rin; 338 | 339 | me.set("radius", config["radius"] || 40); 340 | me.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element)); 341 | me.set("visible", (config.visible != null)?config.visible:true); 342 | me.set("max", config.max || false); 343 | me.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)"}); // default is the common blue to red gradient 344 | me.set("opacity", parseInt(255/(100/config.opacity), 10) || 180); 345 | me.set("width", config.width || 0); 346 | me.set("height", config.height || 0); 347 | me.set("debug", config.debug); 348 | 349 | if(config.legend){ 350 | var legendCfg = config.legend; 351 | legendCfg.gradient = me.get("gradient"); 352 | me.set("legend", new legend(legendCfg)); 353 | } 354 | 355 | }, 356 | resize: function () { 357 | var me = this, 358 | element = me.get("element"), 359 | canvas = me.get("canvas"), 360 | acanvas = me.get("acanvas"); 361 | canvas.width = acanvas.width = me.get("width") || element.style.width.replace(/px/, "") || me.getWidth(element); 362 | this.set("width", canvas.width); 363 | canvas.height = acanvas.height = me.get("height") || element.style.height.replace(/px/, "") || me.getHeight(element); 364 | this.set("height", canvas.height); 365 | }, 366 | 367 | init: function(){ 368 | var me = this, 369 | canvas = document.createElement("canvas"), 370 | acanvas = document.createElement("canvas"), 371 | ctx = canvas.getContext("2d"), 372 | actx = acanvas.getContext("2d"), 373 | element = me.get("element"); 374 | 375 | 376 | me.initColorPalette(); 377 | 378 | me.set("canvas", canvas); 379 | me.set("ctx", ctx); 380 | me.set("acanvas", acanvas); 381 | me.set("actx", actx); 382 | 383 | me.resize(); 384 | canvas.style.cssText = acanvas.style.cssText = "position:absolute;top:0;left:0;z-index:10000000;"; 385 | 386 | if(!me.get("visible")) 387 | canvas.style.display = "none"; 388 | 389 | element.appendChild(canvas); 390 | if(me.get("legend")){ 391 | element.appendChild(me.get("legend").getElement()); 392 | } 393 | 394 | // debugging purposes only 395 | if(me.get("debug")) 396 | document.body.appendChild(acanvas); 397 | 398 | actx.shadowOffsetX = 15000; 399 | actx.shadowOffsetY = 15000; 400 | actx.shadowBlur = 15; 401 | }, 402 | initColorPalette: function(){ 403 | 404 | var me = this, 405 | canvas = document.createElement("canvas"), 406 | gradient = me.get("gradient"), 407 | ctx, grad, testData; 408 | 409 | canvas.width = "1"; 410 | canvas.height = "256"; 411 | ctx = canvas.getContext("2d"); 412 | grad = ctx.createLinearGradient(0,0,1,256); 413 | 414 | // Test how the browser renders alpha by setting a partially transparent pixel 415 | // and reading the result. A good browser will return a value reasonably close 416 | // to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value. 417 | testData = ctx.getImageData(0,0,1,1); 418 | testData.data[0] = testData.data[3] = 64; // 25% red & alpha 419 | testData.data[1] = testData.data[2] = 0; // 0% blue & green 420 | ctx.putImageData(testData, 0, 0); 421 | testData = ctx.getImageData(0,0,1,1); 422 | me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70)); 423 | 424 | for(var x in gradient){ 425 | grad.addColorStop(x, gradient[x]); 426 | } 427 | 428 | ctx.fillStyle = grad; 429 | ctx.fillRect(0,0,1,256); 430 | 431 | me.set("gradient", ctx.getImageData(0,0,1,256).data); 432 | }, 433 | getWidth: function(element){ 434 | var width = element.offsetWidth; 435 | if(element.style.paddingLeft){ 436 | width+=element.style.paddingLeft; 437 | } 438 | if(element.style.paddingRight){ 439 | width+=element.style.paddingRight; 440 | } 441 | 442 | return width; 443 | }, 444 | getHeight: function(element){ 445 | var height = element.offsetHeight; 446 | if(element.style.paddingTop){ 447 | height+=element.style.paddingTop; 448 | } 449 | if(element.style.paddingBottom){ 450 | height+=element.style.paddingBottom; 451 | } 452 | 453 | return height; 454 | }, 455 | colorize: function(x, y){ 456 | // get the private variables 457 | var me = this, 458 | width = me.get("width"), 459 | radius = me.get("radius"), 460 | height = me.get("height"), 461 | actx = me.get("actx"), 462 | ctx = me.get("ctx"), 463 | x2 = radius * 3, 464 | premultiplyAlpha = me.get("premultiplyAlpha"), 465 | palette = me.get("gradient"), 466 | opacity = me.get("opacity"), 467 | bounds = me.get("bounds"), 468 | left, top, bottom, right, 469 | image, imageData, length, alpha, offset, finalAlpha; 470 | 471 | if(x != null && y != null){ 472 | if(x+x2>width){ 473 | x=width-x2; 474 | } 475 | if(x<0){ 476 | x=0; 477 | } 478 | if(y<0){ 479 | y=0; 480 | } 481 | if(y+x2>height){ 482 | y=height-x2; 483 | } 484 | left = x; 485 | top = y; 486 | right = x + x2; 487 | bottom = y + x2; 488 | 489 | }else{ 490 | if(bounds['l'] < 0){ 491 | left = 0; 492 | }else{ 493 | left = bounds['l']; 494 | } 495 | if(bounds['r'] > width){ 496 | right = width; 497 | }else{ 498 | right = bounds['r']; 499 | } 500 | if(bounds['t'] < 0){ 501 | top = 0; 502 | }else{ 503 | top = bounds['t']; 504 | } 505 | if(bounds['b'] > height){ 506 | bottom = height; 507 | }else{ 508 | bottom = bounds['b']; 509 | } 510 | } 511 | 512 | image = actx.getImageData(left, top, right-left, bottom-top); 513 | imageData = image.data; 514 | length = imageData.length; 515 | // loop thru the area 516 | for(var i=3; i < length; i+=4){ 517 | 518 | // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha 519 | alpha = imageData[i], 520 | offset = alpha*4; 521 | 522 | if(!offset) 523 | continue; 524 | 525 | // we ve started with i=3 526 | // set the new r, g and b values 527 | finalAlpha = (alpha < opacity)?alpha:opacity; 528 | imageData[i-3]=palette[offset]; 529 | imageData[i-2]=palette[offset+1]; 530 | imageData[i-1]=palette[offset+2]; 531 | 532 | if (premultiplyAlpha) { 533 | // To fix browsers that premultiply incorrectly, we'll pass in a value scaled 534 | // appropriately so when the multiplication happens the correct value will result. 535 | imageData[i-3] /= 255/finalAlpha; 536 | imageData[i-2] /= 255/finalAlpha; 537 | imageData[i-1] /= 255/finalAlpha; 538 | } 539 | 540 | // we want the heatmap to have a gradient from transparent to the colors 541 | // as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value 542 | imageData[i] = finalAlpha; 543 | } 544 | // the rgb data manipulation didn't affect the ImageData object(defined on the top) 545 | // after the manipulation process we have to set the manipulated data to the ImageData object 546 | image.data = imageData; 547 | ctx.putImageData(image, left, top); 548 | }, 549 | drawAlpha: function(x, y, count, colorize){ 550 | // storing the variables because they will be often used 551 | var me = this, 552 | radius = me.get("radius"), 553 | ctx = me.get("actx"), 554 | max = me.get("max"), 555 | bounds = me.get("bounds"), 556 | xb = x - (1.5 * radius) >> 0, yb = y - (1.5 * radius) >> 0, 557 | xc = x + (1.5 * radius) >> 0, yc = y + (1.5 * radius) >> 0; 558 | 559 | ctx.shadowColor = ('rgba(0,0,0,'+((count)?(count/me.store.max):'0.1')+')'); 560 | 561 | ctx.shadowOffsetX = 15000; 562 | ctx.shadowOffsetY = 15000; 563 | ctx.shadowBlur = 15; 564 | 565 | ctx.beginPath(); 566 | ctx.arc(x - 15000, y - 15000, radius, 0, Math.PI * 2, true); 567 | ctx.closePath(); 568 | ctx.fill(); 569 | if(colorize){ 570 | // finally colorize the area 571 | me.colorize(xb,yb); 572 | }else{ 573 | // or update the boundaries for the area that then should be colorized 574 | if(xb < bounds["l"]){ 575 | bounds["l"] = xb; 576 | } 577 | if(yb < bounds["t"]){ 578 | bounds["t"] = yb; 579 | } 580 | if(xc > bounds['r']){ 581 | bounds['r'] = xc; 582 | } 583 | if(yc > bounds['b']){ 584 | bounds['b'] = yc; 585 | } 586 | } 587 | }, 588 | toggleDisplay: function(){ 589 | var me = this, 590 | visible = me.get("visible"), 591 | canvas = me.get("canvas"); 592 | 593 | if(!visible) 594 | canvas.style.display = "block"; 595 | else 596 | canvas.style.display = "none"; 597 | 598 | me.set("visible", !visible); 599 | }, 600 | // dataURL export 601 | getImageData: function(){ 602 | return this.get("canvas").toDataURL(); 603 | }, 604 | clear: function(){ 605 | var me = this, 606 | w = me.get("width"), 607 | h = me.get("height"); 608 | 609 | me.store.set("data",[]); 610 | // @TODO: reset stores max to 1 611 | //me.store.max = 1; 612 | me.get("ctx").clearRect(0,0,w,h); 613 | me.get("actx").clearRect(0,0,w,h); 614 | }, 615 | cleanup: function(){ 616 | var me = this; 617 | me.get("element").removeChild(me.get("canvas")); 618 | } 619 | }; 620 | 621 | return { 622 | create: function(config){ 623 | return new heatmap(config); 624 | }, 625 | util: { 626 | mousePosition: function(ev){ 627 | // this doesn't work right 628 | // rather use 629 | /* 630 | // this = element to observe 631 | var x = ev.pageX - this.offsetLeft; 632 | var y = ev.pageY - this.offsetTop; 633 | 634 | */ 635 | var x, y; 636 | 637 | if (ev.layerX) { // Firefox 638 | x = ev.layerX; 639 | y = ev.layerY; 640 | } else if (ev.offsetX) { // Opera 641 | x = ev.offsetX; 642 | y = ev.offsetY; 643 | } 644 | if(typeof(x)=='undefined') 645 | return; 646 | 647 | return [x,y]; 648 | } 649 | } 650 | }; 651 | })(); 652 | w.h337 = w.heatmapFactory = heatmapFactory; 653 | })(window); 654 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "1e73249f157f5789fd2fe12a0a9a1328", 8 | "packages": [ 9 | { 10 | "name": "guzzlehttp/guzzle", 11 | "version": "7.8.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/guzzle/guzzle.git", 15 | "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", 20 | "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-json": "*", 25 | "guzzlehttp/promises": "^1.5.3 || ^2.0.1", 26 | "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", 27 | "php": "^7.2.5 || ^8.0", 28 | "psr/http-client": "^1.0", 29 | "symfony/deprecation-contracts": "^2.2 || ^3.0" 30 | }, 31 | "provide": { 32 | "psr/http-client-implementation": "1.0" 33 | }, 34 | "require-dev": { 35 | "bamarni/composer-bin-plugin": "^1.8.2", 36 | "ext-curl": "*", 37 | "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", 38 | "php-http/message-factory": "^1.1", 39 | "phpunit/phpunit": "^8.5.36 || ^9.6.15", 40 | "psr/log": "^1.1 || ^2.0 || ^3.0" 41 | }, 42 | "suggest": { 43 | "ext-curl": "Required for CURL handler support", 44 | "ext-intl": "Required for Internationalized Domain Name (IDN) support", 45 | "psr/log": "Required for using the Log middleware" 46 | }, 47 | "type": "library", 48 | "extra": { 49 | "bamarni-bin": { 50 | "bin-links": true, 51 | "forward-command": false 52 | } 53 | }, 54 | "autoload": { 55 | "files": [ 56 | "src/functions_include.php" 57 | ], 58 | "psr-4": { 59 | "GuzzleHttp\\": "src/" 60 | } 61 | }, 62 | "notification-url": "https://packagist.org/downloads/", 63 | "license": [ 64 | "MIT" 65 | ], 66 | "authors": [ 67 | { 68 | "name": "Graham Campbell", 69 | "email": "hello@gjcampbell.co.uk", 70 | "homepage": "https://github.com/GrahamCampbell" 71 | }, 72 | { 73 | "name": "Michael Dowling", 74 | "email": "mtdowling@gmail.com", 75 | "homepage": "https://github.com/mtdowling" 76 | }, 77 | { 78 | "name": "Jeremy Lindblom", 79 | "email": "jeremeamia@gmail.com", 80 | "homepage": "https://github.com/jeremeamia" 81 | }, 82 | { 83 | "name": "George Mponos", 84 | "email": "gmponos@gmail.com", 85 | "homepage": "https://github.com/gmponos" 86 | }, 87 | { 88 | "name": "Tobias Nyholm", 89 | "email": "tobias.nyholm@gmail.com", 90 | "homepage": "https://github.com/Nyholm" 91 | }, 92 | { 93 | "name": "Márk Sági-Kazár", 94 | "email": "mark.sagikazar@gmail.com", 95 | "homepage": "https://github.com/sagikazarmark" 96 | }, 97 | { 98 | "name": "Tobias Schultze", 99 | "email": "webmaster@tubo-world.de", 100 | "homepage": "https://github.com/Tobion" 101 | } 102 | ], 103 | "description": "Guzzle is a PHP HTTP client library", 104 | "keywords": [ 105 | "client", 106 | "curl", 107 | "framework", 108 | "http", 109 | "http client", 110 | "psr-18", 111 | "psr-7", 112 | "rest", 113 | "web service" 114 | ], 115 | "support": { 116 | "issues": "https://github.com/guzzle/guzzle/issues", 117 | "source": "https://github.com/guzzle/guzzle/tree/7.8.1" 118 | }, 119 | "funding": [ 120 | { 121 | "url": "https://github.com/GrahamCampbell", 122 | "type": "github" 123 | }, 124 | { 125 | "url": "https://github.com/Nyholm", 126 | "type": "github" 127 | }, 128 | { 129 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", 130 | "type": "tidelift" 131 | } 132 | ], 133 | "time": "2023-12-03T20:35:24+00:00" 134 | }, 135 | { 136 | "name": "guzzlehttp/promises", 137 | "version": "2.0.2", 138 | "source": { 139 | "type": "git", 140 | "url": "https://github.com/guzzle/promises.git", 141 | "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" 142 | }, 143 | "dist": { 144 | "type": "zip", 145 | "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", 146 | "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", 147 | "shasum": "" 148 | }, 149 | "require": { 150 | "php": "^7.2.5 || ^8.0" 151 | }, 152 | "require-dev": { 153 | "bamarni/composer-bin-plugin": "^1.8.2", 154 | "phpunit/phpunit": "^8.5.36 || ^9.6.15" 155 | }, 156 | "type": "library", 157 | "extra": { 158 | "bamarni-bin": { 159 | "bin-links": true, 160 | "forward-command": false 161 | } 162 | }, 163 | "autoload": { 164 | "psr-4": { 165 | "GuzzleHttp\\Promise\\": "src/" 166 | } 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "authors": [ 173 | { 174 | "name": "Graham Campbell", 175 | "email": "hello@gjcampbell.co.uk", 176 | "homepage": "https://github.com/GrahamCampbell" 177 | }, 178 | { 179 | "name": "Michael Dowling", 180 | "email": "mtdowling@gmail.com", 181 | "homepage": "https://github.com/mtdowling" 182 | }, 183 | { 184 | "name": "Tobias Nyholm", 185 | "email": "tobias.nyholm@gmail.com", 186 | "homepage": "https://github.com/Nyholm" 187 | }, 188 | { 189 | "name": "Tobias Schultze", 190 | "email": "webmaster@tubo-world.de", 191 | "homepage": "https://github.com/Tobion" 192 | } 193 | ], 194 | "description": "Guzzle promises library", 195 | "keywords": [ 196 | "promise" 197 | ], 198 | "support": { 199 | "issues": "https://github.com/guzzle/promises/issues", 200 | "source": "https://github.com/guzzle/promises/tree/2.0.2" 201 | }, 202 | "funding": [ 203 | { 204 | "url": "https://github.com/GrahamCampbell", 205 | "type": "github" 206 | }, 207 | { 208 | "url": "https://github.com/Nyholm", 209 | "type": "github" 210 | }, 211 | { 212 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", 213 | "type": "tidelift" 214 | } 215 | ], 216 | "time": "2023-12-03T20:19:20+00:00" 217 | }, 218 | { 219 | "name": "guzzlehttp/psr7", 220 | "version": "2.6.2", 221 | "source": { 222 | "type": "git", 223 | "url": "https://github.com/guzzle/psr7.git", 224 | "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" 225 | }, 226 | "dist": { 227 | "type": "zip", 228 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", 229 | "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", 230 | "shasum": "" 231 | }, 232 | "require": { 233 | "php": "^7.2.5 || ^8.0", 234 | "psr/http-factory": "^1.0", 235 | "psr/http-message": "^1.1 || ^2.0", 236 | "ralouphie/getallheaders": "^3.0" 237 | }, 238 | "provide": { 239 | "psr/http-factory-implementation": "1.0", 240 | "psr/http-message-implementation": "1.0" 241 | }, 242 | "require-dev": { 243 | "bamarni/composer-bin-plugin": "^1.8.2", 244 | "http-interop/http-factory-tests": "^0.9", 245 | "phpunit/phpunit": "^8.5.36 || ^9.6.15" 246 | }, 247 | "suggest": { 248 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" 249 | }, 250 | "type": "library", 251 | "extra": { 252 | "bamarni-bin": { 253 | "bin-links": true, 254 | "forward-command": false 255 | } 256 | }, 257 | "autoload": { 258 | "psr-4": { 259 | "GuzzleHttp\\Psr7\\": "src/" 260 | } 261 | }, 262 | "notification-url": "https://packagist.org/downloads/", 263 | "license": [ 264 | "MIT" 265 | ], 266 | "authors": [ 267 | { 268 | "name": "Graham Campbell", 269 | "email": "hello@gjcampbell.co.uk", 270 | "homepage": "https://github.com/GrahamCampbell" 271 | }, 272 | { 273 | "name": "Michael Dowling", 274 | "email": "mtdowling@gmail.com", 275 | "homepage": "https://github.com/mtdowling" 276 | }, 277 | { 278 | "name": "George Mponos", 279 | "email": "gmponos@gmail.com", 280 | "homepage": "https://github.com/gmponos" 281 | }, 282 | { 283 | "name": "Tobias Nyholm", 284 | "email": "tobias.nyholm@gmail.com", 285 | "homepage": "https://github.com/Nyholm" 286 | }, 287 | { 288 | "name": "Márk Sági-Kazár", 289 | "email": "mark.sagikazar@gmail.com", 290 | "homepage": "https://github.com/sagikazarmark" 291 | }, 292 | { 293 | "name": "Tobias Schultze", 294 | "email": "webmaster@tubo-world.de", 295 | "homepage": "https://github.com/Tobion" 296 | }, 297 | { 298 | "name": "Márk Sági-Kazár", 299 | "email": "mark.sagikazar@gmail.com", 300 | "homepage": "https://sagikazarmark.hu" 301 | } 302 | ], 303 | "description": "PSR-7 message implementation that also provides common utility methods", 304 | "keywords": [ 305 | "http", 306 | "message", 307 | "psr-7", 308 | "request", 309 | "response", 310 | "stream", 311 | "uri", 312 | "url" 313 | ], 314 | "support": { 315 | "issues": "https://github.com/guzzle/psr7/issues", 316 | "source": "https://github.com/guzzle/psr7/tree/2.6.2" 317 | }, 318 | "funding": [ 319 | { 320 | "url": "https://github.com/GrahamCampbell", 321 | "type": "github" 322 | }, 323 | { 324 | "url": "https://github.com/Nyholm", 325 | "type": "github" 326 | }, 327 | { 328 | "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", 329 | "type": "tidelift" 330 | } 331 | ], 332 | "time": "2023-12-03T20:05:35+00:00" 333 | }, 334 | { 335 | "name": "jbelien/oauth2-openstreetmap", 336 | "version": "0.1.2", 337 | "source": { 338 | "type": "git", 339 | "url": "https://github.com/jbelien/oauth2-openstreetmap.git", 340 | "reference": "84c992990a317cdfcdfb41dbce548273a502ed44" 341 | }, 342 | "dist": { 343 | "type": "zip", 344 | "url": "https://api.github.com/repos/jbelien/oauth2-openstreetmap/zipball/84c992990a317cdfcdfb41dbce548273a502ed44", 345 | "reference": "84c992990a317cdfcdfb41dbce548273a502ed44", 346 | "shasum": "" 347 | }, 348 | "require": { 349 | "league/oauth2-client": "^2.6" 350 | }, 351 | "require-dev": { 352 | "mockery/mockery": "^1.4", 353 | "phpunit/phpunit": "^9.5", 354 | "squizlabs/php_codesniffer": "^3.6" 355 | }, 356 | "type": "library", 357 | "autoload": { 358 | "psr-4": { 359 | "JBelien\\OAuth2\\Client\\": "src/" 360 | } 361 | }, 362 | "notification-url": "https://packagist.org/downloads/", 363 | "license": [ 364 | "MIT" 365 | ], 366 | "description": "OpenStreetMap OAuth 2.0 support for the PHP League's OAuth 2.0 Client ", 367 | "keywords": [ 368 | "OpenStreetMap", 369 | "authorisation", 370 | "authorization", 371 | "client", 372 | "oauth", 373 | "oauth2", 374 | "osm" 375 | ], 376 | "support": { 377 | "issues": "https://github.com/jbelien/oauth2-openstreetmap/issues", 378 | "source": "https://github.com/jbelien/oauth2-openstreetmap/tree/0.1.2" 379 | }, 380 | "time": "2021-10-18T09:46:22+00:00" 381 | }, 382 | { 383 | "name": "league/oauth2-client", 384 | "version": "2.7.0", 385 | "source": { 386 | "type": "git", 387 | "url": "https://github.com/thephpleague/oauth2-client.git", 388 | "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" 389 | }, 390 | "dist": { 391 | "type": "zip", 392 | "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", 393 | "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", 394 | "shasum": "" 395 | }, 396 | "require": { 397 | "guzzlehttp/guzzle": "^6.0 || ^7.0", 398 | "paragonie/random_compat": "^1 || ^2 || ^9.99", 399 | "php": "^5.6 || ^7.0 || ^8.0" 400 | }, 401 | "require-dev": { 402 | "mockery/mockery": "^1.3.5", 403 | "php-parallel-lint/php-parallel-lint": "^1.3.1", 404 | "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", 405 | "squizlabs/php_codesniffer": "^2.3 || ^3.0" 406 | }, 407 | "type": "library", 408 | "extra": { 409 | "branch-alias": { 410 | "dev-2.x": "2.0.x-dev" 411 | } 412 | }, 413 | "autoload": { 414 | "psr-4": { 415 | "League\\OAuth2\\Client\\": "src/" 416 | } 417 | }, 418 | "notification-url": "https://packagist.org/downloads/", 419 | "license": [ 420 | "MIT" 421 | ], 422 | "authors": [ 423 | { 424 | "name": "Alex Bilbie", 425 | "email": "hello@alexbilbie.com", 426 | "homepage": "http://www.alexbilbie.com", 427 | "role": "Developer" 428 | }, 429 | { 430 | "name": "Woody Gilk", 431 | "homepage": "https://github.com/shadowhand", 432 | "role": "Contributor" 433 | } 434 | ], 435 | "description": "OAuth 2.0 Client Library", 436 | "keywords": [ 437 | "Authentication", 438 | "SSO", 439 | "authorization", 440 | "identity", 441 | "idp", 442 | "oauth", 443 | "oauth2", 444 | "single sign on" 445 | ], 446 | "support": { 447 | "issues": "https://github.com/thephpleague/oauth2-client/issues", 448 | "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" 449 | }, 450 | "time": "2023-04-16T18:19:15+00:00" 451 | }, 452 | { 453 | "name": "paragonie/random_compat", 454 | "version": "v9.99.100", 455 | "source": { 456 | "type": "git", 457 | "url": "https://github.com/paragonie/random_compat.git", 458 | "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" 459 | }, 460 | "dist": { 461 | "type": "zip", 462 | "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", 463 | "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", 464 | "shasum": "" 465 | }, 466 | "require": { 467 | "php": ">= 7" 468 | }, 469 | "require-dev": { 470 | "phpunit/phpunit": "4.*|5.*", 471 | "vimeo/psalm": "^1" 472 | }, 473 | "suggest": { 474 | "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 475 | }, 476 | "type": "library", 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "MIT" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "Paragon Initiative Enterprises", 484 | "email": "security@paragonie.com", 485 | "homepage": "https://paragonie.com" 486 | } 487 | ], 488 | "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 489 | "keywords": [ 490 | "csprng", 491 | "polyfill", 492 | "pseudorandom", 493 | "random" 494 | ], 495 | "support": { 496 | "email": "info@paragonie.com", 497 | "issues": "https://github.com/paragonie/random_compat/issues", 498 | "source": "https://github.com/paragonie/random_compat" 499 | }, 500 | "time": "2020-10-15T08:29:30+00:00" 501 | }, 502 | { 503 | "name": "psr/http-client", 504 | "version": "1.0.3", 505 | "source": { 506 | "type": "git", 507 | "url": "https://github.com/php-fig/http-client.git", 508 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" 509 | }, 510 | "dist": { 511 | "type": "zip", 512 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", 513 | "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", 514 | "shasum": "" 515 | }, 516 | "require": { 517 | "php": "^7.0 || ^8.0", 518 | "psr/http-message": "^1.0 || ^2.0" 519 | }, 520 | "type": "library", 521 | "extra": { 522 | "branch-alias": { 523 | "dev-master": "1.0.x-dev" 524 | } 525 | }, 526 | "autoload": { 527 | "psr-4": { 528 | "Psr\\Http\\Client\\": "src/" 529 | } 530 | }, 531 | "notification-url": "https://packagist.org/downloads/", 532 | "license": [ 533 | "MIT" 534 | ], 535 | "authors": [ 536 | { 537 | "name": "PHP-FIG", 538 | "homepage": "https://www.php-fig.org/" 539 | } 540 | ], 541 | "description": "Common interface for HTTP clients", 542 | "homepage": "https://github.com/php-fig/http-client", 543 | "keywords": [ 544 | "http", 545 | "http-client", 546 | "psr", 547 | "psr-18" 548 | ], 549 | "support": { 550 | "source": "https://github.com/php-fig/http-client" 551 | }, 552 | "time": "2023-09-23T14:17:50+00:00" 553 | }, 554 | { 555 | "name": "psr/http-factory", 556 | "version": "1.1.0", 557 | "source": { 558 | "type": "git", 559 | "url": "https://github.com/php-fig/http-factory.git", 560 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" 561 | }, 562 | "dist": { 563 | "type": "zip", 564 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 565 | "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", 566 | "shasum": "" 567 | }, 568 | "require": { 569 | "php": ">=7.1", 570 | "psr/http-message": "^1.0 || ^2.0" 571 | }, 572 | "type": "library", 573 | "extra": { 574 | "branch-alias": { 575 | "dev-master": "1.0.x-dev" 576 | } 577 | }, 578 | "autoload": { 579 | "psr-4": { 580 | "Psr\\Http\\Message\\": "src/" 581 | } 582 | }, 583 | "notification-url": "https://packagist.org/downloads/", 584 | "license": [ 585 | "MIT" 586 | ], 587 | "authors": [ 588 | { 589 | "name": "PHP-FIG", 590 | "homepage": "https://www.php-fig.org/" 591 | } 592 | ], 593 | "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", 594 | "keywords": [ 595 | "factory", 596 | "http", 597 | "message", 598 | "psr", 599 | "psr-17", 600 | "psr-7", 601 | "request", 602 | "response" 603 | ], 604 | "support": { 605 | "source": "https://github.com/php-fig/http-factory" 606 | }, 607 | "time": "2024-04-15T12:06:14+00:00" 608 | }, 609 | { 610 | "name": "psr/http-message", 611 | "version": "2.0", 612 | "source": { 613 | "type": "git", 614 | "url": "https://github.com/php-fig/http-message.git", 615 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" 616 | }, 617 | "dist": { 618 | "type": "zip", 619 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", 620 | "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", 621 | "shasum": "" 622 | }, 623 | "require": { 624 | "php": "^7.2 || ^8.0" 625 | }, 626 | "type": "library", 627 | "extra": { 628 | "branch-alias": { 629 | "dev-master": "2.0.x-dev" 630 | } 631 | }, 632 | "autoload": { 633 | "psr-4": { 634 | "Psr\\Http\\Message\\": "src/" 635 | } 636 | }, 637 | "notification-url": "https://packagist.org/downloads/", 638 | "license": [ 639 | "MIT" 640 | ], 641 | "authors": [ 642 | { 643 | "name": "PHP-FIG", 644 | "homepage": "https://www.php-fig.org/" 645 | } 646 | ], 647 | "description": "Common interface for HTTP messages", 648 | "homepage": "https://github.com/php-fig/http-message", 649 | "keywords": [ 650 | "http", 651 | "http-message", 652 | "psr", 653 | "psr-7", 654 | "request", 655 | "response" 656 | ], 657 | "support": { 658 | "source": "https://github.com/php-fig/http-message/tree/2.0" 659 | }, 660 | "time": "2023-04-04T09:54:51+00:00" 661 | }, 662 | { 663 | "name": "ralouphie/getallheaders", 664 | "version": "3.0.3", 665 | "source": { 666 | "type": "git", 667 | "url": "https://github.com/ralouphie/getallheaders.git", 668 | "reference": "120b605dfeb996808c31b6477290a714d356e822" 669 | }, 670 | "dist": { 671 | "type": "zip", 672 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 673 | "reference": "120b605dfeb996808c31b6477290a714d356e822", 674 | "shasum": "" 675 | }, 676 | "require": { 677 | "php": ">=5.6" 678 | }, 679 | "require-dev": { 680 | "php-coveralls/php-coveralls": "^2.1", 681 | "phpunit/phpunit": "^5 || ^6.5" 682 | }, 683 | "type": "library", 684 | "autoload": { 685 | "files": [ 686 | "src/getallheaders.php" 687 | ] 688 | }, 689 | "notification-url": "https://packagist.org/downloads/", 690 | "license": [ 691 | "MIT" 692 | ], 693 | "authors": [ 694 | { 695 | "name": "Ralph Khattar", 696 | "email": "ralph.khattar@gmail.com" 697 | } 698 | ], 699 | "description": "A polyfill for getallheaders.", 700 | "support": { 701 | "issues": "https://github.com/ralouphie/getallheaders/issues", 702 | "source": "https://github.com/ralouphie/getallheaders/tree/develop" 703 | }, 704 | "time": "2019-03-08T08:55:37+00:00" 705 | }, 706 | { 707 | "name": "symfony/deprecation-contracts", 708 | "version": "v3.5.0", 709 | "source": { 710 | "type": "git", 711 | "url": "https://github.com/symfony/deprecation-contracts.git", 712 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" 713 | }, 714 | "dist": { 715 | "type": "zip", 716 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", 717 | "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", 718 | "shasum": "" 719 | }, 720 | "require": { 721 | "php": ">=8.1" 722 | }, 723 | "type": "library", 724 | "extra": { 725 | "branch-alias": { 726 | "dev-main": "3.5-dev" 727 | }, 728 | "thanks": { 729 | "name": "symfony/contracts", 730 | "url": "https://github.com/symfony/contracts" 731 | } 732 | }, 733 | "autoload": { 734 | "files": [ 735 | "function.php" 736 | ] 737 | }, 738 | "notification-url": "https://packagist.org/downloads/", 739 | "license": [ 740 | "MIT" 741 | ], 742 | "authors": [ 743 | { 744 | "name": "Nicolas Grekas", 745 | "email": "p@tchwork.com" 746 | }, 747 | { 748 | "name": "Symfony Community", 749 | "homepage": "https://symfony.com/contributors" 750 | } 751 | ], 752 | "description": "A generic function and convention to trigger deprecation notices", 753 | "homepage": "https://symfony.com", 754 | "support": { 755 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" 756 | }, 757 | "funding": [ 758 | { 759 | "url": "https://symfony.com/sponsor", 760 | "type": "custom" 761 | }, 762 | { 763 | "url": "https://github.com/fabpot", 764 | "type": "github" 765 | }, 766 | { 767 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 768 | "type": "tidelift" 769 | } 770 | ], 771 | "time": "2024-04-18T09:32:20+00:00" 772 | } 773 | ], 774 | "packages-dev": [], 775 | "aliases": [], 776 | "minimum-stability": "stable", 777 | "stability-flags": [], 778 | "prefer-stable": false, 779 | "prefer-lowest": false, 780 | "platform": [], 781 | "platform-dev": [], 782 | "plugin-api-version": "2.6.0" 783 | } 784 | --------------------------------------------------------------------------------