From c61e2d9cc9f868d4f7962a5d1db068e07ede6893 Mon Sep 17 00:00:00 2001 From: David Molineus Date: Mon, 14 Nov 2016 11:00:10 +0100 Subject: [PATCH] Refactor overpass support using own implementation of overpass layer. --- assets/maps/contao-leaflet.js | 2 +- assets/maps/src/Mixin.Map.js | 36 +-- assets/maps/src/OverpassLayer.js | 215 ++++++++++++++++++ module/config/config.php | 6 +- module/dca/tl_leaflet_layer.php | 8 +- .../Definition/Layer/OverpassLayer.php | 190 ++++++++++++++++ .../Mapper/Layer/OverpassLayerMapper.php | 27 +-- 7 files changed, 447 insertions(+), 37 deletions(-) create mode 100644 assets/maps/src/OverpassLayer.js create mode 100644 src/Netzmacht/Contao/Leaflet/Definition/Layer/OverpassLayer.php diff --git a/assets/maps/contao-leaflet.js b/assets/maps/contao-leaflet.js index fd25c46..c6ac251 100644 --- a/assets/maps/contao-leaflet.js +++ b/assets/maps/contao-leaflet.js @@ -1 +1 @@ -L.Contao=L.Class.extend({includes:L.Mixin.Events,statics:{ATTRIBUTION:' | netzmacht'},maps:{},icons:{},initialize:function(){L.Icon.Default.imagePath="assets/leaflet/libs/leaflet/images/",this.setGeoJsonListeners(L.GeoJSON)},addMap:function(t,o){return this.maps[t]=o,this.fire("map:added",{id:t,map:o}),this},getMap:function(t){return"undefined"==typeof this.maps[t]?null:this.maps[t]},addIcon:function(t,o){return this.icons[t]=o,this.fire("icon:added",{id:t,icon:o}),this},loadIcons:function(t){for(var o=0;onetzmacht'},maps:{},icons:{},initialize:function(){L.Icon.Default.imagePath="assets/leaflet/libs/leaflet/images/",this.setGeoJsonListeners(L.GeoJSON)},addMap:function(t,o){return this.maps[t]=o,this.fire("map:added",{id:t,map:o}),this},getMap:function(t){return"undefined"==typeof this.maps[t]?null:this.maps[t]},addIcon:function(t,o){return this.icons[t]=o,this.fire("icon:added",{id:t,icon:o}),this},loadIcons:function(t){for(var o=0;o=200&&t<300||304===t}function i(){void 0===a.status||n(a.status)?o.call(a,null,a):o.call(a,a,null)}var s=!1;if("undefined"==typeof window.XMLHttpRequest)return o(Error("Browser not supported"));if("undefined"==typeof e){var r=t.match(/^\s*https?:\/\/[^\/]*/);e=r&&r[0]!==location.protocol+"//"+location.hostname+(location.port?":"+location.port:"")}var a=new window.XMLHttpRequest;if(e&&!("withCredentials"in a)){a=new window.XDomainRequest;var p=o;o=function(){if(s)p.apply(this,arguments);else{var t=this,o=arguments;setTimeout(function(){p.apply(t,o)},0)}}}return"onload"in a?a.onload=i:a.onreadystatechange=function(){4===a.readyState&&i()},a.onerror=function(t){o.call(this,t||!0,null),o=function(){}},a.onprogress=function(){},a.ontimeout=function(t){o.call(this,t,null),o=function(){}},a.onabort=function(t){o.call(this,t,null),o=function(){}},a.open("GET",t,!0),a.send(null),s=!0,a}}); \ No newline at end of file diff --git a/assets/maps/src/Mixin.Map.js b/assets/maps/src/Mixin.Map.js index e1b8268..110ff44 100644 --- a/assets/maps/src/Mixin.Map.js +++ b/assets/maps/src/Mixin.Map.js @@ -30,23 +30,31 @@ L.Map.include({ } if (this._dynamicBounds) { - options = {}; - - if (this.options.boundsPadding) { - options.padding = this.options.boundsPadding; - } else { - if (this.options.boundsPaddingTopLeft) { - options.paddingTopLeft = this.options.boundsPaddingTopLeft; - } - if (this.options.boundsPaddingBottomRight) { - options.paddingBottomRight = this.options.boundsPaddingBottomRight; - } - } - - this.fitBounds(this._dynamicBounds, options); + this.fitBounds(this._dynamicBounds, this.getBoundsOptions()); } }, + /** + * Get the bounds optons + * @returns {{}} + */ + getBoundsOptions: function () { + options = {}; + + if (this.options.boundsPadding) { + options.padding = this.options.boundsPadding; + } else { + if (this.options.boundsPaddingTopLeft) { + options.paddingTopLeft = this.options.boundsPaddingTopLeft; + } + if (this.options.boundsPaddingBottomRight) { + options.paddingBottomRight = this.options.boundsPaddingBottomRight; + } + } + + return options; + }, + /** * Scan recursively for bounds in a layer and extend _dynamicBounds if any found. * diff --git a/assets/maps/src/OverpassLayer.js b/assets/maps/src/OverpassLayer.js new file mode 100644 index 0000000..2018903 --- /dev/null +++ b/assets/maps/src/OverpassLayer.js @@ -0,0 +1,215 @@ +/** + * Get the bounds as overpass bbox string. + * + * @returns {string} + */ +L.LatLngBounds.prototype.toOverpassBBoxString = function () { + var a = this._southWest, + b = this._northEast; + + return [a.lat, a.lng, b.lat, b.lng].join(","); +}; + +/** + * Implementation of the overpass layer. Heavily inspired by + * https://github.com/kartenkarsten/leaflet-layer-overpass. + */ +L.OverPassLayer = L.FeatureGroup.extend({ + options: { + minZoom: 0, + endpoint: '//overpass-api.de/api/', + query: '(node(BBOX)[organic];node(BBOX)[second_hand];);out qt;' + }, + /** + * Initialize the layer. + * + * @param options + */ + initialize: function (options) { + L.Util.setOptions(this, options); + + this.options.pointToLayer = this.pointToLayer; + this.options.onEachFeature = this.onEachFeature; + this.options.dynamicLoad = this.options.query.match(/BBOX/g) ? true : false; + + this._layer = L.geoJson(); + this._layers = {}; + + this.addLayer(this._layer); + }, + /** + * Refresh the data of the layer. + * + * TODO: Implement some caching. + */ + refreshData: function () { + if (this._map.getZoom() < this.options.minZoom) { + return; + } + + var bounds = this._map.getBounds().toOverpassBBoxString(); + var query = this.options.query.replace(/(BBOX)/g, bounds); + var url = this.options.endpoint + "interpreter?data=[out:json];" + query; + + this._map.fire('dataloading', {layer: this}); + + this.request(url, function (error, response) { + var data = JSON.parse(response.response); + var features = osmtogeojson(data); + var layer = L.geoJson(features, { + pointToLayer: this.options.pointToLayer.bind(this), + onEachFeature: this.options.onEachFeature.bind(this) + }); + + this.addLayer(layer); + this.removeLayer(this._layer); + this._layer = layer; + + if (this.options.boundsMode === 'extend' && layer.getBounds().isValid()) { + var bounds = this._map.getBounds(); + bounds = bounds.extend(layer.getBounds()); + + this._map.fitBounds(bounds, this._map.getBoundsOptions()); + } + + this._map.fire('dataload', {layer: this}); + }.bind(this)); + }, + /** + * @param map + */ + onAdd: function (map) { + if (this.options.boundsMode === 'fit' && this.options.dynamicLoad) { + map.on('moveend', this.refreshData, this); + } + + this.refreshData(); + }, + pointToLayer: function (feature, latlng) { + var type = 'marker'; + var marker = L.marker(latlng, feature.properties.options); + + if (feature.properties) { + if (feature.properties.radius) { + marker.setRadius(feature.properties.radius); + } + + if (feature.properties.icon) { + var icon = this._map.getIcon(feature.properties.icon); + + if (icon) { + marker.setIcon(icon); + } + } + + L.contao.bindPopupFromFeature(marker, feature); + } + + this._map.fire('point:added', {marker: marker, feature: feature, latlng: latlng, type: type}); + + return marker; + }, + onEachFeature: function (feature, layer) { + if (feature.properties) { + L.Util.setOptions(layer, feature.properties.options); + + L.contao.bindPopupFromFeature(layer, feature); + + this._map.fire('feature:added', {feature: feature, layer: layer}); + } + }, + /** + * Make an ajax request. Clone of corslite from MapQuest. + */ + request: function (url, callback, cors) { + var sent = false; + + if (typeof window.XMLHttpRequest === 'undefined') { + return callback(Error('Browser not supported')); + } + + if (typeof cors === 'undefined') { + var m = url.match(/^\s*https?:\/\/[^\/]*/); + cors = m && (m[0] !== location.protocol + '//' + location.hostname + + (location.port ? ':' + location.port : '')); + } + + var x = new window.XMLHttpRequest(); + + function isSuccessful(status) { + return status >= 200 && status < 300 || status === 304; + } + + if (cors && !('withCredentials' in x)) { + // IE8-9 + x = new window.XDomainRequest(); + + // Ensure callback is never called synchronously, i.e., before + // x.send() returns (this has been observed in the wild). + // See https://github.com/mapbox/mapbox.js/issues/472 + var original = callback; + callback = function() { + if (sent) { + original.apply(this, arguments); + } else { + var that = this, args = arguments; + setTimeout(function() { + original.apply(that, args); + }, 0); + } + } + } + + function loaded() { + if ( + // XDomainRequest + x.status === undefined || + // modern browsers + isSuccessful(x.status)) callback.call(x, null, x); + else callback.call(x, x, null); + } + + // Both `onreadystatechange` and `onload` can fire. `onreadystatechange` + // has [been supported for longer](http://stackoverflow.com/a/9181508/229001). + if ('onload' in x) { + x.onload = loaded; + } else { + x.onreadystatechange = function readystate() { + if (x.readyState === 4) { + loaded(); + } + }; + } + + // Call the callback with the XMLHttpRequest object as an error and prevent + // it from ever being called again by reassigning it to `noop` + x.onerror = function error(evt) { + // XDomainRequest provides no evt parameter + callback.call(this, evt || true, null); + callback = function() { }; + }; + + // IE9 must have onprogress be set to a unique function. + x.onprogress = function() { }; + + x.ontimeout = function(evt) { + callback.call(this, evt, null); + callback = function() { }; + }; + + x.onabort = function(evt) { + callback.call(this, evt, null); + callback = function() { }; + }; + + // GET is the only supported HTTP Verb by XDomainRequest and is the + // only one supported here. + x.open('GET', url, true); + + // Send the request. Sending data is not supported. + x.send(null); + sent = true; + + return x; + } +}); diff --git a/module/config/config.php b/module/config/config.php index 7250c30..11c0100 100644 --- a/module/config/config.php +++ b/module/config/config.php @@ -280,7 +280,11 @@ $GLOBALS['LEAFLET_LAYERS'] = array } return $label; - } + }, + 'boundsMode' => array( + 'extend' => true, + 'fit' => true, + ), ), ); diff --git a/module/dca/tl_leaflet_layer.php b/module/dca/tl_leaflet_layer.php index 7f867b4..fa7710a 100644 --- a/module/dca/tl_leaflet_layer.php +++ b/module/dca/tl_leaflet_layer.php @@ -219,13 +219,11 @@ $GLOBALS['TL_DCA']['tl_leaflet_layer'] = array 'overpassQuery', 'overpassEndpoint', 'minZoom', - 'overpassCallback', + 'boundsMode' ), '+expert' => array( - 'minZoomIndicatorPosition', - 'debug', - 'minZoomIndicatorMessage', - 'minZoomIndicatorMessageNoLayer', + 'onEachFeature', + 'pointToLayer', ), ), ), diff --git a/src/Netzmacht/Contao/Leaflet/Definition/Layer/OverpassLayer.php b/src/Netzmacht/Contao/Leaflet/Definition/Layer/OverpassLayer.php new file mode 100644 index 0000000..cd94ff7 --- /dev/null +++ b/src/Netzmacht/Contao/Leaflet/Definition/Layer/OverpassLayer.php @@ -0,0 +1,190 @@ + + * @copyright 2016 netzmacht David Molineus. All rights reserved. + * @filesource + * + */ + +namespace Netzmacht\Contao\Leaflet\Definition\Layer; + +use Netzmacht\JavascriptBuilder\Encoder; +use Netzmacht\JavascriptBuilder\Type\AnonymousFunction; +use Netzmacht\JavascriptBuilder\Type\ConvertsToJavascript; +use Netzmacht\JavascriptBuilder\Type\Expression; +use Netzmacht\LeafletPHP\Definition\AbstractLayer; +use Netzmacht\LeafletPHP\Definition\HasOptions; +use Netzmacht\LeafletPHP\Encoder\EncodeHelperTrait; + +/** + * Class OverpassLayer provides implementation of https://github.com/kartenkarsten/leaflet-layer-overpass. + * + * @package Netzmacht\LeafletPHP\Plugins\OverpassLayer + */ +class OverpassLayer extends AbstractLayer implements HasOptions, ConvertsToJavascript +{ + use EncodeHelperTrait; + + /** + * {@inheritdoc} + */ + public static function getType() + { + return 'OverpassLayer'; + } + + /** + * {@inheritdoc} + */ + public static function getRequiredLibraries() + { + $libs = parent::getRequiredLibraries(); + $libs[] = 'osmtogeojson'; + + return $libs; + } + + /** + * OverpassLayer constructor. + * + * @param string $identifier Indicator of the layer. + * @param array $options Options. + */ + public function __construct($identifier, array $options = []) + { + parent::__construct($identifier); + + $this->setOptions($options); + } + + /** + * Set the debug mode. + * + * @param bool $debug Debug mode. + * + * @return $this + */ + public function setDebug($debug) + { + return $this->setOption('debug', (bool) $debug); + } + + /** + * Get debug mode. + * + * @return bool + */ + public function getDebug() + { + return $this->getOption('debug', false); + } + + /** + * Set the query. + * + * @param string $query Query. + * + * @return $this + */ + public function setQuery($query) + { + return $this->setOption('query', $query); + } + + /** + * Get query. + * + * @return bool + */ + public function getQuery() + { + return $this->getOption('query', '(node(BBOX)[organic];node(BBOX)[second_hand];);out qt;'); + } + + /** + * Set the endpoint. + * + * @param string $endpoint Endpoint. + * + * @return $this + */ + public function setEndpoint($endpoint) + { + return $this->setOption('endpoint', $endpoint); + } + + /** + * Get endpoint. + * + * @return bool + */ + public function getEndpoint() + { + return $this->getOption('endpoint', '//overpass-api.de/api/'); + } + + /** + * Set point to layer function. + * + * @param Expression|AnonymousFunction $function The function callback. + * + * @return $this + */ + public function setPointToLayer($function) + { + return $this->setOption('pointToLayer', $function); + } + + /** + * Set on each feature function. + * + * @param Expression|AnonymousFunction $function The function callback. + * + * @return $this + */ + public function setOnEachFeature($function) + { + return $this->setOption('onEachFeature', $function); + } + + /** + * Set the minZoom. + * + * @param int $minZoom MinZoom. + * + * @return $this + */ + public function setMinZoom($minZoom) + { + return $this->setOption('minZoom', (int) $minZoom); + } + + /** + * Get minZoom. + * + * @return bool + */ + public function getMinZoom() + { + return $this->getOption('minZoom', 15); + } + + /** + * {@inheritdoc} + */ + public function encode(Encoder $encoder, $flags = null) + { + $buffer = sprintf ( + '%s = new L.OverPassLayer(%s)%s', + $encoder->encodeReference($this), + $encoder->encodeArray($this->getOptions(), JSON_FORCE_OBJECT), + $encoder->close($flags) + ); + + $buffer .= $this->encodeMethodCalls($this->getMethodCalls(), $encoder, $flags); + + return $buffer; + } +} diff --git a/src/Netzmacht/Contao/Leaflet/Mapper/Layer/OverpassLayerMapper.php b/src/Netzmacht/Contao/Leaflet/Mapper/Layer/OverpassLayerMapper.php index afd093c..36fb44d 100644 --- a/src/Netzmacht/Contao/Leaflet/Mapper/Layer/OverpassLayerMapper.php +++ b/src/Netzmacht/Contao/Leaflet/Mapper/Layer/OverpassLayerMapper.php @@ -10,12 +10,12 @@ namespace Netzmacht\Contao\Leaflet\Mapper\Layer; +use Model; +use Netzmacht\Contao\Leaflet\Definition\Layer\OverpassLayer; use Netzmacht\Contao\Leaflet\Filter\Filter; use Netzmacht\Contao\Leaflet\Mapper\DefinitionMapper; -use Netzmacht\Contao\Leaflet\Mapper\OptionsBuilder; use Netzmacht\JavascriptBuilder\Type\Expression; use Netzmacht\LeafletPHP\Definition; -use Netzmacht\LeafletPHP\Plugins\OverpassLayer\OverpassLayer; /** * Class OverpassLayerMapper @@ -36,7 +36,7 @@ class OverpassLayerMapper extends AbstractLayerMapper * * @var string */ - protected static $definitionClass = 'Netzmacht\LeafletPHP\Plugins\OverpassLayer\OverpassLayer'; + protected static $definitionClass = 'Netzmacht\Contao\Leaflet\Definition\Layer\OverpassLayer'; /** * {@inheritdoc} @@ -47,8 +47,8 @@ class OverpassLayerMapper extends AbstractLayerMapper $this->optionsBuilder ->addOption('query', 'overpassQuery') - ->addOption('minzoom', 'minZoom') - ->addOption('debug') + ->addOption('minZoom') + ->addOption('boundsMode') ->addOption('overpassEndpoint', 'endpoint'); } @@ -57,7 +57,7 @@ class OverpassLayerMapper extends AbstractLayerMapper */ protected function build( Definition $definition, - \Model $model, + Model $model, DefinitionMapper $mapper, Filter $filter = null, Definition $parent = null @@ -66,17 +66,12 @@ class OverpassLayerMapper extends AbstractLayerMapper return; } - $minZoomIndicatorOptions = $definition->getMinZoomIndicatorOptions(); - $minZoomIndicatorOptionsBuilder = new OptionsBuilder(); - $minZoomIndicatorOptionsBuilder - ->addOption('position', 'minZoomIndicatorPosition') - ->addOption('minZoomMessageNoLayer', 'minZoomIndicatorMessageNoLayer') - ->addOption('minZoomMessage', 'minZoomIndicatorMessage'); + if ($model->pointToLayer) { + $definition->setPointToLayer(new Expression($model->pointToLayer)); + } - $minZoomIndicatorOptionsBuilder->build($minZoomIndicatorOptions, $model); - - if ($model->overpassCallback) { - $definition->setCallback(new Expression($model->overpassCallback)); + if ($model->onEachFeature) { + $definition->setOnEachFeature(new Expression($model->onEachFeature)); } } }