diff --git a/.gitignore b/.gitignore index 032708f..7eefdb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,12 @@ config.php +.phpunit.result.cache +.idea -cache/*.cache -/.project +cache/** + +# composer +composer.phar +/vendor/ + +# my test file +test.php diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/cache/readme.txt b/cache/readme.txt deleted file mode 100644 index 533e59c..0000000 --- a/cache/readme.txt +++ /dev/null @@ -1 +0,0 @@ -make sure folder "cache" is writeable \ No newline at end of file diff --git a/cache/result_metacommunities.json b/cache/result_metacommunities.json deleted file mode 100644 index 127f1f6..0000000 --- a/cache/result_metacommunities.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "Freifunk 3Laendereck": {}, - "Freifunk Altmark": {}, - "Freifunk Bielefeld": {}, - "Freifunk Dresden": {}, - "Freifunk Franken": { - "url": "http://www.freifunk-franken.de" - }, - "Freifunk Gütersloh": {}, - "Freifunk Köln, Bonn und Umgebung": {}, - "Freifunk MWU": {}, - "Freifunk Meißen": {}, - "Freifunk Muenchen": {}, - "Freifunk NRW": {}, - "Freifunk Oldenburg": {}, - "Freifunk Reihen": {}, - "Freifunk Rhein-Neckar": {}, - "Freifunk Rheinland": { - "url": "https://freifunk-rheinland.net/" - }, - "Freifunk Rheinland e.V.": { - "url": "https://freifunk-rheinland.net/" - }, - "Freifunk Rheinland e.V. Domäne Rheinufer": { - "url": "https://freifunk-rheinland.net/" - }, - "Freifunk Ruhrgebiet": { - "url": "https://freifunk-rheinland.net/" - }, - "Rheinland - Domäne Ruhrgebiet": { - "url": "https://freifunk-rheinland.net/" - }, - "Freifunk-NRW": {}, - "Funkfeuer": {}, - "Gadow/Zootzen": {}, - "Opennet Initiative e.V.": {}, - "Saarland": {}, - "Schleswig-Holstein": { - "url": "http://freifunk.in-kiel.de/" - }, - "ffnord": {}, - "stuttgart": {} -} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..34f8142 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "vendor_name/freifunk-karte", + "description": "description", + "minimum-stability": "stable", + "license": "GNU General Public License v2.0", + "authors": [ + { + "name": "Tino Dietel", + "email": "tino.dietel@projekt2k.de" + } + ], + "require": { + "phpseclib/phpseclib": "2.*", + "influxdb/influxdb-php": "^1.15", + "ext-curl": "*" + }, + + "autoload": { + "psr-4": { + "Lib\\": "lib/", + "ffmap\\": "lib/" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "lukaswhite/directory": "^0.0.1", + "squizlabs/php_codesniffer": "3.5.*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..6b29d28 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2758 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f77e678f6f74e1dec38bf06e8cafe737", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79", + "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2020-10-10T11:47:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "60d379c243457e073cff02bc323a2a86cb355631" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", + "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.0" + }, + "time": "2020-09-30T07:37:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.7.0" + }, + "time": "2020-09-30T07:37:11+00:00" + }, + { + "name": "influxdb/influxdb-php", + "version": "1.15.1", + "source": { + "type": "git", + "url": "https://github.com/influxdata/influxdb-php.git", + "reference": "447acb600969f9510c9f1900a76d442fc3537b0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/influxdata/influxdb-php/zipball/447acb600969f9510c9f1900a76d442fc3537b0e", + "reference": "447acb600969f9510c9f1900a76d442fc3537b0e", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0|^7.0", + "php": "^5.5 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "suggest": { + "ext-curl": "Curl extension, needed for Curl driver", + "stefanotorresi/influxdb-php-async": "An asyncronous client for InfluxDB, implemented via ReactPHP." + }, + "type": "library", + "autoload": { + "psr-4": { + "InfluxDB\\": "src/InfluxDB" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Hoogendijk", + "email": "stephen@tca0.nl" + }, + { + "name": "Daniel Martinez", + "email": "danimartcas@hotmail.com" + }, + { + "name": "Gianluca Arbezzano", + "email": "gianarb92@gmail.com" + } + ], + "description": "InfluxDB client library for PHP", + "keywords": [ + "client", + "influxdata", + "influxdb", + "influxdb class", + "influxdb client", + "influxdb library", + "time series" + ], + "support": { + "issues": "https://github.com/influxdata/influxdb-php/issues", + "source": "https://github.com/influxdata/influxdb-php/tree/1.15.1" + }, + "time": "2020-09-18T13:24:03+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.30", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/136b9ca7eebef78be14abf90d65c5e57b6bc5d36", + "reference": "136b9ca7eebef78be14abf90d65c5e57b6bc5d36", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.30" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2020-12-17T05:42:04+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.8", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2020-10-23T02:01:07+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "lukaswhite/directory", + "version": "0.0.1", + "source": { + "type": "git", + "url": "https://github.com/lukaswhite/directory.git", + "reference": "77b73d637197b19a7840cc030296962e4e6ae59c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lukaswhite/directory/zipball/77b73d637197b19a7840cc030296962e4e6ae59c", + "reference": "77b73d637197b19a7840cc030296962e4e6ae59c", + "shasum": "" + }, + "require-dev": { + "phpunit/php-code-coverage": "^6.0", + "phpunit/phpunit": "7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lukaswhite\\Directory\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukas White", + "email": "hello@lukaswhite.com" + } + ], + "description": "A PHP class for manipulating a directory on a filesystem", + "support": { + "issues": "https://github.com/lukaswhite/directory/issues", + "source": "https://github.com/lukaswhite/directory/tree/0.0.1" + }, + "time": "2018-09-27T11:26:45+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.10.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + }, + "time": "2020-12-20T10:01:03+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", + "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.0.4" + }, + "time": "2020-12-13T23:18:30+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.12.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "245710e971a030f42e08f4912863805570f23d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", + "reference": "245710e971a030f42e08f4912863805570f23d39", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/1.12.2" + }, + "time": "2020-12-19T10:15:11+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", + "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.10.2", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:44:49+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:57:25+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", + "reference": "8e16c225d57c3d6808014df6b1dd7598d0a5bbbe", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.1", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.3", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^2.3", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.0" + }, + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-12-04T05:05:53+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:52:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:24:23+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", + "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:55:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:18:59+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozart/assert/issues", + "source": "https://github.com/webmozart/assert/tree/master" + }, + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/config.php_example.php b/config.php_example.php old mode 100644 new mode 100755 index 21667fe..a1a130c --- a/config.php_example.php +++ b/config.php_example.php @@ -1,28 +1,48 @@ -OpenStreetMap'; - -$mapInitalView = array( - 'latitude' => 49.447733, - 'longitude' => 10.767502, - 'zoom' => 10, -); - -// db acces for logging -// set to false to disable db-logging of nodecount -$dbAccess = array( - 'host' => 'localhost', - 'db' => 'mydb', - 'user' => 'myuser', - 'pass' => 'mypass' -); - -/* - * this will be used for some css and js - * don't forget to add a slash - */ -$localNetmon = 'https://netmon.freifunk-emskirchen.de/'; +OpenStreetMap'; + +$mapInitalView = array( + 'latitude' => 51.16, + 'longitude' => 10.45, + 'zoom' => 6, +); + +$dbAccess = [ +# mysql connection parameters for node statistics +# empty if not wanted +# 'host' => '', +# 'db' => '', +# 'user' => '', +# 'pass' => '' +]; + +$serverUpload = [ +# ftp for upload of results +# empty if not wanted +# 'host' => '', +# 'user' => '', +# 'password' => '', +# 'target' => '/cache', +]; + +$influxDB = [ + 'host' => 'your influxdb host', + 'port' => '8086', + 'dbName' => 'freifunk_karte', + 'user' => 'influx user', + 'password' => 'influx pass', + + 'add_tag_env' => $environment, +]; + +$trackingCode = "...."; diff --git a/css/MarkerCluster.Default.css b/css/MarkerCluster.Default.css new file mode 100755 index 0000000..da330ca --- /dev/null +++ b/css/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + + /* IE 6-8 fallback colors */ +.leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); + } + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); + } +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); + } + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); + } +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/css/MarkerCluster.css b/css/MarkerCluster.css new file mode 100755 index 0000000..1980892 --- /dev/null +++ b/css/MarkerCluster.css @@ -0,0 +1,6 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; + } diff --git a/css/site.css b/css/site.css old mode 100644 new mode 100755 index 545dae1..3f4966b --- a/css/site.css +++ b/css/site.css @@ -1,52 +1,56 @@ -html{ - height:100%; - overflow: hidden; -} -body { - height:100%; -} -table.display tr { - background-color: white; -} -table.display tr:nth-child(even) { - background-color: #E2E4FF; -} - -#map { - height:100%; -} - -#map .leaflet-popup h3.router { - font-size: 15px; - color: #dc0067; - font-weight: bold; -} -#map .leaflet-popup h4.comm { - font-size: 13px; -} - -#map .leaflet-popup p { - margin: 4px 0; -} - -#map .leaflet-popup .errorNote { - color: red; - font-weight: bold; -} - -#toList.btn { - position: absolute; - z-index: 10; - bottom: 15px; - left: 15px; -} - -.leaflet-center-on-position -{ - background-image: url(../img/gps13.png); -} - -#informationModal .tab-pane { - max-height: 560px; - overflow-y: auto; -} \ No newline at end of file +html{ + height:100%; + overflow: hidden; +} +body { + height:100%; +} +table.display tr { + background-color: white; +} +table.display tr:nth-child(even) { + background-color: #E2E4FF; +} + +#map { + height:100%; +} + +#map .leaflet-popup h3.router { + font-size: 15px; + color: #dc0067; + font-weight: bold; +} +#map .leaflet-popup h4.comm { + font-size: 13px; +} + +#map .leaflet-popup p { + margin: 4px 0; +} + +#map .leaflet-popup .errorNote { + color: red; + font-weight: bold; +} + +#toList.btn { + position: absolute; + z-index: 500; + bottom: 15px; + left: 15px; +} + +.leaflet-center-on-position +{ + background-image: url(../img/gps13.png); +} + +#informationModal .tab-pane { + max-height: 560px; + overflow-y: auto; +} + +textarea#log { + font-family: monospace; +} diff --git a/data.php b/data.php old mode 100644 new mode 100755 index 5f35af2..0852f9d --- a/data.php +++ b/data.php @@ -4,118 +4,146 @@ * * this will try to load a cched result if not older than 24h */ + +namespace ffmap; + +use Lib\InfluxLog; + +require __DIR__ . '/vendor/autoload.php'; + +define('APP_STARTED', true); + +$startTS = microtime(true); + $offset = 1 * 60 * 60; -header('Cache-Control: public, max-age='.$offset); -header ("Expires: " . gmdate ("D, d M Y H:i:s", time() + $offset) . " GMT"); +header('Cache-Control: public, max-age=' . $offset); +header("Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT"); header('Content-Type: application/json'); -error_reporting(-1); + ini_set('display_errors', 'On'); +ini_set('display_errors', 1); +ini_set('display_startup_errors', 1); +error_reporting(E_ALL); -require 'config.php'; +ini_set('max_execution_time', (60 * 60)); +set_time_limit((60 * 60)); +ini_set('memory_limit', '1024M'); -if(!isset($_REQUEST[$forceReparseKey])) -{ - // fetch cached result - the shortcut - $response = array( - 'communities' => getFromCache('communities'), - 'allTheRouters' => getFromCache('routers'), - 'metaCommunities' => getFromCache('metacommunities'), - 'isCachedresult' => true, - ); -} -else -{ - // reparse requested - // actually parse now - - require 'lib/simpleCachedCurl.inc.php'; - require 'lib/nodelistparser.php'; - require 'lib/jsv4/jsv4.php'; - require 'lib/log.php'; - - $apiUrl = 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'; - - $parser = new nodeListParser(); - - // uncomment to enable debugoutput from simplecachedcurl - // $parser->setDebug(true); - - $parser->setCachePath(dirname(__FILE__).'/cache/'); - $parser->setSource($apiUrl); - - $ffnw = new stdClass; - $ffnw->name = 'Freifunk NordWest'; - $ffnw->nameShort = 'Freifunk NordWest'; - $ffnw->url = 'https://netmon.nordwest.freifunk.net/'; - $ffnw->parser = 'Netmon'; - $parser->addAdditional('ffnw', $ffnw); - - $ffj = new stdClass; - $ffj->name = 'Freifunk Jena'; - $ffj->nameShort = 'Freifunk Jena'; - $ffj->url = 'https://freifunk-jena.de/ffmap/'; - $ffj->parser = 'Ffmap'; - $parser->addAdditional('ffj', $ffj); - - $ffffm = new stdClass; - $ffffm->name = 'Frankfurt am Main'; - $ffffm->nameShort = 'Frankfurt am Main'; - $ffffm->url = 'http://map.ffm.freifunk.net/'; - $ffffm->parser = 'Ffmap'; - $parser->addAdditional('ffffm', $ffffm); - - $ff_ruhrg_fb = new stdClass; - $ff_ruhrg_fb->name = 'Freifunk Ruhrgebiet - FB'; - $ff_ruhrg_fb->nameShort = 'Freifunk Ruhrgebiet - FB'; - $ff_ruhrg_fb->url = 'http://map.freifunk-ruhrgebiet.de/data/'; - $ff_ruhrg_fb->parser = 'Ffmap'; - $parser->addAdditional('ff_ruhrg_fb', $ff_ruhrg_fb); - - $parseResult = $parser->getParsed(true); - - $response = array( - 'communities' => $parseResult['communities'], - 'allTheRouters' => $parseResult['routerList'] - ); - - if(is_array($dbAccess)) - { - $db = new mysqli($dbAccess['host'], $dbAccess['user'], $dbAccess['pass'], $dbAccess['db']); - $log = new log($db); - $log->add(sizeof($parseResult['routerList'])); - } +require 'config.php'; +if (isset($_REQUEST[$forceReparseKey])) { + // reparse requested + // actually parse now + + require 'lib/jsv4/Jsv4.php'; + require 'lib/log.php'; + + $apiUrl = 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'; + + $cachePath = dirname(__FILE__) . '/cache/'; + $cache = new CommunityCacheHandler($cachePath); + $curlHelper = new CurlHelper(); + $communityDebug = new CommunityDebug(); + + $parser = new NodeListParser($cache, $curlHelper); + + $parser->setCommunityDebug($communityDebug); + $parser->setCachePath($cachePath); + $parser->setSource($apiUrl); + + require 'fixedCommunities.php'; + + $parseResult = $parser->getParsed(true); + + $response = array( + 'communities' => count((array)$parseResult['communities']), + 'allTheRouters' => count($parseResult['routerList']), + 'memoryUsed' => round(memory_get_peak_usage(true) / 1024 / 1024, 1) . 'Mb', + ); + + if (!empty($dbAccess)) { + $db = new mysqli($dbAccess['host'], $dbAccess['user'], $dbAccess['pass'], $dbAccess['db']); + $log = new log($db); + $log->add(sizeof($parseResult['routerList'])); + } +} else { + // fetch cached result - the shortcut + $response = array( + 'communities' => getFromCache('communities'), + 'allTheRouters' => getFromCache('routers'), + 'metaCommunities' => getFromCache('metacommunities'), + 'isCachedresult' => true, + ); } /** * if processonly is set we handle a reparse cron request */ -if(isset($_REQUEST['processonly']) && isset($parser)) -{ - $report = array( - 'communities' => sizeof($response['communities']), - 'nodes' => sizeof($response['allTheRouters']), - 'stats' => $parser->getParseStatistics(), - ); - - echo json_encode($report, JSON_PRETTY_PRINT); +if (isset($_REQUEST['processonly']) && isset($parser)) { + $report = array( + 'communities' => $response['communities'], + 'nodes' => $response['allTheRouters'], + 'memoryUsed' => round(memory_get_peak_usage(true) / 1024 / 1024, 1) . 'Mb', + ); + + echo json_encode($report, JSON_PRETTY_PRINT); +} else { + echo json_encode($response, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); } -else -{ - echo json_encode($response, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); + +if (isset($_REQUEST['upload']) + && $response['communities'] > 10 + && $response['allTheRouters'] > 1000) { + $parseTime = microtime(true) - $startTS; + include 'upload_cache.php'; + $uploadTime = microtime(true) - $startTS - $parseTime; + + if (isset($environment) && $environment == 'live') { + echo "refreshing live statistics.\n"; + $curlHelper->doCall('https://www.freifunk-karte.de/log_to_db.php?token='.$setDataLogPointToken); + } + + if (!empty($influxDB)) { + $influxLog = new InfluxLog( + $influxDB['host'], + $influxDB['port'], + $influxDB['user'], + $influxDB['password'], + $influxDB['dbName'], + ); + + $fields = [ + 'communities' => (int)$response['communities'], + 'parse_time' => (int)$parseTime, + 'upload_time' => (int)round($uploadTime), + 'upload_time_float' => (float)$uploadTime, + 'mem_usage' => memory_get_peak_usage(true), + 'curl_calls' => (int)$curlHelper->getCallCounter(), + 'env' => isset($influxDB['add_tag_env']) ? $influxDB['add_tag_env'] : 'undefined' + ]; + + try { + $influxLog->logPoint([ + 'value' => (int)$response['allTheRouters'], + 'fields' => $fields, + ]); + } catch (\InfluxDB\Exception $e) { + echo 'Logging to InfluxDB failed: ' . $e->getMessage; + } + } } -function getFromCache($key) +/** + * @param $key string + * @return false|mixed + */ +function getFromCache(string $key) { - $filename = dirname(__FILE__).'/cache/result_'.$key.'.json'; - - if ( !file_exists($filename) ) - { - return false; - } - else - { - return json_decode(file_get_contents($filename)); - } + $filename = dirname(__FILE__) . '/cache/result_' . $key . '.json'; + if (!file_exists($filename)) { + return false; + } else { + return json_decode(file_get_contents($filename)); + } } diff --git a/debug.php b/debug.php old mode 100644 new mode 100755 index 44a76f4..16600ff --- a/debug.php +++ b/debug.php @@ -1,238 +1,218 @@ - - - - - - Freifunk-Karte-Debugview - - - - - - -
- -
-

Freifunk-Karte Debugview

-

-
- -
-
-
- - -
-
- - -
-
- -
- -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- -
- - -
-
-
-
-
- - - - - - - - + + + + + + Freifunk-Karte-Debugview + + + + + + +
+ +
+

Freifunk-Karte Debugview

+

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + + + + + + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..f785830 Binary files /dev/null and b/favicon.ico differ diff --git a/fetch.php b/fetch.php old mode 100644 new mode 100755 index a8dcc1e..71f4b43 --- a/fetch.php +++ b/fetch.php @@ -1,74 +1,74 @@ - array( - 'tpl' => 'templates/about.tpl', - ), - 'stats' => array( - 'tpl' => 'templates/stats.tpl', - 'contr' => 'fetchStats', - ), - 'gpxfile' => array( - 'tpl' => 'templates/gpxfile.tpl', - ), -); - -if(isset($_REQUEST['content']) && isset($supported[$_REQUEST['content']])) -{ - $data = false; - - if(!empty($supported[$_REQUEST['content']]['contr'])) - { - $controller = $supported[$_REQUEST['content']]['contr']; - - if(function_exists($controller)) - { - $data = $controller(); - } - } - - include(__DIR__.'/'.$supported[$_REQUEST['content']]['tpl']); - -} -elseif(isset($_REQUEST['get_debug'])) -{ - $filename = 'cache/result_statistics.json'; - - if(file_exists($filename)) - { - echo file_get_contents($filename); - } -} - -function fetchStats() -{ - global $dbAccess; - require 'lib/log.php'; - - $db = new mysqli($dbAccess['host'], $dbAccess['user'], $dbAccess['pass'], $dbAccess['db']); - $log = new log($db); - $data = $log->get(); - - $min = array(); - $max = array(); - - $midOver = 12; - $dateOf = 6; - - $c = 0; - $set = array(); - $setDate = false; - $lastDate = false; - - foreach ($data as $item) - { - $min[] = array($item['ts'], $item['min']); - $max[] = array($item['ts'], $item['max']); - } - - return array($min, $max); -} + array( + 'tpl' => 'templates/about.tpl', + ), + 'stats' => array( + 'tpl' => 'templates/stats.tpl', + 'contr' => 'fetchStats', + ), + 'gpxfile' => array( + 'tpl' => 'templates/gpxfile.tpl', + ), +); + +if(isset($_REQUEST['content']) && isset($supported[$_REQUEST['content']])) +{ + $data = false; + + if(!empty($supported[$_REQUEST['content']]['contr'])) + { + $controller = $supported[$_REQUEST['content']]['contr']; + + if(function_exists($controller)) + { + $data = $controller(); + } + } + + include(__DIR__.'/'.$supported[$_REQUEST['content']]['tpl']); + +} +elseif(isset($_REQUEST['get_debug'])) +{ + $filename = 'cache/result_statistics.json'; + + if(file_exists($filename)) + { + echo file_get_contents($filename); + } +} + +function fetchStats() +{ + global $dbAccess; + require 'lib/log.php'; + + $db = new mysqli($dbAccess['host'], $dbAccess['user'], $dbAccess['pass'], $dbAccess['db']); + $log = new log($db); + $data = $log->get(); + + $min = array(); + $max = array(); + + $midOver = 12; + $dateOf = 6; + + $c = 0; + $set = array(); + $setDate = false; + $lastDate = false; + + foreach ($data as $item) + { + $min[] = array($item['ts'], $item['min']); + $max[] = array($item['ts'], $item['max']); + } + + return array($min, $max); +} diff --git a/fixedCommunities.php b/fixedCommunities.php new file mode 100644 index 0000000..ab6b99b --- /dev/null +++ b/fixedCommunities.php @@ -0,0 +1,111 @@ +name = 'Freifunk Duesseldorf-Flingern'; +$ff_ddfl->nameShort = 'Duesseldorf Flingern'; +$ff_ddfl->url = 'https://karte.ffdus.de/data/nodes.json'; +$ff_ddfl->homePage = 'http://www.ffdus.de/'; +$ff_ddfl->parser = 'Ffmap'; +$parser->addAdditional('ff_duesseldorf-flingern', $ff_ddfl); + +$ff_wt = new stdClass(); +$ff_wt->name = 'Freifunk Wuppertal'; +$ff_wt->nameShort = 'Wuppertal'; +$ff_wt->url = 'https://map.freifunk-wuppertal.net/data/nodes.json'; +$ff_wt->homePage = 'http://www.freifunk-wuppertal.net'; +$ff_wt->parser = 'Ffmap'; +$parser->addAdditional('ff_wt', $ff_wt); + +foreach (["wtbg","sdlh","hd","mdb","doerfer","ln","hlb","bs","mb","mq","su","wa","ar"] as $index => $identifier) { + ${'ff_winterb_'.$index} = new stdClass(); + ${'ff_winterb_'.$index}->name = 'Freifunk Winterberg '.$identifier; + ${'ff_winterb_'.$index}->nameShort = 'Winterberg'.$identifier; + ${'ff_winterb_'.$index}->url = 'https://map.freifunk-winterberg.net/data/'.$identifier.'/meshviewer.json'; + ${'ff_winterb_'.$index}->homePage = 'https://map.freifunk-winterberg.net/'; + ${'ff_winterb_'.$index}->parser = 'Ffmap'; + $parser->addAdditional('ff_winterberg_'.$index, ${'ff_winterb_'.$index}); +} + +$ff_lz = new stdClass(); +$ff_lz->name = 'Freifunk Leipzig'; +$ff_lz->nameShort = 'Leipzig'; +$ff_lz->url = 'http://db.leipzig.freifunk.net/uptime/hopglass/v2/nodes.json'; +$ff_lz->homePage = 'http://leipzig.freifunk.net/'; +$ff_lz->parser = 'Ffmap'; +$parser->addAdditional('ff_lz', $ff_lz); + +$ff_lp = new stdClass(); +$ff_lp->name = 'Freifunk Lippe'; +$ff_lp->nameShort = 'Lippe '; +$ff_lp->url = 'https://map.freifunk-lippe.de/map/data/meshviewer.json'; +$ff_lp->homePage = 'http://freifunk-lippe.de/'; +$ff_lp->parser = 'Ffmap'; +$parser->addAdditional('ff_lp', $ff_lp); + +$ff_eb = new stdClass(); +$ff_eb->name = 'Freifunk Einbeck'; +$ff_eb->nameShort = 'Einbeck '; +$ff_eb->url = 'http://vps643489.ovh.net/meshviewer/data/meshviewer.json'; +$ff_eb->homePage = 'https://freifunk-einbeck.de/'; +$ff_eb->parser = 'Ffmap'; +$parser->addAdditional('ff_eb', $ff_eb); + +$ff_en = new stdClass(); +$ff_en->name = 'Freifunk Ennepetal'; +$ff_en->nameShort = 'Ennepetal'; +$ff_en->url = 'https://karte.ff-en.de/data/meshviewer.json'; +$ff_en->homePage = 'https://freifunk-en.de/'; +$ff_en->parser = 'Ffmap'; +$parser->addAdditional('ff_en', $ff_en); + +foreach (range(1, 4) as $index) { + ${'ff_gt_'.$index} = new stdClass(); + ${'ff_gt_'.$index}->name = 'Freifunk Kreis GT '.$index; + ${'ff_gt_'.$index}->nameShort = 'Gütersloh'.$index; + ${'ff_gt_'.$index}->url = 'https://map03.4830.org/data/map_0'.$index.'/nodes.json'; + ${'ff_gt_'.$index}->homePage = 'https://freifunk-kreisgt.de/'; + ${'ff_gt_'.$index}->parser = 'Ffmap'; + $parser->addAdditional('ff_gt_'.$index, ${'ff_gt_'.$index}); +} + +$ff_goettingen = new stdClass(); +$ff_goettingen->name = 'Freifunk Göttingen'; +$ff_goettingen->nameShort = 'Göttingen'; +$ff_goettingen->url = 'https://cccgoe.de/map2data/nodes.json'; +$ff_goettingen->homePage = 'http://freifunk-goettingen.de'; +$ff_goettingen->parser = 'Ffmap'; +$parser->addAdditional('ff_goettingen', $ff_goettingen); + +$ff_weimar = new stdClass(); +$ff_weimar->name = 'Freifunk Weimar'; +$ff_weimar->nameShort = 'Weimar'; +$ff_weimar->url = 'https://hopglass.weimarnetz.de/data/nodes.json'; +$ff_weimar->homePage = 'https://weimarnetz.de/'; +$ff_weimar->parser = 'Ffmap'; +$parser->addAdditional('ff_weimar', $ff_weimar); + +$ff_chemnitz2 = new stdClass(); +$ff_chemnitz2->name = 'Freifunk Chemnitz'; +$ff_chemnitz2->nameShort = 'Chemnitz'; +$ff_chemnitz2->url = 'https://gianotti.routers.chemnitz.freifunk.net/nodelist.json'; +$ff_chemnitz2->homePage = 'https://www.chemnitz.freifunk.net/'; +$ff_chemnitz2->parser = 'nodelist'; +$parser->addAdditional('ff_chemnitz2', $ff_chemnitz2); + +$ff_kbumland = new stdClass(); +$ff_kbumland->name = 'Freifunk KoelnBonnUmland'; +$ff_kbumland->nameShort = 'KBU'; +$ff_kbumland->url = 'https://map.kbu.freifunk.net/data/ffkbuu/meshviewer.json'; +$ff_kbumland->homePage = 'https://map.kbu.freifunk.net/#!/de/map'; +$ff_kbumland->parser = 'Ffmap'; +$parser->addAdditional('ff_kbumland', $ff_kbumland); + +$ff_euskirchen = new stdClass(); +$ff_euskirchen->name = 'Freifunk Euskirchen'; +$ff_euskirchen->nameShort = 'Euskirchen'; +$ff_euskirchen->url = 'https://map.kbu.freifunk.net/data/ffeu/meshviewer.json'; +$ff_euskirchen->homePage = 'https://ffeu.de/'; +$ff_euskirchen->parser = 'Ffmap'; +$parser->addAdditional('ff_euskirchen', $ff_euskirchen); diff --git a/img/ajax-loader.gif b/img/ajax-loader.gif old mode 100644 new mode 100755 diff --git a/img/gps13.png b/img/gps13.png old mode 100644 new mode 100755 diff --git a/img/hotspot.png b/img/hotspot.png old mode 100644 new mode 100755 diff --git a/img/hotspot_offline.png b/img/hotspot_offline.png old mode 100644 new mode 100755 diff --git a/index.php b/index.php old mode 100644 new mode 100755 index d6b0b2f..0d412fe --- a/index.php +++ b/index.php @@ -1,117 +1,117 @@ - - - - - - Freifunk-Karte - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - + + + + + + Freifunk-Karte + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/flot/LICENSE.txt b/js/flot/LICENSE.txt old mode 100644 new mode 100755 index 5308ada..719da06 --- a/js/flot/LICENSE.txt +++ b/js/flot/LICENSE.txt @@ -1,22 +1,22 @@ -Copyright (c) 2007-2014 IOLA and Ole Laursen - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +Copyright (c) 2007-2014 IOLA and Ole Laursen + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/js/flot/README.md b/js/flot/README.md old mode 100644 new mode 100755 diff --git a/js/flot/jquery.flot.min.js b/js/flot/jquery.flot.min.js old mode 100644 new mode 100755 diff --git a/js/flot/jquery.flot.time.min.js b/js/flot/jquery.flot.time.min.js old mode 100644 new mode 100755 diff --git a/js/graham_scan.js b/js/graham_scan.js old mode 100644 new mode 100755 diff --git a/js/heatmap.min.js b/js/heatmap.min.js old mode 100644 new mode 100755 index 8a8bbe1..f62852c --- a/js/heatmap.min.js +++ b/js/heatmap.min.js @@ -1,9 +1,9 @@ -/* - * heatmap.js v2.0.0 | JavaScript Heatmap Library - * - * Copyright 2008-2014 Patrick Wied - All rights reserved. - * Dual licensed under MIT and Beerware license - * - * :: 2014-08-05 01:42 - */ +/* + * heatmap.js v2.0.0 | JavaScript Heatmap Library + * + * Copyright 2008-2014 Patrick Wied - All rights reserved. + * Dual licensed under MIT and Beerware license + * + * :: 2014-08-05 01:42 + */ (function(a){var b={defaultRadius:40,defaultRenderer:"canvas2d",defaultGradient:{.25:"rgb(0,0,255)",.55:"rgb(0,255,0)",.85:"yellow",1:"rgb(255,0,0)"},defaultMaxOpacity:1,defaultMinOpacity:0,defaultBlur:.85,defaultXField:"x",defaultYField:"y",defaultValueField:"value",plugins:{}};var c=function i(){var a=function d(a){this._coordinator={};this._data=[];this._radi=[];this._min=0;this._max=1;this._xField=a["xField"]||a.defaultXField;this._yField=a["yField"]||a.defaultYField;this._valueField=a["valueField"]||a.defaultValueField;if(a["radius"]){this._cfgRadius=a["radius"]}};var c=b.defaultRadius;a.prototype={_organiseData:function(a,b){var d=a[this._xField];var e=a[this._yField];var f=this._radi;var g=this._data;var h=this._max;var i=this._min;var j=a[this._valueField]||1;var k=a.radius||this._cfgRadius||c;if(!g[d]){g[d]=[];f[d]=[]}if(!g[d][e]){g[d][e]=j;f[d][e]=k}else{g[d][e]+=j}if(g[d][e]>h){if(!b){this._max=g[d][e]}else{this.setDataMax(g[d][e])}return false}else{return{x:d,y:e,value:j,radius:k,min:i,max:h}}},_unOrganizeData:function(){var a=[];var b=this._data;var c=this._radi;for(var d in b){for(var e in b[d]){a.push({x:d,y:e,radius:c[d][e],value:b[d][e]})}}return{min:this._min,max:this._max,data:a}},_onExtremaChange:function(){this._coordinator.emit("extremachange",{min:this._min,max:this._max})},addData:function(){if(arguments[0].length>0){var a=arguments[0];var b=a.length;while(b--){this.addData.call(this,a[b])}}else{var c=this._organiseData(arguments[0],true);if(c){this._coordinator.emit("renderpartial",{min:this._min,max:this._max,data:[c]})}}return this},setData:function(a){var b=a.data;var c=b.length;this._max=a.max;this._min=a.min||0;this._data=[];this._radi=[];for(var d=0;dthis._renderBoundaries[2]){this._renderBoundaries[2]=l+2*j}if(m+2*j>this._renderBoundaries[3]){this._renderBoundaries[3]=m+2*j}}},_colorize:function(){var a=this._renderBoundaries[0];var b=this._renderBoundaries[1];var c=this._renderBoundaries[2]-a;var d=this._renderBoundaries[3]-b;var e=this._width;var f=this._height;var g=this._opacity;var h=this._maxOpacity;var i=this._minOpacity;if(a<0){a=0}if(b<0){b=0}if(a+c>e){c=e-a}if(b+d>f){d=f-b}var j=this.shadowCtx.getImageData(a,b,c,d);var k=j.data;var l=k.length;var m=this._palette;for(var n=3;n0){q=g}else{if(o>0;return b},getDataURL:function(){return this.canvas.toDataURL()}};return d}();var e=function k(){var a=false;if(b["defaultRenderer"]==="canvas2d"){a=d}return a}();var f={merge:function(){var a={};var b=arguments.length;for(var c=0;c 0) { - var len = pointOrArray.length; - while(len--) { - this.addData(pointOrArray[len]); - } - } else { - var latField = this.cfg.latField || 'lat'; - var lngField = this.cfg.lngField || 'lng'; - var valueField = this.cfg.valueField || 'value'; - var entry = pointOrArray; - var latlng = new L.LatLng(entry[latField], entry[lngField]); - var dataObj = { latlng: latlng }; - - dataObj[valueField] = entry[valueField]; - this._max = Math.max(this._max, dataObj[valueField]); - this._min = Math.min(this._min, dataObj[valueField]); - - if (entry.radius) { - dataObj.radius = entry.radius; - } - this._data.push(dataObj); - this._draw(); - } - }, - _resetOrigin: function () { - this._origin = this._map.layerPointToLatLng(new L.Point(0, 0)); - this._draw(); - } -}); - -HeatmapOverlay.CSS_TRANSFORM = (function() { - var div = document.createElement('div'); - var props = [ - 'transform', - 'WebkitTransform', - 'MozTransform', - 'OTransform', - 'msTransform' - ]; - - for (var i = 0; i < props.length; i++) { - var prop = props[i]; - if (div.style[prop] !== undefined) { - return prop; - } - } - - return props[0]; +/* +* Leaflet Heatmap Overlay +* +* Copyright (c) 2014, Patrick Wied (http://www.patrick-wied.at) +* Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) +* and the Beerware (http://en.wikipedia.org/wiki/Beerware) license. +*/ + +// Leaflet < 0.8 compatibility +if (typeof L.Layer === 'undefined') { + L.Layer = L.Class; +} + +var HeatmapOverlay = L.Layer.extend({ + + initialize: function (config) { + this.cfg = config; + this._el = L.DomUtil.create('div', 'leaflet-zoom-hide'); + this._data = []; + this._max = 1; + this._min = 0; + this.cfg.container = this._el; + }, + + onAdd: function (map) { + var size = map.getSize(); + + this._map = map; + + this._width = size.x; + this._height = size.y; + + this._el.style.width = size.x + 'px'; + this._el.style.height = size.y + 'px'; + + this._resetOrigin(); + + map.getPanes().overlayPane.appendChild(this._el); + + if (!this._heatmap) { + this._heatmap = h337.create(this.cfg); + } + + // on zoom, reset origin + map.on('viewreset', this._resetOrigin, this); + // redraw whenever dragend + map.on('dragend', this._draw, this); + + this._draw(); + }, + + onRemove: function (map) { + // remove layer's DOM elements and listeners + map.getPanes().overlayPane.removeChild(this._el); + + map.off('viewreset', this._resetOrigin, this); + map.off('dragend', this._draw, this); + }, + _draw: function() { + if (!this._map) { return; } + + var point = this._map.latLngToContainerPoint(this._origin); + + // reposition the layer + this._el.style[HeatmapOverlay.CSS_TRANSFORM] = 'translate(' + + -Math.round(point.x) + 'px,' + + -Math.round(point.y) + 'px)'; + + this._update(); + }, + _update: function() { + var bounds, zoom, scale; + + bounds = this._map.getBounds(); + zoom = this._map.getZoom(); + scale = Math.pow(2, zoom); + + if (this._data.length == 0) { + return; + } + + var generatedData = { max: this._max, min: this._min }; + var latLngPoints = []; + var radiusMultiplier = this.cfg.scaleRadius ? scale : 1; + var localMax = 0; + var localMin = 0; + var valueField = this.cfg.valueField; + var len = this._data.length; + + while (len--) { + var entry = this._data[len]; + var value = entry[valueField]; + var latlng = entry.latlng; + + + // we don't wanna render points that are not even on the map ;-) + if (!bounds.contains(latlng)) { + continue; + } + // local max is the maximum within current bounds + localMax = Math.max(value, localMax); + localMin = Math.min(value, localMin); + + var point = this._map.latLngToContainerPoint(latlng); + var latlngPoint = { x: Math.round(point.x), y: Math.round(point.y) }; + latlngPoint[valueField] = value; + + var radius; + + if (entry.radius) { + radius = entry.radius * radiusMultiplier; + } else { + radius = (this.cfg.radius || 2) * radiusMultiplier; + } + latlngPoint.radius = radius; + latLngPoints.push(latlngPoint); + } + if (this.cfg.useLocalExtrema) { + generatedData.max = localMax; + generatedData.min = localMin; + } + + generatedData.data = latLngPoints; + + this._heatmap.setData(generatedData); + }, + setData: function(data) { + this._max = data.max || this._max; + this._min = data.min || this._min; + var latField = this.cfg.latField || 'lat'; + var lngField = this.cfg.lngField || 'lng'; + var valueField = this.cfg.valueField || 'value'; + + // transform data to latlngs + var data = data.data; + var len = data.length; + var d = []; + + while (len--) { + var entry = data[len]; + var latlng = new L.LatLng(entry[latField], entry[lngField]); + var dataObj = { latlng: latlng }; + dataObj[valueField] = entry[valueField]; + if (entry.radius) { + dataObj.radius = entry.radius; + } + d.push(dataObj); + } + this._data = d; + + this._draw(); + }, + // experimential... not ready. + addData: function(pointOrArray) { + if (pointOrArray.length > 0) { + var len = pointOrArray.length; + while(len--) { + this.addData(pointOrArray[len]); + } + } else { + var latField = this.cfg.latField || 'lat'; + var lngField = this.cfg.lngField || 'lng'; + var valueField = this.cfg.valueField || 'value'; + var entry = pointOrArray; + var latlng = new L.LatLng(entry[latField], entry[lngField]); + var dataObj = { latlng: latlng }; + + dataObj[valueField] = entry[valueField]; + this._max = Math.max(this._max, dataObj[valueField]); + this._min = Math.min(this._min, dataObj[valueField]); + + if (entry.radius) { + dataObj.radius = entry.radius; + } + this._data.push(dataObj); + this._draw(); + } + }, + _resetOrigin: function () { + this._origin = this._map.layerPointToLatLng(new L.Point(0, 0)); + this._draw(); + } +}); + +HeatmapOverlay.CSS_TRANSFORM = (function() { + var div = document.createElement('div'); + var props = [ + 'transform', + 'WebkitTransform', + 'MozTransform', + 'OTransform', + 'msTransform' + ]; + + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (div.style[prop] !== undefined) { + return prop; + } + } + + return props[0]; })(); \ No newline at end of file diff --git a/js/leaflet.markercluster-src.js b/js/leaflet.markercluster-src.js new file mode 100755 index 0000000..1ca4767 --- /dev/null +++ b/js/leaflet.markercluster-src.js @@ -0,0 +1,2163 @@ +/* + Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. + https://github.com/Leaflet/Leaflet.markercluster + (c) 2012-2013, Dave Leaver, smartrak +*/ +(function (window, document, undefined) {/* + * L.MarkerClusterGroup extends L.FeatureGroup by clustering the markers contained within + */ + +L.MarkerClusterGroup = L.FeatureGroup.extend({ + + options: { + maxClusterRadius: 80, //A cluster will cover at most this many pixels from its center + iconCreateFunction: null, + + spiderfyOnMaxZoom: true, + showCoverageOnHover: true, + zoomToBoundsOnClick: true, + singleMarkerMode: false, + + disableClusteringAtZoom: null, + + // Setting this to false prevents the removal of any clusters outside of the viewpoint, which + // is the default behaviour for performance reasons. + removeOutsideVisibleBounds: true, + + //Whether to animate adding markers after adding the MarkerClusterGroup to the map + // If you are adding individual markers set to true, if adding bulk markers leave false for massive performance gains. + animateAddingMarkers: false, + + //Increase to increase the distance away that spiderfied markers appear from the center + spiderfyDistanceMultiplier: 1, + + // When bulk adding layers, adds markers in chunks. Means addLayers may not add all the layers in the call, others will be loaded during setTimeouts + chunkedLoading: false, + chunkInterval: 200, // process markers for a maximum of ~ n milliseconds (then trigger the chunkProgress callback) + chunkDelay: 50, // at the end of each interval, give n milliseconds back to system/browser + chunkProgress: null, // progress callback: function(processed, total, elapsed) (e.g. for a progress indicator) + + //Options to pass to the L.Polygon constructor + polygonOptions: {} + }, + + initialize: function (options) { + L.Util.setOptions(this, options); + if (!this.options.iconCreateFunction) { + this.options.iconCreateFunction = this._defaultIconCreateFunction; + } + + this._featureGroup = L.featureGroup(); + this._featureGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); + + this._nonPointGroup = L.featureGroup(); + this._nonPointGroup.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); + + this._inZoomAnimation = 0; + this._needsClustering = []; + this._needsRemoving = []; //Markers removed while we aren't on the map need to be kept track of + //The bounds of the currently shown area (from _getExpandedVisibleBounds) Updated on zoom/move + this._currentShownBounds = null; + + this._queue = []; + }, + + addLayer: function (layer) { + + if (layer instanceof L.LayerGroup) { + var array = []; + for (var i in layer._layers) { + array.push(layer._layers[i]); + } + return this.addLayers(array); + } + + //Don't cluster non point data + if (!layer.getLatLng) { + this._nonPointGroup.addLayer(layer); + return this; + } + + if (!this._map) { + this._needsClustering.push(layer); + return this; + } + + if (this.hasLayer(layer)) { + return this; + } + + + //If we have already clustered we'll need to add this one to a cluster + + if (this._unspiderfy) { + this._unspiderfy(); + } + + this._addLayer(layer, this._maxZoom); + + //Work out what is visible + var visibleLayer = layer, + currentZoom = this._map.getZoom(); + if (layer.__parent) { + while (visibleLayer.__parent._zoom >= currentZoom) { + visibleLayer = visibleLayer.__parent; + } + } + + if (this._currentShownBounds.contains(visibleLayer.getLatLng())) { + if (this.options.animateAddingMarkers) { + this._animationAddLayer(layer, visibleLayer); + } else { + this._animationAddLayerNonAnimated(layer, visibleLayer); + } + } + return this; + }, + + removeLayer: function (layer) { + + if (layer instanceof L.LayerGroup) + { + var array = []; + for (var i in layer._layers) { + array.push(layer._layers[i]); + } + return this.removeLayers(array); + } + + //Non point layers + if (!layer.getLatLng) { + this._nonPointGroup.removeLayer(layer); + return this; + } + + if (!this._map) { + if (!this._arraySplice(this._needsClustering, layer) && this.hasLayer(layer)) { + this._needsRemoving.push(layer); + } + return this; + } + + if (!layer.__parent) { + return this; + } + + if (this._unspiderfy) { + this._unspiderfy(); + this._unspiderfyLayer(layer); + } + + //Remove the marker from clusters + this._removeLayer(layer, true); + + if (this._featureGroup.hasLayer(layer)) { + this._featureGroup.removeLayer(layer); + if (layer.setOpacity) { + layer.setOpacity(1); + } + } + + return this; + }, + + //Takes an array of markers and adds them in bulk + addLayers: function (layersArray) { + var fg = this._featureGroup, + npg = this._nonPointGroup, + chunked = this.options.chunkedLoading, + chunkInterval = this.options.chunkInterval, + chunkProgress = this.options.chunkProgress, + newMarkers, i, l, m; + + if (this._map) { + var offset = 0, + started = (new Date()).getTime(); + var process = L.bind(function () { + var start = (new Date()).getTime(); + for (; offset < layersArray.length; offset++) { + if (chunked && offset % 200 === 0) { + // every couple hundred markers, instrument the time elapsed since processing started: + var elapsed = (new Date()).getTime() - start; + if (elapsed > chunkInterval) { + break; // been working too hard, time to take a break :-) + } + } + + m = layersArray[offset]; + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + this._addLayer(m, this._maxZoom); + + //If we just made a cluster of size 2 then we need to remove the other marker from the map (if it is) or we never will + if (m.__parent) { + if (m.__parent.getChildCount() === 2) { + var markers = m.__parent.getAllChildMarkers(), + otherMarker = markers[0] === m ? markers[1] : markers[0]; + fg.removeLayer(otherMarker); + } + } + } + + if (chunkProgress) { + // report progress and time elapsed: + chunkProgress(offset, layersArray.length, (new Date()).getTime() - started); + } + + if (offset === layersArray.length) { + //Update the icons of all those visible clusters that were affected + this._featureGroup.eachLayer(function (c) { + if (c instanceof L.MarkerCluster && c._iconNeedsUpdate) { + c._updateIcon(); + } + }); + + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + } else { + setTimeout(process, this.options.chunkDelay); + } + }, this); + + process(); + } else { + newMarkers = []; + for (i = 0, l = layersArray.length; i < l; i++) { + m = layersArray[i]; + + //Not point data, can't be clustered + if (!m.getLatLng) { + npg.addLayer(m); + continue; + } + + if (this.hasLayer(m)) { + continue; + } + + newMarkers.push(m); + } + this._needsClustering = this._needsClustering.concat(newMarkers); + } + return this; + }, + + //Takes an array of markers and removes them in bulk + removeLayers: function (layersArray) { + var i, l, m, + fg = this._featureGroup, + npg = this._nonPointGroup; + + if (!this._map) { + for (i = 0, l = layersArray.length; i < l; i++) { + m = layersArray[i]; + this._arraySplice(this._needsClustering, m); + npg.removeLayer(m); + } + return this; + } + + for (i = 0, l = layersArray.length; i < l; i++) { + m = layersArray[i]; + + if (!m.__parent) { + npg.removeLayer(m); + continue; + } + + this._removeLayer(m, true, true); + + if (fg.hasLayer(m)) { + fg.removeLayer(m); + if (m.setOpacity) { + m.setOpacity(1); + } + } + } + + //Fix up the clusters and markers on the map + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._zoom, this._currentShownBounds); + + fg.eachLayer(function (c) { + if (c instanceof L.MarkerCluster) { + c._updateIcon(); + } + }); + + return this; + }, + + //Removes all layers from the MarkerClusterGroup + clearLayers: function () { + //Need our own special implementation as the LayerGroup one doesn't work for us + + //If we aren't on the map (yet), blow away the markers we know of + if (!this._map) { + this._needsClustering = []; + delete this._gridClusters; + delete this._gridUnclustered; + } + + if (this._noanimationUnspiderfy) { + this._noanimationUnspiderfy(); + } + + //Remove all the visible layers + this._featureGroup.clearLayers(); + this._nonPointGroup.clearLayers(); + + this.eachLayer(function (marker) { + delete marker.__parent; + }); + + if (this._map) { + //Reset _topClusterLevel and the DistanceGrids + this._generateInitialClusters(); + } + + return this; + }, + + //Override FeatureGroup.getBounds as it doesn't work + getBounds: function () { + var bounds = new L.LatLngBounds(); + + if (this._topClusterLevel) { + bounds.extend(this._topClusterLevel._bounds); + } + + for (var i = this._needsClustering.length - 1; i >= 0; i--) { + bounds.extend(this._needsClustering[i].getLatLng()); + } + + bounds.extend(this._nonPointGroup.getBounds()); + + return bounds; + }, + + //Overrides LayerGroup.eachLayer + eachLayer: function (method, context) { + var markers = this._needsClustering.slice(), + i; + + if (this._topClusterLevel) { + this._topClusterLevel.getAllChildMarkers(markers); + } + + for (i = markers.length - 1; i >= 0; i--) { + method.call(context, markers[i]); + } + + this._nonPointGroup.eachLayer(method, context); + }, + + //Overrides LayerGroup.getLayers + getLayers: function () { + var layers = []; + this.eachLayer(function (l) { + layers.push(l); + }); + return layers; + }, + + //Overrides LayerGroup.getLayer, WARNING: Really bad performance + getLayer: function (id) { + var result = null; + + this.eachLayer(function (l) { + if (L.stamp(l) === id) { + result = l; + } + }); + + return result; + }, + + //Returns true if the given layer is in this MarkerClusterGroup + hasLayer: function (layer) { + if (!layer) { + return false; + } + + var i, anArray = this._needsClustering; + + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === layer) { + return true; + } + } + + anArray = this._needsRemoving; + for (i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === layer) { + return false; + } + } + + return !!(layer.__parent && layer.__parent._group === this) || this._nonPointGroup.hasLayer(layer); + }, + + //Zoom down to show the given layer (spiderfying if necessary) then calls the callback + zoomToShowLayer: function (layer, callback) { + + var showMarker = function () { + if ((layer._icon || layer.__parent._icon) && !this._inZoomAnimation) { + this._map.off('moveend', showMarker, this); + this.off('animationend', showMarker, this); + + if (layer._icon) { + callback(); + } else if (layer.__parent._icon) { + var afterSpiderfy = function () { + this.off('spiderfied', afterSpiderfy, this); + callback(); + }; + + this.on('spiderfied', afterSpiderfy, this); + layer.__parent.spiderfy(); + } + } + }; + + if (layer._icon && this._map.getBounds().contains(layer.getLatLng())) { + //Layer is visible ond on screen, immediate return + callback(); + } else if (layer.__parent._zoom < this._map.getZoom()) { + //Layer should be visible at this zoom level. It must not be on screen so just pan over to it + this._map.on('moveend', showMarker, this); + this._map.panTo(layer.getLatLng()); + } else { + var moveStart = function () { + this._map.off('movestart', moveStart, this); + moveStart = null; + }; + + this._map.on('movestart', moveStart, this); + this._map.on('moveend', showMarker, this); + this.on('animationend', showMarker, this); + layer.__parent.zoomToBounds(); + + if (moveStart) { + //Never started moving, must already be there, probably need clustering however + showMarker.call(this); + } + } + }, + + //Overrides FeatureGroup.onAdd + onAdd: function (map) { + this._map = map; + var i, l, layer; + + if (!isFinite(this._map.getMaxZoom())) { + throw "Map has no maxZoom specified"; + } + + this._featureGroup.onAdd(map); + this._nonPointGroup.onAdd(map); + + if (!this._gridClusters) { + this._generateInitialClusters(); + } + + for (i = 0, l = this._needsRemoving.length; i < l; i++) { + layer = this._needsRemoving[i]; + this._removeLayer(layer, true); + } + this._needsRemoving = []; + + //Remember the current zoom level and bounds + this._zoom = this._map.getZoom(); + this._currentShownBounds = this._getExpandedVisibleBounds(); + + this._map.on('zoomend', this._zoomEnd, this); + this._map.on('moveend', this._moveEnd, this); + + if (this._spiderfierOnAdd) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnAdd(); + } + + this._bindEvents(); + + //Actually add our markers to the map: + l = this._needsClustering; + this._needsClustering = []; + this.addLayers(l); + }, + + //Overrides FeatureGroup.onRemove + onRemove: function (map) { + map.off('zoomend', this._zoomEnd, this); + map.off('moveend', this._moveEnd, this); + + this._unbindEvents(); + + //In case we are in a cluster animation + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + + if (this._spiderfierOnRemove) { //TODO FIXME: Not sure how to have spiderfier add something on here nicely + this._spiderfierOnRemove(); + } + + + + //Clean up all the layers we added to the map + this._hideCoverage(); + this._featureGroup.onRemove(map); + this._nonPointGroup.onRemove(map); + + this._featureGroup.clearLayers(); + + this._map = null; + }, + + getVisibleParent: function (marker) { + var vMarker = marker; + while (vMarker && !vMarker._icon) { + vMarker = vMarker.__parent; + } + return vMarker || null; + }, + + //Remove the given object from the given array + _arraySplice: function (anArray, obj) { + for (var i = anArray.length - 1; i >= 0; i--) { + if (anArray[i] === obj) { + anArray.splice(i, 1); + return true; + } + } + }, + + //Internal function for removing a marker from everything. + //dontUpdateMap: set to true if you will handle updating the map manually (for bulk functions) + _removeLayer: function (marker, removeFromDistanceGrid, dontUpdateMap) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + fg = this._featureGroup, + map = this._map; + + //Remove the marker from distance clusters it might be in + if (removeFromDistanceGrid) { + for (var z = this._maxZoom; z >= 0; z--) { + if (!gridUnclustered[z].removeObject(marker, map.project(marker.getLatLng(), z))) { + break; + } + } + } + + //Work our way up the clusters removing them as we go if required + var cluster = marker.__parent, + markers = cluster._markers, + otherMarker; + + //Remove the marker from the immediate parents marker list + this._arraySplice(markers, marker); + + while (cluster) { + cluster._childCount--; + + if (cluster._zoom < 0) { + //Top level, do nothing + break; + } else if (removeFromDistanceGrid && cluster._childCount <= 1) { //Cluster no longer required + //We need to push the other marker up to the parent + otherMarker = cluster._markers[0] === marker ? cluster._markers[1] : cluster._markers[0]; + + //Update distance grid + gridClusters[cluster._zoom].removeObject(cluster, map.project(cluster._cLatLng, cluster._zoom)); + gridUnclustered[cluster._zoom].addObject(otherMarker, map.project(otherMarker.getLatLng(), cluster._zoom)); + + //Move otherMarker up to parent + this._arraySplice(cluster.__parent._childClusters, cluster); + cluster.__parent._markers.push(otherMarker); + otherMarker.__parent = cluster.__parent; + + if (cluster._icon) { + //Cluster is currently on the map, need to put the marker on the map instead + fg.removeLayer(cluster); + if (!dontUpdateMap) { + fg.addLayer(otherMarker); + } + } + } else { + cluster._recalculateBounds(); + if (!dontUpdateMap || !cluster._icon) { + cluster._updateIcon(); + } + } + + cluster = cluster.__parent; + } + + delete marker.__parent; + }, + + _isOrIsParent: function (el, oel) { + while (oel) { + if (el === oel) { + return true; + } + oel = oel.parentNode; + } + return false; + }, + + _propagateEvent: function (e) { + if (e.layer instanceof L.MarkerCluster) { + //Prevent multiple clustermouseover/off events if the icon is made up of stacked divs (Doesn't work in ie <= 8, no relatedTarget) + if (e.originalEvent && this._isOrIsParent(e.layer._icon, e.originalEvent.relatedTarget)) { + return; + } + e.type = 'cluster' + e.type; + } + + this.fire(e.type, e); + }, + + //Default functionality + _defaultIconCreateFunction: function (cluster) { + var childCount = cluster.getChildCount(); + + var c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + + return new L.DivIcon({ html: '
' + childCount + '
', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); + }, + + _bindEvents: function () { + var map = this._map, + spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick; + + //Zoom on cluster click or spiderfy if we are at the lowest level + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.on('clusterclick', this._zoomOrSpiderfy, this); + } + + //Show convex hull (boundary) polygon on mouse over + if (showCoverageOnHover) { + this.on('clustermouseover', this._showCoverage, this); + this.on('clustermouseout', this._hideCoverage, this); + map.on('zoomend', this._hideCoverage, this); + } + }, + + _zoomOrSpiderfy: function (e) { + var map = this._map; + if (map.getMaxZoom() === map.getZoom()) { + if (this.options.spiderfyOnMaxZoom) { + e.layer.spiderfy(); + } + } else if (this.options.zoomToBoundsOnClick) { + e.layer.zoomToBounds(); + } + + // Focus the map again for keyboard users. + if (e.originalEvent && e.originalEvent.keyCode === 13) { + map._container.focus(); + } + }, + + _showCoverage: function (e) { + var map = this._map; + if (this._inZoomAnimation) { + return; + } + if (this._shownPolygon) { + map.removeLayer(this._shownPolygon); + } + if (e.layer.getChildCount() > 2 && e.layer !== this._spiderfied) { + this._shownPolygon = new L.Polygon(e.layer.getConvexHull(), this.options.polygonOptions); + map.addLayer(this._shownPolygon); + } + }, + + _hideCoverage: function () { + if (this._shownPolygon) { + this._map.removeLayer(this._shownPolygon); + this._shownPolygon = null; + } + }, + + _unbindEvents: function () { + var spiderfyOnMaxZoom = this.options.spiderfyOnMaxZoom, + showCoverageOnHover = this.options.showCoverageOnHover, + zoomToBoundsOnClick = this.options.zoomToBoundsOnClick, + map = this._map; + + if (spiderfyOnMaxZoom || zoomToBoundsOnClick) { + this.off('clusterclick', this._zoomOrSpiderfy, this); + } + if (showCoverageOnHover) { + this.off('clustermouseover', this._showCoverage, this); + this.off('clustermouseout', this._hideCoverage, this); + map.off('zoomend', this._hideCoverage, this); + } + }, + + _zoomEnd: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + this._mergeSplitClusters(); + + this._zoom = this._map._zoom; + this._currentShownBounds = this._getExpandedVisibleBounds(); + }, + + _moveEnd: function () { + if (this._inZoomAnimation) { + return; + } + + var newBounds = this._getExpandedVisibleBounds(); + + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, newBounds); + this._topClusterLevel._recursivelyAddChildrenToMap(null, this._map._zoom, newBounds); + + this._currentShownBounds = newBounds; + return; + }, + + _generateInitialClusters: function () { + var maxZoom = this._map.getMaxZoom(), + radius = this.options.maxClusterRadius, + radiusFn = radius; + + //If we just set maxClusterRadius to a single number, we need to create + //a simple function to return that number. Otherwise, we just have to + //use the function we've passed in. + if (typeof radius !== "function") { + radiusFn = function () { return radius; }; + } + + if (this.options.disableClusteringAtZoom) { + maxZoom = this.options.disableClusteringAtZoom - 1; + } + this._maxZoom = maxZoom; + this._gridClusters = {}; + this._gridUnclustered = {}; + + //Set up DistanceGrids for each zoom + for (var zoom = maxZoom; zoom >= 0; zoom--) { + this._gridClusters[zoom] = new L.DistanceGrid(radiusFn(zoom)); + this._gridUnclustered[zoom] = new L.DistanceGrid(radiusFn(zoom)); + } + + this._topClusterLevel = new L.MarkerCluster(this, -1); + }, + + //Zoom: Zoom to start adding at (Pass this._maxZoom to start at the bottom) + _addLayer: function (layer, zoom) { + var gridClusters = this._gridClusters, + gridUnclustered = this._gridUnclustered, + markerPoint, z; + + if (this.options.singleMarkerMode) { + layer.options.icon = this.options.iconCreateFunction({ + getChildCount: function () { + return 1; + }, + getAllChildMarkers: function () { + return [layer]; + } + }); + } + + //Find the lowest zoom level to slot this one in + for (; zoom >= 0; zoom--) { + markerPoint = this._map.project(layer.getLatLng(), zoom); // calculate pixel position + + //Try find a cluster close by + var closest = gridClusters[zoom].getNearObject(markerPoint); + if (closest) { + closest._addChild(layer); + layer.__parent = closest; + return; + } + + //Try find a marker close by to form a new cluster with + closest = gridUnclustered[zoom].getNearObject(markerPoint); + if (closest) { + var parent = closest.__parent; + if (parent) { + this._removeLayer(closest, false); + } + + //Create new cluster with these 2 in it + + var newCluster = new L.MarkerCluster(this, zoom, closest, layer); + gridClusters[zoom].addObject(newCluster, this._map.project(newCluster._cLatLng, zoom)); + closest.__parent = newCluster; + layer.__parent = newCluster; + + //First create any new intermediate parent clusters that don't exist + var lastParent = newCluster; + for (z = zoom - 1; z > parent._zoom; z--) { + lastParent = new L.MarkerCluster(this, z, lastParent); + gridClusters[z].addObject(lastParent, this._map.project(closest.getLatLng(), z)); + } + parent._addChild(lastParent); + + //Remove closest from this zoom level and any above that it is in, replace with newCluster + for (z = zoom; z >= 0; z--) { + if (!gridUnclustered[z].removeObject(closest, this._map.project(closest.getLatLng(), z))) { + break; + } + } + + return; + } + + //Didn't manage to cluster in at this zoom, record us as a marker here and continue upwards + gridUnclustered[zoom].addObject(layer, markerPoint); + } + + //Didn't get in anything, add us to the top + this._topClusterLevel._addChild(layer); + layer.__parent = this._topClusterLevel; + return; + }, + + //Enqueue code to fire after the marker expand/contract has happened + _enqueue: function (fn) { + this._queue.push(fn); + if (!this._queueTimeout) { + this._queueTimeout = setTimeout(L.bind(this._processQueue, this), 300); + } + }, + _processQueue: function () { + for (var i = 0; i < this._queue.length; i++) { + this._queue[i].call(this); + } + this._queue.length = 0; + clearTimeout(this._queueTimeout); + this._queueTimeout = null; + }, + + //Merge and split any existing clusters that are too big or small + _mergeSplitClusters: function () { + + //Incase we are starting to split before the animation finished + this._processQueue(); + + if (this._zoom < this._map._zoom && this._currentShownBounds.intersects(this._getExpandedVisibleBounds())) { //Zoom in, split + this._animationStart(); + //Remove clusters now off screen + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, this._zoom, this._getExpandedVisibleBounds()); + + this._animationZoomIn(this._zoom, this._map._zoom); + + } else if (this._zoom > this._map._zoom) { //Zoom out, merge + this._animationStart(); + + this._animationZoomOut(this._zoom, this._map._zoom); + } else { + this._moveEnd(); + } + }, + + //Gets the maps visible bounds expanded in each direction by the size of the screen (so the user cannot see an area we do not cover in one pan) + _getExpandedVisibleBounds: function () { + if (!this.options.removeOutsideVisibleBounds) { + return this.getBounds(); + } + + var map = this._map, + bounds = map.getBounds(), + sw = bounds._southWest, + ne = bounds._northEast, + latDiff = L.Browser.mobile ? 0 : Math.abs(sw.lat - ne.lat), + lngDiff = L.Browser.mobile ? 0 : Math.abs(sw.lng - ne.lng); + + return new L.LatLngBounds( + new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true), + new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true)); + }, + + //Shared animation code + _animationAddLayerNonAnimated: function (layer, newCluster) { + if (newCluster === layer) { + this._featureGroup.addLayer(layer); + } else if (newCluster._childCount === 2) { + newCluster._addToMap(); + + var markers = newCluster.getAllChildMarkers(); + this._featureGroup.removeLayer(markers[0]); + this._featureGroup.removeLayer(markers[1]); + } else { + newCluster._updateIcon(); + } + } +}); + +L.MarkerClusterGroup.include(!L.DomUtil.TRANSITION ? { + + //Non Animated versions of everything + _animationStart: function () { + //Do nothing... + }, + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel); + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + + //We didn't actually animate, but we use this event to mean "clustering animations have finished" + this.fire('animationend'); + }, + _animationAddLayer: function (layer, newCluster) { + this._animationAddLayerNonAnimated(layer, newCluster); + } +} : { + + //Animated versions here + _animationStart: function () { + this._map._mapPane.className += ' leaflet-cluster-anim'; + this._inZoomAnimation++; + }, + _animationEnd: function () { + if (this._map) { + this._map._mapPane.className = this._map._mapPane.className.replace(' leaflet-cluster-anim', ''); + } + this._inZoomAnimation--; + this.fire('animationend'); + }, + _animationZoomIn: function (previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(), + fg = this._featureGroup, + i; + + //Add all children of current clusters to map and remove those clusters from map + this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { + var startPos = c._latlng, + markers = c._markers, + m; + + if (!bounds.contains(startPos)) { + startPos = null; + } + + if (c._isSingleParent() && previousZoomLevel + 1 === newZoomLevel) { //Immediately add the new child and remove us + fg.removeLayer(c); + c._recursivelyAddChildrenToMap(null, newZoomLevel, bounds); + } else { + //Fade out old cluster + c.setOpacity(0); + c._recursivelyAddChildrenToMap(startPos, newZoomLevel, bounds); + } + + //Remove all markers that aren't visible any more + //TODO: Do we actually need to do this on the higher levels too? + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + if (!bounds.contains(m._latlng)) { + fg.removeLayer(m); + } + } + + }); + + this._forceLayout(); + + //Update opacities + this._topClusterLevel._recursivelyBecomeVisible(bounds, newZoomLevel); + //TODO Maybe? Update markers in _recursivelyBecomeVisible + fg.eachLayer(function (n) { + if (!(n instanceof L.MarkerCluster) && n._icon) { + n.setOpacity(1); + } + }); + + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, newZoomLevel, function (c) { + c._recursivelyRestoreChildPositions(newZoomLevel); + }); + + //Remove the old clusters and close the zoom animation + this._enqueue(function () { + //update the positions of the just added clusters/markers + this._topClusterLevel._recursively(bounds, previousZoomLevel, 0, function (c) { + fg.removeLayer(c); + c.setOpacity(1); + }); + + this._animationEnd(); + }); + }, + + _animationZoomOut: function (previousZoomLevel, newZoomLevel) { + this._animationZoomOutSingle(this._topClusterLevel, previousZoomLevel - 1, newZoomLevel); + + //Need to add markers for those that weren't on the map before but are now + this._topClusterLevel._recursivelyAddChildrenToMap(null, newZoomLevel, this._getExpandedVisibleBounds()); + //Remove markers that were on the map before but won't be now + this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds, previousZoomLevel, this._getExpandedVisibleBounds()); + }, + _animationZoomOutSingle: function (cluster, previousZoomLevel, newZoomLevel) { + var bounds = this._getExpandedVisibleBounds(); + + //Animate all of the markers in the clusters to move to their cluster center point + cluster._recursivelyAnimateChildrenInAndAddSelfToMap(bounds, previousZoomLevel + 1, newZoomLevel); + + var me = this; + + //Update the opacity (If we immediately set it they won't animate) + this._forceLayout(); + cluster._recursivelyBecomeVisible(bounds, newZoomLevel); + + //TODO: Maybe use the transition timing stuff to make this more reliable + //When the animations are done, tidy up + this._enqueue(function () { + + //This cluster stopped being a cluster before the timeout fired + if (cluster._childCount === 1) { + var m = cluster._markers[0]; + //If we were in a cluster animation at the time then the opacity and position of our child could be wrong now, so fix it + m.setLatLng(m.getLatLng()); + if (m.setOpacity) { + m.setOpacity(1); + } + } else { + cluster._recursively(bounds, newZoomLevel, 0, function (c) { + c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel + 1); + }); + } + me._animationEnd(); + }); + }, + _animationAddLayer: function (layer, newCluster) { + var me = this, + fg = this._featureGroup; + + fg.addLayer(layer); + if (newCluster !== layer) { + if (newCluster._childCount > 2) { //Was already a cluster + + newCluster._updateIcon(); + this._forceLayout(); + this._animationStart(); + + layer._setPos(this._map.latLngToLayerPoint(newCluster.getLatLng())); + layer.setOpacity(0); + + this._enqueue(function () { + fg.removeLayer(layer); + layer.setOpacity(1); + + me._animationEnd(); + }); + + } else { //Just became a cluster + this._forceLayout(); + + me._animationStart(); + me._animationZoomOutSingle(newCluster, this._map.getMaxZoom(), this._map.getZoom()); + } + } + }, + + //Force a browser layout of stuff in the map + // Should apply the current opacity and location to all elements so we can update them again for an animation + _forceLayout: function () { + //In my testing this works, infact offsetWidth of any element seems to work. + //Could loop all this._layers and do this for each _icon if it stops working + + L.Util.falseFn(document.body.offsetWidth); + } +}); + +L.markerClusterGroup = function (options) { + return new L.MarkerClusterGroup(options); +}; + + +L.MarkerCluster = L.Marker.extend({ + initialize: function (group, zoom, a, b) { + + L.Marker.prototype.initialize.call(this, a ? (a._cLatLng || a.getLatLng()) : new L.LatLng(0, 0), { icon: this }); + + + this._group = group; + this._zoom = zoom; + + this._markers = []; + this._childClusters = []; + this._childCount = 0; + this._iconNeedsUpdate = true; + + this._bounds = new L.LatLngBounds(); + + if (a) { + this._addChild(a); + } + if (b) { + this._addChild(b); + } + }, + + //Recursively retrieve all child markers of this cluster + getAllChildMarkers: function (storageArray) { + storageArray = storageArray || []; + + for (var i = this._childClusters.length - 1; i >= 0; i--) { + this._childClusters[i].getAllChildMarkers(storageArray); + } + + for (var j = this._markers.length - 1; j >= 0; j--) { + storageArray.push(this._markers[j]); + } + + return storageArray; + }, + + //Returns the count of how many child markers we have + getChildCount: function () { + return this._childCount; + }, + + //Zoom to the minimum of showing all of the child markers, or the extents of this cluster + zoomToBounds: function () { + var childClusters = this._childClusters.slice(), + map = this._group._map, + boundsZoom = map.getBoundsZoom(this._bounds), + zoom = this._zoom + 1, + mapZoom = map.getZoom(), + i; + + //calculate how far we need to zoom down to see all of the markers + while (childClusters.length > 0 && boundsZoom > zoom) { + zoom++; + var newClusters = []; + for (i = 0; i < childClusters.length; i++) { + newClusters = newClusters.concat(childClusters[i]._childClusters); + } + childClusters = newClusters; + } + + if (boundsZoom > zoom) { + this._group._map.setView(this._latlng, zoom); + } else if (boundsZoom <= mapZoom) { //If fitBounds wouldn't zoom us down, zoom us down instead + this._group._map.setView(this._latlng, mapZoom + 1); + } else { + this._group._map.fitBounds(this._bounds); + } + }, + + getBounds: function () { + var bounds = new L.LatLngBounds(); + bounds.extend(this._bounds); + return bounds; + }, + + _updateIcon: function () { + this._iconNeedsUpdate = true; + if (this._icon) { + this.setIcon(this); + } + }, + + //Cludge for Icon, we pretend to be an icon for performance + createIcon: function () { + if (this._iconNeedsUpdate) { + this._iconObj = this._group.options.iconCreateFunction(this); + this._iconNeedsUpdate = false; + } + return this._iconObj.createIcon(); + }, + createShadow: function () { + return this._iconObj.createShadow(); + }, + + + _addChild: function (new1, isNotificationFromChild) { + + this._iconNeedsUpdate = true; + this._expandBounds(new1); + + if (new1 instanceof L.MarkerCluster) { + if (!isNotificationFromChild) { + this._childClusters.push(new1); + new1.__parent = this; + } + this._childCount += new1._childCount; + } else { + if (!isNotificationFromChild) { + this._markers.push(new1); + } + this._childCount++; + } + + if (this.__parent) { + this.__parent._addChild(new1, true); + } + }, + + //Expand our bounds and tell our parent to + _expandBounds: function (marker) { + var addedCount, + addedLatLng = marker._wLatLng || marker._latlng; + + if (marker instanceof L.MarkerCluster) { + this._bounds.extend(marker._bounds); + addedCount = marker._childCount; + } else { + this._bounds.extend(addedLatLng); + addedCount = 1; + } + + if (!this._cLatLng) { + // when clustering, take position of the first point as the cluster center + this._cLatLng = marker._cLatLng || addedLatLng; + } + + // when showing clusters, take weighted average of all points as cluster center + var totalCount = this._childCount + addedCount; + + //Calculate weighted latlng for display + if (!this._wLatLng) { + this._latlng = this._wLatLng = new L.LatLng(addedLatLng.lat, addedLatLng.lng); + } else { + this._wLatLng.lat = (addedLatLng.lat * addedCount + this._wLatLng.lat * this._childCount) / totalCount; + this._wLatLng.lng = (addedLatLng.lng * addedCount + this._wLatLng.lng * this._childCount) / totalCount; + } + }, + + //Set our markers position as given and add it to the map + _addToMap: function (startPos) { + if (startPos) { + this._backupLatlng = this._latlng; + this.setLatLng(startPos); + } + this._group._featureGroup.addLayer(this); + }, + + _recursivelyAnimateChildrenIn: function (bounds, center, maxZoom) { + this._recursively(bounds, 0, maxZoom - 1, + function (c) { + var markers = c._markers, + i, m; + for (i = markers.length - 1; i >= 0; i--) { + m = markers[i]; + + //Only do it if the icon is still on the map + if (m._icon) { + m._setPos(center); + m.setOpacity(0); + } + } + }, + function (c) { + var childClusters = c._childClusters, + j, cm; + for (j = childClusters.length - 1; j >= 0; j--) { + cm = childClusters[j]; + if (cm._icon) { + cm._setPos(center); + cm.setOpacity(0); + } + } + } + ); + }, + + _recursivelyAnimateChildrenInAndAddSelfToMap: function (bounds, previousZoomLevel, newZoomLevel) { + this._recursively(bounds, newZoomLevel, 0, + function (c) { + c._recursivelyAnimateChildrenIn(bounds, c._group._map.latLngToLayerPoint(c.getLatLng()).round(), previousZoomLevel); + + //TODO: depthToAnimateIn affects _isSingleParent, if there is a multizoom we may/may not be. + //As a hack we only do a animation free zoom on a single level zoom, if someone does multiple levels then we always animate + if (c._isSingleParent() && previousZoomLevel - 1 === newZoomLevel) { + c.setOpacity(1); + c._recursivelyRemoveChildrenFromMap(bounds, previousZoomLevel); //Immediately remove our children as we are replacing them. TODO previousBounds not bounds + } else { + c.setOpacity(0); + } + + c._addToMap(); + } + ); + }, + + _recursivelyBecomeVisible: function (bounds, zoomLevel) { + this._recursively(bounds, 0, zoomLevel, null, function (c) { + c.setOpacity(1); + }); + }, + + _recursivelyAddChildrenToMap: function (startPos, zoomLevel, bounds) { + this._recursively(bounds, -1, zoomLevel, + function (c) { + if (zoomLevel === c._zoom) { + return; + } + + //Add our child markers at startPos (so they can be animated out) + for (var i = c._markers.length - 1; i >= 0; i--) { + var nm = c._markers[i]; + + if (!bounds.contains(nm._latlng)) { + continue; + } + + if (startPos) { + nm._backupLatlng = nm.getLatLng(); + + nm.setLatLng(startPos); + if (nm.setOpacity) { + nm.setOpacity(0); + } + } + + c._group._featureGroup.addLayer(nm); + } + }, + function (c) { + c._addToMap(startPos); + } + ); + }, + + _recursivelyRestoreChildPositions: function (zoomLevel) { + //Fix positions of child markers + for (var i = this._markers.length - 1; i >= 0; i--) { + var nm = this._markers[i]; + if (nm._backupLatlng) { + nm.setLatLng(nm._backupLatlng); + delete nm._backupLatlng; + } + } + + if (zoomLevel - 1 === this._zoom) { + //Reposition child clusters + for (var j = this._childClusters.length - 1; j >= 0; j--) { + this._childClusters[j]._restorePosition(); + } + } else { + for (var k = this._childClusters.length - 1; k >= 0; k--) { + this._childClusters[k]._recursivelyRestoreChildPositions(zoomLevel); + } + } + }, + + _restorePosition: function () { + if (this._backupLatlng) { + this.setLatLng(this._backupLatlng); + delete this._backupLatlng; + } + }, + + //exceptBounds: If set, don't remove any markers/clusters in it + _recursivelyRemoveChildrenFromMap: function (previousBounds, zoomLevel, exceptBounds) { + var m, i; + this._recursively(previousBounds, -1, zoomLevel - 1, + function (c) { + //Remove markers at every level + for (i = c._markers.length - 1; i >= 0; i--) { + m = c._markers[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.setOpacity) { + m.setOpacity(1); + } + } + } + }, + function (c) { + //Remove child clusters at just the bottom level + for (i = c._childClusters.length - 1; i >= 0; i--) { + m = c._childClusters[i]; + if (!exceptBounds || !exceptBounds.contains(m._latlng)) { + c._group._featureGroup.removeLayer(m); + if (m.setOpacity) { + m.setOpacity(1); + } + } + } + } + ); + }, + + //Run the given functions recursively to this and child clusters + // boundsToApplyTo: a L.LatLngBounds representing the bounds of what clusters to recurse in to + // zoomLevelToStart: zoom level to start running functions (inclusive) + // zoomLevelToStop: zoom level to stop running functions (inclusive) + // runAtEveryLevel: function that takes an L.MarkerCluster as an argument that should be applied on every level + // runAtBottomLevel: function that takes an L.MarkerCluster as an argument that should be applied at only the bottom level + _recursively: function (boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel) { + var childClusters = this._childClusters, + zoom = this._zoom, + i, c; + + if (zoomLevelToStart > zoom) { //Still going down to required depth, just recurse to child clusters + for (i = childClusters.length - 1; i >= 0; i--) { + c = childClusters[i]; + if (boundsToApplyTo.intersects(c._bounds)) { + c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); + } + } + } else { //In required depth + + if (runAtEveryLevel) { + runAtEveryLevel(this); + } + if (runAtBottomLevel && this._zoom === zoomLevelToStop) { + runAtBottomLevel(this); + } + + //TODO: This loop is almost the same as above + if (zoomLevelToStop > zoom) { + for (i = childClusters.length - 1; i >= 0; i--) { + c = childClusters[i]; + if (boundsToApplyTo.intersects(c._bounds)) { + c._recursively(boundsToApplyTo, zoomLevelToStart, zoomLevelToStop, runAtEveryLevel, runAtBottomLevel); + } + } + } + } + }, + + _recalculateBounds: function () { + var markers = this._markers, + childClusters = this._childClusters, + i; + + this._bounds = new L.LatLngBounds(); + delete this._wLatLng; + + for (i = markers.length - 1; i >= 0; i--) { + this._expandBounds(markers[i]); + } + for (i = childClusters.length - 1; i >= 0; i--) { + this._expandBounds(childClusters[i]); + } + }, + + + //Returns true if we are the parent of only one cluster and that cluster is the same as us + _isSingleParent: function () { + //Don't need to check this._markers as the rest won't work if there are any + return this._childClusters.length > 0 && this._childClusters[0]._childCount === this._childCount; + } +}); + + + +L.DistanceGrid = function (cellSize) { + this._cellSize = cellSize; + this._sqCellSize = cellSize * cellSize; + this._grid = {}; + this._objectPoint = { }; +}; + +L.DistanceGrid.prototype = { + + addObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + stamp = L.Util.stamp(obj); + + this._objectPoint[stamp] = point; + + cell.push(obj); + }, + + updateObject: function (obj, point) { + this.removeObject(obj); + this.addObject(obj, point); + }, + + //Returns true if the object was found + removeObject: function (obj, point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + grid = this._grid, + row = grid[y] = grid[y] || {}, + cell = row[x] = row[x] || [], + i, len; + + delete this._objectPoint[L.Util.stamp(obj)]; + + for (i = 0, len = cell.length; i < len; i++) { + if (cell[i] === obj) { + + cell.splice(i, 1); + + if (len === 1) { + delete row[x]; + } + + return true; + } + } + + }, + + eachObject: function (fn, context) { + var i, j, k, len, row, cell, removed, + grid = this._grid; + + for (i in grid) { + row = grid[i]; + + for (j in row) { + cell = row[j]; + + for (k = 0, len = cell.length; k < len; k++) { + removed = fn.call(context, cell[k]); + if (removed) { + k--; + len--; + } + } + } + } + }, + + getNearObject: function (point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + i, j, k, row, cell, len, obj, dist, + objectPoint = this._objectPoint, + closestDistSq = this._sqCellSize, + closest = null; + + for (i = y - 1; i <= y + 1; i++) { + row = this._grid[i]; + if (row) { + + for (j = x - 1; j <= x + 1; j++) { + cell = row[j]; + if (cell) { + + for (k = 0, len = cell.length; k < len; k++) { + obj = cell[k]; + dist = this._sqDist(objectPoint[L.Util.stamp(obj)], point); + if (dist < closestDistSq) { + closestDistSq = dist; + closest = obj; + } + } + } + } + } + } + return closest; + }, + + _getCoord: function (x) { + return Math.floor(x / this._cellSize); + }, + + _sqDist: function (p, p2) { + var dx = p2.x - p.x, + dy = p2.y - p.y; + return dx * dx + dy * dy; + } +}; + + +/* Copyright (c) 2012 the authors listed at the following URL, and/or +the authors of referenced articles or incorporated external code: +http://en.literateprograms.org/Quickhull_(Javascript)?action=history&offset=20120410175256 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Retrieved from: http://en.literateprograms.org/Quickhull_(Javascript)?oldid=18434 +*/ + +(function () { + L.QuickHull = { + + /* + * @param {Object} cpt a point to be measured from the baseline + * @param {Array} bl the baseline, as represented by a two-element + * array of latlng objects. + * @returns {Number} an approximate distance measure + */ + getDistant: function (cpt, bl) { + var vY = bl[1].lat - bl[0].lat, + vX = bl[0].lng - bl[1].lng; + return (vX * (cpt.lat - bl[0].lat) + vY * (cpt.lng - bl[0].lng)); + }, + + /* + * @param {Array} baseLine a two-element array of latlng objects + * representing the baseline to project from + * @param {Array} latLngs an array of latlng objects + * @returns {Object} the maximum point and all new points to stay + * in consideration for the hull. + */ + findMostDistantPointFromBaseLine: function (baseLine, latLngs) { + var maxD = 0, + maxPt = null, + newPoints = [], + i, pt, d; + + for (i = latLngs.length - 1; i >= 0; i--) { + pt = latLngs[i]; + d = this.getDistant(pt, baseLine); + + if (d > 0) { + newPoints.push(pt); + } else { + continue; + } + + if (d > maxD) { + maxD = d; + maxPt = pt; + } + } + + return { maxPoint: maxPt, newPoints: newPoints }; + }, + + + /* + * Given a baseline, compute the convex hull of latLngs as an array + * of latLngs. + * + * @param {Array} latLngs + * @returns {Array} + */ + buildConvexHull: function (baseLine, latLngs) { + var convexHullBaseLines = [], + t = this.findMostDistantPointFromBaseLine(baseLine, latLngs); + + if (t.maxPoint) { // if there is still a point "outside" the base line + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([baseLine[0], t.maxPoint], t.newPoints) + ); + convexHullBaseLines = + convexHullBaseLines.concat( + this.buildConvexHull([t.maxPoint, baseLine[1]], t.newPoints) + ); + return convexHullBaseLines; + } else { // if there is no more point "outside" the base line, the current base line is part of the convex hull + return [baseLine[0]]; + } + }, + + /* + * Given an array of latlngs, compute a convex hull as an array + * of latlngs + * + * @param {Array} latLngs + * @returns {Array} + */ + getConvexHull: function (latLngs) { + // find first baseline + var maxLat = false, minLat = false, + maxPt = null, minPt = null, + i; + + for (i = latLngs.length - 1; i >= 0; i--) { + var pt = latLngs[i]; + if (maxLat === false || pt.lat > maxLat) { + maxPt = pt; + maxLat = pt.lat; + } + if (minLat === false || pt.lat < minLat) { + minPt = pt; + minLat = pt.lat; + } + } + var ch = [].concat(this.buildConvexHull([minPt, maxPt], latLngs), + this.buildConvexHull([maxPt, minPt], latLngs)); + return ch; + } + }; +}()); + +L.MarkerCluster.include({ + getConvexHull: function () { + var childMarkers = this.getAllChildMarkers(), + points = [], + p, i; + + for (i = childMarkers.length - 1; i >= 0; i--) { + p = childMarkers[i].getLatLng(); + points.push(p); + } + + return L.QuickHull.getConvexHull(points); + } +}); + + +//This code is 100% based on https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet +//Huge thanks to jawj for implementing it first to make my job easy :-) + +L.MarkerCluster.include({ + + _2PI: Math.PI * 2, + _circleFootSeparation: 25, //related to circumference of circle + _circleStartAngle: Math.PI / 6, + + _spiralFootSeparation: 28, //related to size of spiral (experiment!) + _spiralLengthStart: 11, + _spiralLengthFactor: 5, + + _circleSpiralSwitchover: 9, //show spiral instead of circle from this marker count upwards. + // 0 -> always spiral; Infinity -> always circle + + spiderfy: function () { + if (this._group._spiderfied === this || this._group._inZoomAnimation) { + return; + } + + var childMarkers = this.getAllChildMarkers(), + group = this._group, + map = group._map, + center = map.latLngToLayerPoint(this._latlng), + positions; + + this._group._unspiderfy(); + this._group._spiderfied = this; + + //TODO Maybe: childMarkers order by distance to center + + if (childMarkers.length >= this._circleSpiralSwitchover) { + positions = this._generatePointsSpiral(childMarkers.length, center); + } else { + center.y += 10; //Otherwise circles look wrong + positions = this._generatePointsCircle(childMarkers.length, center); + } + + this._animationSpiderfy(childMarkers, positions); + }, + + unspiderfy: function (zoomDetails) { + /// Argument from zoomanim if being called in a zoom animation or null otherwise + if (this._group._inZoomAnimation) { + return; + } + this._animationUnspiderfy(zoomDetails); + + this._group._spiderfied = null; + }, + + _generatePointsCircle: function (count, centerPt) { + var circumference = this._group.options.spiderfyDistanceMultiplier * this._circleFootSeparation * (2 + count), + legLength = circumference / this._2PI, //radius from circumference + angleStep = this._2PI / count, + res = [], + i, angle; + + res.length = count; + + for (i = count - 1; i >= 0; i--) { + angle = this._circleStartAngle + i * angleStep; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + } + + return res; + }, + + _generatePointsSpiral: function (count, centerPt) { + var legLength = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthStart, + separation = this._group.options.spiderfyDistanceMultiplier * this._spiralFootSeparation, + lengthFactor = this._group.options.spiderfyDistanceMultiplier * this._spiralLengthFactor, + angle = 0, + res = [], + i; + + res.length = count; + + for (i = count - 1; i >= 0; i--) { + angle += separation / legLength + i * 0.0005; + res[i] = new L.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))._round(); + legLength += this._2PI * lengthFactor / angle; + } + return res; + }, + + _noanimationUnspiderfy: function () { + var group = this._group, + map = group._map, + fg = group._featureGroup, + childMarkers = this.getAllChildMarkers(), + m, i; + + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + fg.removeLayer(m); + + if (m._preSpiderfyLatlng) { + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + } + if (m.setZIndexOffset) { + m.setZIndexOffset(0); + } + + if (m._spiderLeg) { + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + } + + group._spiderfied = null; + } +}); + +L.MarkerCluster.include(!L.DomUtil.TRANSITION ? { + //Non Animated versions of everything + _animationSpiderfy: function (childMarkers, positions) { + var group = this._group, + map = group._map, + fg = group._featureGroup, + i, m, leg, newPos; + + for (i = childMarkers.length - 1; i >= 0; i--) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + if (m.setZIndexOffset) { + m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING + } + + fg.addLayer(m); + + + leg = new L.Polyline([this._latlng, newPos], { weight: 1.5, color: '#222' }); + map.addLayer(leg); + m._spiderLeg = leg; + } + this.setOpacity(0.3); + group.fire('spiderfied'); + }, + + _animationUnspiderfy: function () { + this._noanimationUnspiderfy(); + } +} : { + //Animated versions here + SVG_ANIMATION: (function () { + return document.createElementNS('http://www.w3.org/2000/svg', 'animate').toString().indexOf('SVGAnimate') > -1; + }()), + + _animationSpiderfy: function (childMarkers, positions) { + var me = this, + group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerPos = map.latLngToLayerPoint(this._latlng), + i, m, leg, newPos; + + //Add markers to map hidden at our center point + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + //If it is a marker, add it now and we'll animate it out + if (m.setOpacity) { + m.setZIndexOffset(1000000); //Make these appear on top of EVERYTHING + m.setOpacity(0); + + fg.addLayer(m); + + m._setPos(thisLayerPos); + } else { + //Vectors just get immediately added + fg.addLayer(m); + } + } + + group._forceLayout(); + group._animationStart(); + + var initialLegOpacity = L.Path.SVG ? 0 : 0.3, + xmlns = L.Path.SVG_NS; + + + for (i = childMarkers.length - 1; i >= 0; i--) { + newPos = map.layerPointToLatLng(positions[i]); + m = childMarkers[i]; + + //Move marker to new position + m._preSpiderfyLatlng = m._latlng; + m.setLatLng(newPos); + + if (m.setOpacity) { + m.setOpacity(1); + } + + + //Add Legs. + leg = new L.Polyline([me._latlng, newPos], { weight: 1.5, color: '#222', opacity: initialLegOpacity }); + map.addLayer(leg); + m._spiderLeg = leg; + + //Following animations don't work for canvas + if (!L.Path.SVG || !this.SVG_ANIMATION) { + continue; + } + + //How this works: + //http://stackoverflow.com/questions/5924238/how-do-you-animate-an-svg-path-in-ios + //http://dev.opera.com/articles/view/advanced-svg-animation-techniques/ + + //Animate length + var length = leg._path.getTotalLength(); + leg._path.setAttribute("stroke-dasharray", length + "," + length); + + var anim = document.createElementNS(xmlns, "animate"); + anim.setAttribute("attributeName", "stroke-dashoffset"); + anim.setAttribute("begin", "indefinite"); + anim.setAttribute("from", length); + anim.setAttribute("to", 0); + anim.setAttribute("dur", 0.25); + leg._path.appendChild(anim); + anim.beginElement(); + + //Animate opacity + anim = document.createElementNS(xmlns, "animate"); + anim.setAttribute("attributeName", "stroke-opacity"); + anim.setAttribute("attributeName", "stroke-opacity"); + anim.setAttribute("begin", "indefinite"); + anim.setAttribute("from", 0); + anim.setAttribute("to", 0.5); + anim.setAttribute("dur", 0.25); + leg._path.appendChild(anim); + anim.beginElement(); + } + me.setOpacity(0.3); + + //Set the opacity of the spiderLegs back to their correct value + // The animations above override this until they complete. + // If the initial opacity of the spiderlegs isn't 0 then they appear before the animation starts. + if (L.Path.SVG) { + this._group._forceLayout(); + + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]._spiderLeg; + + m.options.opacity = 0.5; + m._path.setAttribute('stroke-opacity', 0.5); + } + } + + setTimeout(function () { + group._animationEnd(); + group.fire('spiderfied'); + }, 200); + }, + + _animationUnspiderfy: function (zoomDetails) { + var group = this._group, + map = group._map, + fg = group._featureGroup, + thisLayerPos = zoomDetails ? map._latLngToNewLayerPoint(this._latlng, zoomDetails.zoom, zoomDetails.center) : map.latLngToLayerPoint(this._latlng), + childMarkers = this.getAllChildMarkers(), + svg = L.Path.SVG && this.SVG_ANIMATION, + m, i, a; + + group._animationStart(); + + //Make us visible and bring the child markers back in + this.setOpacity(1); + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + //Marker was added to us after we were spidified + if (!m._preSpiderfyLatlng) { + continue; + } + + //Fix up the location to the real one + m.setLatLng(m._preSpiderfyLatlng); + delete m._preSpiderfyLatlng; + //Hack override the location to be our center + if (m.setOpacity) { + m._setPos(thisLayerPos); + m.setOpacity(0); + } else { + fg.removeLayer(m); + } + + //Animate the spider legs back in + if (svg) { + a = m._spiderLeg._path.childNodes[0]; + a.setAttribute('to', a.getAttribute('from')); + a.setAttribute('from', 0); + a.beginElement(); + + a = m._spiderLeg._path.childNodes[1]; + a.setAttribute('from', 0.5); + a.setAttribute('to', 0); + a.setAttribute('stroke-opacity', 0); + a.beginElement(); + + m._spiderLeg._path.setAttribute('stroke-opacity', 0); + } + } + + setTimeout(function () { + //If we have only <= one child left then that marker will be shown on the map so don't remove it! + var stillThereChildCount = 0; + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + if (m._spiderLeg) { + stillThereChildCount++; + } + } + + + for (i = childMarkers.length - 1; i >= 0; i--) { + m = childMarkers[i]; + + if (!m._spiderLeg) { //Has already been unspiderfied + continue; + } + + + if (m.setOpacity) { + m.setOpacity(1); + m.setZIndexOffset(0); + } + + if (stillThereChildCount > 1) { + fg.removeLayer(m); + } + + map.removeLayer(m._spiderLeg); + delete m._spiderLeg; + } + group._animationEnd(); + }, 200); + } +}); + + +L.MarkerClusterGroup.include({ + //The MarkerCluster currently spiderfied (if any) + _spiderfied: null, + + _spiderfierOnAdd: function () { + this._map.on('click', this._unspiderfyWrapper, this); + + if (this._map.options.zoomAnimation) { + this._map.on('zoomstart', this._unspiderfyZoomStart, this); + } + //Browsers without zoomAnimation or a big zoom don't fire zoomstart + this._map.on('zoomend', this._noanimationUnspiderfy, this); + + if (L.Path.SVG && !L.Browser.touch) { + this._map._initPathRoot(); + //Needs to happen in the pageload, not after, or animations don't work in webkit + // http://stackoverflow.com/questions/8455200/svg-animate-with-dynamically-added-elements + //Disable on touch browsers as the animation messes up on a touch zoom and isn't very noticable + } + }, + + _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(); //Ensure that markers are back where they should be + }, + + + //On zoom start we add a zoomanim handler so that we are guaranteed to be last (after markers are animated) + //This means we can define the animation they do rather than Markers doing an animation to their actual location + _unspiderfyZoomStart: function () { + if (!this._map) { //May have been removed from the map by a zoomEnd handler + return; + } + + this._map.on('zoomanim', this._unspiderfyZoomAnim, this); + }, + _unspiderfyZoomAnim: function (zoomDetails) { + //Wait until the first zoomanim after the user has finished touch-zooming before running the animation + if (L.DomUtil.hasClass(this._map._mapPane, 'leaflet-touching')) { + return; + } + + this._map.off('zoomanim', this._unspiderfyZoomAnim, this); + this._unspiderfy(zoomDetails); + }, + + + _unspiderfyWrapper: function () { + /// _unspiderfy but passes no arguments + this._unspiderfy(); + }, + + _unspiderfy: function (zoomDetails) { + if (this._spiderfied) { + this._spiderfied.unspiderfy(zoomDetails); + } + }, + + _noanimationUnspiderfy: function () { + if (this._spiderfied) { + this._spiderfied._noanimationUnspiderfy(); + } + }, + + //If the given layer is currently being spiderfied then we unspiderfy it so it isn't on the map anymore etc + _unspiderfyLayer: function (layer) { + if (layer._spiderLeg) { + this._featureGroup.removeLayer(layer); + + layer.setOpacity(1); + //Position will be fixed up immediately in _animationUnspiderfy + layer.setZIndexOffset(0); + + this._map.removeLayer(layer._spiderLeg); + delete layer._spiderLeg; + } + } +}); + + +}(window, document)); \ No newline at end of file diff --git a/js/meta_map.js b/js/meta_map.js old mode 100644 new mode 100755 index b87cd4b..654143d --- a/js/meta_map.js +++ b/js/meta_map.js @@ -1,656 +1,661 @@ -// docready -$(function() { - init(); - icons = prepareIcon(); - - $.ajax({ - url: 'data.php', - data: window.location.search.substring(1), - beforeSend: function() - { - ajaxModalTimeout = window.setTimeout("$('#waitModal').modal('show');", 1000); - }, - success: function(response) - { - communities = response.communities; - metaCommunities = response.metaCommunities; - addPoints2Map(response.allTheRouters); - }, - complete : function() - { - window.clearTimeout(ajaxModalTimeout); - $('#waitModal').modal('hide') - } - }); - -}); - -/** - * the leaflet map - * - * @var {object} - */ -var map; - -/** - * will hold the icons for nodes as created by prepareIcon() - * - * @var {array} - */ -var icons; - -/** - * all the communities - * - * @var {object} - */ -var communities, metaCommunities; // todo: make this none global - -/** - * a window timeout used for the modal shown if the ajax-fetch takes very long - * - * @var {timeout} - */ -var ajaxModalTimeout; - -/** - * will hold the layer-control - * - * @var {leafletControl} - */ -var layerControl; - -/** - * stores marker and area, that indicate tzhe user position - * after the map has been centered - * - * @var {object} - */ -var locMarker, locArea; - -/** - * will contain the outer hull of a cluster - */ -var clusterArea; - -/* - * configure prunecluster - */ -PruneCluster.Cluster.ENABLE_MARKERS_LIST = true; - -/** - * initialize map - * - * prepare mapcontainer - * set tile-layer - */ -function init() -{ - extendIcons(); - - var requestedLat = parseFloat(getURLParameter('lat')); - var requestedLng = parseFloat(getURLParameter('lng')); - var requestedZoom = parseInt(getURLParameter('z')); - - var startView = { - latitude: mapInitalView.latitude, - longitude: mapInitalView.longitude, - zoom: mapInitalView.zoom - }; - - if(requestedLat && requestedLng) - { - startView.latitude = requestedLat; - startView.longitude = requestedLng; - } - - if(requestedZoom) - { - startView.zoom = requestedZoom; - } - - map = L.map('map').setView([startView.latitude, startView.longitude], startView.zoom); - - L.tileLayer(tileServerUrl, - { - attribution: tileServerAttribution, - maxZoom: 18 - }).addTo(map); - - map.addControl(centerOnPosition); - - map.on('moveend overlayadd overlayremove', setDirectLink); - - setDirectLink(); - -} - -/** - * set some informations to the stat-tab in modal - * @param {int} comCount community count - * @param {int} nodeCount node count - */ -function setStats(comCount, nodeCount) -{ - $('#countCom').text(comCount); - $('#countNodes').text(nodeCount); -} - -/** - * creates a link for the current view and sets it in the modal - */ -function setDirectLink() -{ - var roundBy = 100000; - var z = map.getZoom(); - var pos = map.getCenter(); - var lat = Math.round(pos.lat * roundBy) / roundBy; - var lng = Math.round(pos.lng * roundBy) / roundBy; - - var layers = []; - - if(typeof layerControl != 'undefined') - { - // check for every layer if it is active - $.each(layerControl._layers, function(key, item) - { - if(map.hasLayer(item.layer)) - { - layers.push(item.name); - } - }); - } - - layers = layers.join('|'); - - var newLink = document.location.origin + document.location.pathname - +'?lat='+lat+'&lng='+lng+'&z='+z; - - // add layers only to query string if any is selected and its not only the default one - if(layers != '' && layers != 'Nodes') - { - newLink = newLink+'&l='+layers; - } - - $('#direktlink').text(newLink); - - - // set link to limited GPX file: - var bounds = map.getBounds(); - var newGpxLink = document.location.origin + '/fetch.php?content=gpxfile&minlat='+bounds.getSouth()+'&maxlat='+bounds.getNorth()+'&minlon='+bounds.getWest()+'&maxlon='+bounds.getEast(); - $('#gpxlink').attr("href", newGpxLink) -} - -/** - * returns icons for online/offline routers - * @return {object} object with 2 leaflet-icons - */ -function prepareIcon() -{ - var icon = L.icon({ - iconUrl: 'img/hotspot.png', - iconSize: [42, 27], // size of the icon - iconAnchor: [21, 13], // point of the icon which will correspond to marker's location - popupAnchor: [0, -9] // point from which the popup should open relative to the iconAnchor - }); - - var icon_off = L.icon({ - iconUrl:'img/hotspot_offline.png', - iconSize: [42, 27], // size of the icon - iconAnchor: [21, 13], // point of the icon which will correspond to marker's location - popupAnchor: [0, -9] // point from which the popup should open relative to the iconAnchor - }); - - return {hotspot:icon, hotspotOffline:icon_off}; -} - -/** - * parse data and add points to cluster-layer - * @param {object} data - */ -function addPoints2Map(data) -{ - var pruneCluster = preparePruneCluster(); - - // prepare heatmap overlay - var heatmapLayer = new HeatmapOverlay({ - "radius": 30, - "scaleRadius": false, - "useLocalExtrema": false - }); - var heatMapData = []; - - var heatmapUserLayer = new HeatmapOverlay({ - "radius": 50, - "scaleRadius": false, - "useLocalExtrema": false, - "valueField": 'count' - }); - var heatMapUserData = []; - - - // add all entries to clustergroup and heatmap - $.each(data, function(i, router) - { - heatMapData.push({ - lat: router.lat, - lng: router.long, - count: 1.1 - }); - - heatMapUserData.push({ - lat: router.lat, - lng: router.long, - count: router.clients - }); - - var marker = new PruneCluster.Marker(router.lat, router.long); - - marker.category = 0; - marker.data.icon = icons.hotspot; - - if(router.status == 'online') - { - marker.category = 1; - } - else if(router.status == 'offline') - { - marker.category = 2; - marker.data.icon = icons.hotspotOffline; - } - - marker.data.name = router.name; - marker.data.popup = getTooltipContent(router); - - pruneCluster.RegisterMarker(marker); - }); - - - // + Finally add the pruneCluster to the map Object. - map.addLayer(pruneCluster); - - // add heatmap layer - map.addLayer(heatmapLayer); - heatmapLayer.setData({ - max: 1, - data: heatMapData - }); - - map.addLayer(heatmapUserLayer); - heatmapUserLayer.setData({ - max: 1, - data: heatMapUserData - }); - - var layers = { - // add the cluster layer - "Nodes": pruneCluster, - - // add the heatmap layer - "HeatMap Router": heatmapLayer, - "HeatMap User": heatmapUserLayer - }; - - var selectedLayers = getURLParameter('l'); - - if(selectedLayers) - { - // layers have been preselected in the url - selectedLayers = selectedLayers.replace(/%20/g,' '); - selectedLayers = selectedLayers.split('|'); - - $.each(layers, function(key, layer) - { - if($.inArray(key, selectedLayers) != -1) - { - map.addLayer(layer); - } - else - { - map.removeLayer(layer); - } - }); - } - else - { - // hide heatmap layer by default - map.removeLayer(heatmapLayer); - map.removeLayer(heatmapUserLayer); - } - - // add layer controls for all layers - layerControl = L.control.layers({}, layers).addTo(map); - - // update stats - var countCom = 0; - for (var k in communities) - { - if (communities.hasOwnProperty(k)) - { - ++countCom; - } - } - - setStats(countCom, data.length); - setDirectLink(); -} - -/** - * get tooltip-html for router - * - * @param {object} routerData - * @return {string} - */ -function getTooltipContent(routerData) -{ - var tooltip = '

'+routerData.name+'

'; - - if(typeof communities[routerData.community] != 'undefined') - { - var thisRouterCommunity = communities[routerData.community]; - tooltip += '

Freifunk-Gruppe: '+thisRouterCommunity.name+'

'; - - if(thisRouterCommunity.meta !== false) - { - var metaName = thisRouterCommunity.meta; - - if( - typeof metaCommunities[metaName] != 'undefined' - && - typeof metaCommunities[metaName].url != 'undefined' - ) - { - // add link to metacommunity - metaName = ''+metaName+''; - } - - tooltip += '

Übergruppe: '+metaName+'

'; - } - - } - tooltip += '

'; - - if(routerData.clients != '?') - { - tooltip += 'verbundene Clients: '+routerData.clients+'
'; - } - - if(routerData.status != 'online' && routerData.status != '?') - { - tooltip += 'Router ist offline !'; - } - else if(routerData.status == '?') - { - tooltip += 'Routerstatus unbekannt'; - } - - tooltip += '

'; - - return tooltip; -} - -/** - * returns the value of a getparameter from the url - * - * uf no url is given, doc.loc.href is used - * - * @param {string} name - * @param {string|boolean} url - * @return string|null - */ -function getURLParameter(name, url) -{ - if(typeof url == 'undefined' || !url) - { - var url = document.location.href; - } - - return (RegExp(name + '=' + '(.+?)(&|$)').exec(url)||[,null])[1]; -} - -/* - * icon/Button zum Zentrieren der Karte - */ -L.Control.CenterOnPosition = L.Control.extend( -{ - options: - { - position: 'topleft', - }, - onAdd: function (map) - { - var controlDiv = L.DomUtil.create('div', 'leaflet-draw-toolbar leaflet-bar'); - - L.DomEvent - .addListener(controlDiv, 'click', L.DomEvent.stopPropagation) - .addListener(controlDiv, 'click', L.DomEvent.preventDefault) - .addListener(controlDiv, 'click', function () { - map.locate( - { - setView: true, - maxZoom: 18 - }); - }); - - var controlUI = L.DomUtil.create('a', 'leaflet-center-on-position', controlDiv); - controlUI.title = 'Auf Position zentrieren'; - controlUI.href = '#'; - - map.on('locationfound', onLocationFound); - - return controlDiv; - } -}); - -/** - * eventhandler called when a location is found - * - * this is triggered after the user has clicked the "center on position" - * button a valid position has been found. - * - * @param {event} e - */ -function onLocationFound(e) -{ - // remove previously added location-layers - if(locArea) - { - map.removeLayer(locArea); - map.removeLayer(locMarker); - } - - locMarker = L.marker(e.latlng) - .addTo(map) - .bindPopup("Du bist vermutlich innerhalb dieses Kreises") - .openPopup(); - - // draw a circle around the position indication accuracy - locArea = L.circle(e.latlng, (e.accuracy / 2)).addTo(map); - - window.setTimeout(function(){map.removeLayer(locArea);}, 3000); -} - -var centerOnPosition = new L.Control.CenterOnPosition(); - -/** - * shows the area of the hovered cluster - * @param {object} cluster - */ -function showClusterHull(cluster) -{ - var convexHull = new ConvexHullGrahamScan(); - - $.each(cluster._clusterMarkers, function(i, marker) - { - convexHull.addPoint(marker.position.lat, marker.position.lng); - }); - - var hullPoints = convexHull.getHull(); - var points = []; - - $.each(hullPoints, function(i, point) - { - points.push([point.x, point.y]); - }); - - clusterArea = L.polygon(points,{ - color: '#009ee0', - fillColor: '#009ee0', - fillOpacity: 0.5 - }).addTo(map); -} - -/** - * extend L.Icon for PruneCluster - */ -function extendIcons() -{ - // category-colors: status unknown, online, offline - var colors = ['#ff4b00', '#dc0067', '#666666'], - pi2 = Math.PI * 2; - - L.Icon.MarkerCluster = L.Icon.extend( - { - options: - { - iconSize: new L.Point(44, 44), - className: 'prunecluster leaflet-markercluster-icon' - }, - createIcon: function () - { - // based on L.Icon.Canvas from shramov/leaflet-plugins (BSD licence) - var e = document.createElement('canvas'); - this._setIconStyles(e, 'icon'); - var s = this.options.iconSize; - e.width = s.x; - e.height = s.y; - this.draw(e.getContext('2d'), s.x, s.y); - return e; - }, - createShadow: function () - { - return null; - }, - draw: function (canvas, width, height) - { - var lol = 0; - var start = 0; - - for (var i = 0, l = colors.length; i < l; ++i) - { - var size = this.stats[i] / this.population; - - if (size > 0) - { - canvas.beginPath(); - canvas.moveTo(22, 22); - canvas.fillStyle = colors[i]; - var from = start + 0.14, - to = start + size * pi2; - - if (to < from) - { - from = start; - } - - start = start + size * pi2; - - canvas.arc(22, 22, 18, from, to); - canvas.lineTo(22, 22); - canvas.fill(); - canvas.closePath(); - } - } - - canvas.beginPath(); - canvas.fillStyle = 'white'; - canvas.arc(22, 22, 14, 0, Math.PI * 2); - canvas.fill(); - canvas.closePath(); - canvas.fillStyle = '#666666'; - canvas.textAlign = 'center'; - canvas.textBaseline = 'middle'; - canvas.font = 'bold 12px sans-serif'; - canvas.fillText(this.population, 22, 22, 40); - } - }); -} - -/** - * creates a prune-cluster - * - * @return {object} - */ -function preparePruneCluster() -{ - // +--- Init the prune Cluste Plugin for Leaflet: https://github.com/SINTEF-9012/PruneCluster--------------- - var pruneCluster = new PruneClusterForLeaflet(); - - pruneCluster.BuildLeafletCluster = function(cluster, position) - { - var m = new L.Marker(position, - { - icon: pruneCluster.BuildLeafletClusterIcon(cluster) - }); - - m.on('click', function() - { - map.removeLayer(clusterArea); - - // Compute the cluster bounds (it's slow : O(n)) - var markersArea = pruneCluster.Cluster.FindMarkersInArea(cluster.bounds); - var b = pruneCluster.Cluster.ComputeBounds(markersArea); - - if (b) - { - var bounds = new L.LatLngBounds( - new L.LatLng(b.minLat, b.maxLng), - new L.LatLng(b.maxLat, b.minLng)); - - var zoomLevelBefore = pruneCluster._map.getZoom(); - var zoomLevelAfter = pruneCluster._map.getBoundsZoom(bounds, false, new L.Point(20, 20, null)); - - // If the zoom level doesn't change - if (zoomLevelAfter === zoomLevelBefore) - { - // Send an event for the LeafletSpiderfier - pruneCluster._map.fire('overlappingmarkers', { - cluster: pruneCluster, - markers: markersArea, - center: m.getLatLng(), - marker: m - }); - - pruneCluster._map.setView(position, zoomLevelAfter); - } - else - { - pruneCluster._map.fitBounds(bounds); - } - } - }) - .on('mouseover', function() - { - showClusterHull(cluster); - }) - .on('mouseout', function() - { - map.removeLayer(clusterArea); - }); - - return m; - }; - - // + Make a custom Icon for the Cluster. Also Taken from: https://github.com/SINTEF-9012/PruneCluster - pruneCluster.BuildLeafletClusterIcon = function (cluster) - { - var e = new L.Icon.MarkerCluster(); - e.stats = cluster.stats; - e.population = cluster.population; - return e; - }; - - pruneCluster.Cluster.Size = 100; - - return pruneCluster; -} +// docready +$(function() { + init(); + icons = prepareIcon(); + + $.ajax({ + url: 'data.php', + data: window.location.search.substring(1), + beforeSend: function() + { + ajaxModalTimeout = window.setTimeout("$('#waitModal').modal('show');", 1000); + }, + success: function(response) + { + communities = response.communities; + metaCommunities = response.metaCommunities; + addPoints2Map(response.allTheRouters); + }, + complete : function() + { + window.clearTimeout(ajaxModalTimeout); + $('#waitModal').modal('hide') + } + }); + +}); + +/** + * the leaflet map + * + * @var {object} + */ +var map; + +/** + * will hold the icons for nodes as created by prepareIcon() + * + * @var {array} + */ +var icons; + +/** + * all the communities + * + * @var {object} + */ +var communities, metaCommunities; // todo: make this none global + +/** + * a window timeout used for the modal shown if the ajax-fetch takes very long + * + * @var {timeout} + */ +var ajaxModalTimeout; + +/** + * will hold the layer-control + * + * @var {leafletControl} + */ +var layerControl; + +/** + * stores marker and area, that indicate tzhe user position + * after the map has been centered + * + * @var {object} + */ +var locMarker, locArea; + +/** + * will contain the outer hull of a cluster + */ +var clusterArea; + +/* + * configure prunecluster + */ +PruneCluster.Cluster.ENABLE_MARKERS_LIST = true; + +/** + * initialize map + * + * prepare mapcontainer + * set tile-layer + */ +function init() +{ + extendIcons(); + + var requestedLat = parseFloat(getURLParameter('lat')); + var requestedLng = parseFloat(getURLParameter('lng')); + var requestedZoom = parseInt(getURLParameter('z')); + + var startView = { + latitude: mapInitalView.latitude, + longitude: mapInitalView.longitude, + zoom: mapInitalView.zoom + }; + + if(requestedLat && requestedLng) + { + startView.latitude = requestedLat; + startView.longitude = requestedLng; + } + + if(requestedZoom) + { + startView.zoom = requestedZoom; + } + + map = L.map('map').setView([startView.latitude, startView.longitude], startView.zoom); + + L.tileLayer(tileServerUrl, + { + attribution: tileServerAttribution, + maxZoom: 18 + }).addTo(map); + + map.addControl(centerOnPosition); + + map.on('moveend overlayadd overlayremove', setDirectLink); + + setDirectLink(); + +} + +/** + * set some informations to the stat-tab in modal + * @param {int} comCount community count + * @param {int} nodeCount node count + */ +function setStats(comCount, nodeCount) +{ + $('#countCom').text(comCount); + $('#countNodes').text(nodeCount); +} + +/** + * creates a link for the current view and sets it in the modal + */ +function setDirectLink() +{ + var roundBy = 100000; + var z = map.getZoom(); + var pos = map.getCenter(); + var lat = Math.round(pos.lat * roundBy) / roundBy; + var lng = Math.round(pos.lng * roundBy) / roundBy; + + var layers = []; + + if(typeof layerControl != 'undefined') + { + // check for every layer if it is active + $.each(layerControl._layers, function(key, item) + { + if(map.hasLayer(item.layer)) + { + layers.push(item.name); + } + }); + } + + layers = layers.join('|'); + + var newLink = document.location.origin + document.location.pathname + +'?lat='+lat+'&lng='+lng+'&z='+z; + + // add layers only to query string if any is selected and its not only the default one + if(layers != '' && layers != 'Nodes') + { + newLink = newLink+'&l='+layers; + } + + $('#direktlink').text(newLink); + + + // set link to limited GPX file: + var bounds = map.getBounds(); + var newGpxLink = document.location.origin + '/fetch.php?content=gpxfile&minlat='+bounds.getSouth()+'&maxlat='+bounds.getNorth()+'&minlon='+bounds.getWest()+'&maxlon='+bounds.getEast(); + $('#gpxlink').attr("href", newGpxLink) +} + +/** + * returns icons for online/offline routers + * @return {object} object with 2 leaflet-icons + */ +function prepareIcon() +{ + var icon = L.icon({ + iconUrl: 'img/hotspot.png', + iconSize: [42, 27], // size of the icon + iconAnchor: [21, 13], // point of the icon which will correspond to marker's location + popupAnchor: [0, -9] // point from which the popup should open relative to the iconAnchor + }); + + var icon_off = L.icon({ + iconUrl:'img/hotspot_offline.png', + iconSize: [42, 27], // size of the icon + iconAnchor: [21, 13], // point of the icon which will correspond to marker's location + popupAnchor: [0, -9] // point from which the popup should open relative to the iconAnchor + }); + + return {hotspot:icon, hotspotOffline:icon_off}; +} + +/** + * parse data and add points to cluster-layer + * @param {object} data + */ +function addPoints2Map(data) +{ + var pruneCluster = preparePruneCluster(); + + // prepare heatmap overlay + var heatmapLayer = new HeatmapOverlay({ + "radius": 30, + "scaleRadius": false, + "useLocalExtrema": false + }); + var heatMapData = []; + + var heatmapUserLayer = new HeatmapOverlay({ + "radius": 50, + "scaleRadius": false, + "useLocalExtrema": false, + "valueField": 'count' + }); + var heatMapUserData = []; + + + // add all entries to clustergroup and heatmap + $.each(data, function(i, router) + { + if (isNaN(router.lat) || isNaN(router.long)) { + // usually none-numeric values should not reach this point, but lets be save + return; + } + + heatMapData.push({ + lat: router.lat, + lng: router.long, + count: 1.1 + }); + + heatMapUserData.push({ + lat: router.lat, + lng: router.long, + count: router.clients + }); + + var marker = new PruneCluster.Marker(router.lat, router.long); + + marker.category = 0; + marker.data.icon = icons.hotspot; + + if(router.status == 'online') + { + marker.category = 1; + } + else if(router.status == 'offline') + { + marker.category = 2; + marker.data.icon = icons.hotspotOffline; + } + + marker.data.name = router.name; + marker.data.popup = getTooltipContent(router); + + pruneCluster.RegisterMarker(marker); + }); + + + // + Finally add the pruneCluster to the map Object. + map.addLayer(pruneCluster); + + // add heatmap layer + map.addLayer(heatmapLayer); + heatmapLayer.setData({ + max: 1, + data: heatMapData + }); + + map.addLayer(heatmapUserLayer); + heatmapUserLayer.setData({ + max: 1, + data: heatMapUserData + }); + + var layers = { + // add the cluster layer + "Nodes": pruneCluster, + + // add the heatmap layer + "HeatMap Router": heatmapLayer, + "HeatMap User": heatmapUserLayer + }; + + var selectedLayers = getURLParameter('l'); + + if(selectedLayers) + { + // layers have been preselected in the url + selectedLayers = selectedLayers.replace(/%20/g,' '); + selectedLayers = selectedLayers.split('|'); + + $.each(layers, function(key, layer) + { + if($.inArray(key, selectedLayers) != -1) + { + map.addLayer(layer); + } + else + { + map.removeLayer(layer); + } + }); + } + else + { + // hide heatmap layer by default + map.removeLayer(heatmapLayer); + map.removeLayer(heatmapUserLayer); + } + + // add layer controls for all layers + layerControl = L.control.layers({}, layers).addTo(map); + + // update stats + var countCom = 0; + for (var k in communities) + { + if (communities.hasOwnProperty(k)) + { + ++countCom; + } + } + + setStats(countCom, data.length); + setDirectLink(); +} + +/** + * get tooltip-html for router + * + * @param {object} routerData + * @return {string} + */ +function getTooltipContent(routerData) +{ + var tooltip = '

'+routerData.name+'

'; + + if(typeof communities[routerData.community] != 'undefined') + { + var thisRouterCommunity = communities[routerData.community]; + tooltip += '

Freifunk-Gruppe: '+thisRouterCommunity.name+'

'; + + if(thisRouterCommunity.meta !== false) + { + var metaName = thisRouterCommunity.meta; + + if( + typeof metaCommunities[metaName] != 'undefined' + && + typeof metaCommunities[metaName].url != 'undefined' + ) + { + // add link to metacommunity + metaName = ''+metaName+''; + } + + tooltip += '

Übergruppe: '+metaName+'

'; + } + + } + tooltip += '

'; + + if(routerData.clients != '?') + { + tooltip += 'verbundene Clients: '+routerData.clients+'
'; + } + + if(routerData.status != 'online' && routerData.status != '?') + { + tooltip += 'Router ist offline !'; + } + else if(routerData.status == '?') + { + tooltip += 'Routerstatus unbekannt'; + } + + tooltip += '

'; + + return tooltip; +} + +/** + * returns the value of a getparameter from the url + * + * uf no url is given, doc.loc.href is used + * + * @param {string} name + * @param {string|boolean} url + * @return string|null + */ +function getURLParameter(name, url) +{ + if(typeof url == 'undefined' || !url) + { + var url = document.location.href; + } + + return (RegExp(name + '=' + '(.+?)(&|$)').exec(url)||[,null])[1]; +} + +/* + * icon/Button zum Zentrieren der Karte + */ +L.Control.CenterOnPosition = L.Control.extend( +{ + options: + { + position: 'topleft', + }, + onAdd: function (map) + { + var controlDiv = L.DomUtil.create('div', 'leaflet-draw-toolbar leaflet-bar'); + + L.DomEvent + .addListener(controlDiv, 'click', L.DomEvent.stopPropagation) + .addListener(controlDiv, 'click', L.DomEvent.preventDefault) + .addListener(controlDiv, 'click', function () { + map.locate( + { + setView: true, + maxZoom: 18 + }); + }); + + var controlUI = L.DomUtil.create('a', 'leaflet-center-on-position', controlDiv); + controlUI.title = 'Auf Position zentrieren'; + controlUI.href = '#'; + + map.on('locationfound', onLocationFound); + + return controlDiv; + } +}); + +/** + * eventhandler called when a location is found + * + * this is triggered after the user has clicked the "center on position" + * button a valid position has been found. + * + * @param {event} e + */ +function onLocationFound(e) +{ + // remove previously added location-layers + if(locArea) + { + map.removeLayer(locArea); + map.removeLayer(locMarker); + } + + locMarker = L.marker(e.latlng) + .addTo(map) + .bindPopup("Du bist vermutlich innerhalb dieses Kreises") + .openPopup(); + + // draw a circle around the position indication accuracy + locArea = L.circle(e.latlng, (e.accuracy / 2)).addTo(map); + + window.setTimeout(function(){map.removeLayer(locArea);}, 3000); +} + +var centerOnPosition = new L.Control.CenterOnPosition(); + +/** + * shows the area of the hovered cluster + * @param {object} cluster + */ +function showClusterHull(cluster) +{ + var convexHull = new ConvexHullGrahamScan(); + + $.each(cluster._clusterMarkers, function(i, marker) + { + convexHull.addPoint(marker.position.lat, marker.position.lng); + }); + + var hullPoints = convexHull.getHull(); + var points = []; + + $.each(hullPoints, function(i, point) + { + points.push([point.x, point.y]); + }); + + clusterArea = L.polygon(points,{ + color: '#009ee0', + fillColor: '#009ee0', + fillOpacity: 0.5 + }).addTo(map); +} + +/** + * extend L.Icon for PruneCluster + */ +function extendIcons() +{ + // category-colors: status unknown, online, offline + var colors = ['#ff4b00', '#dc0067', '#666666'], + pi2 = Math.PI * 2; + + L.Icon.MarkerCluster = L.Icon.extend( + { + options: + { + iconSize: new L.Point(44, 44), + className: 'prunecluster leaflet-markercluster-icon' + }, + createIcon: function () + { + // based on L.Icon.Canvas from shramov/leaflet-plugins (BSD licence) + var e = document.createElement('canvas'); + this._setIconStyles(e, 'icon'); + var s = this.options.iconSize; + e.width = s.x; + e.height = s.y; + this.draw(e.getContext('2d'), s.x, s.y); + return e; + }, + createShadow: function () + { + return null; + }, + draw: function (canvas, width, height) + { + var lol = 0; + var start = 0; + + for (var i = 0, l = colors.length; i < l; ++i) + { + var size = this.stats[i] / this.population; + + if (size > 0) + { + canvas.beginPath(); + canvas.moveTo(22, 22); + canvas.fillStyle = colors[i]; + var from = start + 0.14, + to = start + size * pi2; + + if (to < from) + { + from = start; + } + + start = start + size * pi2; + + canvas.arc(22, 22, 18, from, to); + canvas.lineTo(22, 22); + canvas.fill(); + canvas.closePath(); + } + } + + canvas.beginPath(); + canvas.fillStyle = 'white'; + canvas.arc(22, 22, 14, 0, Math.PI * 2); + canvas.fill(); + canvas.closePath(); + canvas.fillStyle = '#666666'; + canvas.textAlign = 'center'; + canvas.textBaseline = 'middle'; + canvas.font = 'bold 12px sans-serif'; + canvas.fillText(this.population, 22, 22, 40); + } + }); +} + +/** + * creates a prune-cluster + * + * @return {object} + */ +function preparePruneCluster() +{ + // +--- Init the prune Cluste Plugin for Leaflet: https://github.com/SINTEF-9012/PruneCluster--------------- + var pruneCluster = new PruneClusterForLeaflet(); + + pruneCluster.BuildLeafletCluster = function(cluster, position) + { + var m = new L.Marker(position, + { + icon: pruneCluster.BuildLeafletClusterIcon(cluster) + }); + + m.on('click', function() + { + map.removeLayer(clusterArea); + + // Compute the cluster bounds (it's slow : O(n)) + var markersArea = pruneCluster.Cluster.FindMarkersInArea(cluster.bounds); + var b = pruneCluster.Cluster.ComputeBounds(markersArea); + + if (b) + { + var bounds = new L.LatLngBounds( + new L.LatLng(b.minLat, b.maxLng), + new L.LatLng(b.maxLat, b.minLng)); + + var zoomLevelBefore = pruneCluster._map.getZoom(); + var zoomLevelAfter = pruneCluster._map.getBoundsZoom(bounds, false, new L.Point(20, 20, null)); + + // If the zoom level doesn't change + if (zoomLevelAfter === zoomLevelBefore) + { + // Send an event for the LeafletSpiderfier + pruneCluster._map.fire('overlappingmarkers', { + cluster: pruneCluster, + markers: markersArea, + center: m.getLatLng(), + marker: m + }); + + pruneCluster._map.setView(position, zoomLevelAfter); + } + else + { + pruneCluster._map.fitBounds(bounds); + } + } + }) + .on('mouseover', function() + { + showClusterHull(cluster); + }) + .on('mouseout', function() + { + map.removeLayer(clusterArea); + }); + + return m; + }; + + // + Make a custom Icon for the Cluster. Also Taken from: https://github.com/SINTEF-9012/PruneCluster + pruneCluster.BuildLeafletClusterIcon = function (cluster) + { + var e = new L.Icon.MarkerCluster(); + e.stats = cluster.stats; + e.population = cluster.population; + return e; + }; + + pruneCluster.Cluster.Size = 100; + + return pruneCluster; +} diff --git a/js/prunecluster/LICENSE.txt b/js/prunecluster/LICENSE.txt old mode 100644 new mode 100755 index 620cd45..62c5641 --- a/js/prunecluster/LICENSE.txt +++ b/js/prunecluster/LICENSE.txt @@ -1,21 +1,21 @@ -The MIT License (MIT) - -Copyright (c) 2014 SINTEF-9012 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +The MIT License (MIT) + +Copyright (c) 2014 SINTEF-9012 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/js/prunecluster/PruneCluster.js b/js/prunecluster/PruneCluster.js old mode 100644 new mode 100755 diff --git a/js/site.js b/js/site.js old mode 100644 new mode 100755 index 5177816..16c64a8 --- a/js/site.js +++ b/js/site.js @@ -1,26 +1,26 @@ -$(function() { - /** - * initialize modal tabs to load content via ajax if needed - */ - $('#informationModal .modalMainTabs a').click(function (e) - { - e.preventDefault() - var source = $(this).data('tabsource'); - - if(source) - { - var $target = $($(e.target).attr('href')); - - if(!$target.is('.contentLoaded')) - { - $target.load(source, function(x) - { - $target.addClass('contentLoaded'); - }); - } - - } - - $(this).tab('show'); - }) -}); +$(function() { + /** + * initialize modal tabs to load content via ajax if needed + */ + $('#informationModal .modalMainTabs a').click(function (e) + { + e.preventDefault() + var source = $(this).data('tabsource'); + + if(source) + { + var $target = $($(e.target).attr('href')); + + if(!$target.is('.contentLoaded')) + { + $target.load(source, function(x) + { + $target.addClass('contentLoaded'); + }); + } + + } + + $(this).tab('show'); + }) +}); diff --git a/lib/CommunityCacheHandler.php b/lib/CommunityCacheHandler.php new file mode 100644 index 0000000..6b8f83c --- /dev/null +++ b/lib/CommunityCacheHandler.php @@ -0,0 +1,177 @@ +setCachePath($path); + } + + /** + * @param string $path + */ + public function setCachePath(string $path) + { + $this->cachePath = $path; + } + + /** + * @param string $key + * @return string + */ + protected function getCachePathByKey(string $key): string + { + $targetDir = $this->cachePath . '/communities'; + $key = str_replace('/', '_', $key); + $key = str_replace('.', '', $key); + return $targetDir.'/c_' . $key . '.json'; + } + + /** + * @param string $communityKey + * @return false|mixed + */ + protected function getFromCacheFile(string $communityKey) + { + $filePath = $this->getCachePathByKey($communityKey); + + if (!file_exists($filePath)) { + return false; + } + + $fileContent = file_get_contents($filePath); + + $cacheData = json_decode($fileContent); + + if (!is_object($cacheData)) { + return false; + } + + return $cacheData; + } + + /** + * TODO external filehandler-class + * @param string $communityKey + * @param string $entryKey + * @param string $cacheTimeout + * @return false|object + */ + public function readCache(string $communityKey, string $entryKey, string $cacheTimeout) + { + if (isset($this->memoryCache[$communityKey])) { + $cacheData = $this->memoryCache[$communityKey]; + } else { + $cacheData = $this->getFromCacheFile($communityKey); + + if (!$cacheData) { + return false; + } + + $this->memoryCache[$communityKey] = $cacheData; + } + + + if (!isset($cacheData->$entryKey->updated)) { + return false; + } + + $updated = new \DateTime($cacheData->$entryKey->updated); + $cacheInvalidationTime = new \DateTime($cacheTimeout); + + // is it older than our $cacheTimeout limit? + if ($updated->getTimestamp() < $cacheInvalidationTime->getTimestamp()) { + return false; + } + + return isset($cacheData->$entryKey->content) + ? $cacheData->$entryKey->content + : false; + } + + /** + * TODO external filehandler-class + * @param string $communityKey + * @param string $entryKey + * @param object $data + */ + public function storeCache(string $communityKey, string $entryKey, object $data) + { + $cacheData = $this->getFromCacheFile($communityKey); + + if (!$cacheData) { + $cacheData = (object) [ + + ]; + } + + $now = new \DateTime(); + $cacheData->$entryKey = (object) [ + 'updated' => $now->format(\DateTime::ATOM), + 'content' => $data + ]; + + $targetDir = $this->cachePath . '/communities'; + + if (!is_dir($targetDir)) { + mkdir($targetDir); + } + + $filePath = $this->getCachePathByKey($communityKey); + file_put_contents($filePath, json_encode($cacheData)); + chmod($filePath, 0777); + + $this->memoryCache[$communityKey] = $cacheData; + } + + /** + * @param string $communityKey + * @param string $entryKey + */ + public function clearCacheEntry(string $communityKey, string $entryKey) + { + $cacheData = $this->getFromCacheFile($communityKey); + + if (!$cacheData) { + return; + } + + if (!isset($cacheData->$entryKey)) { + // is not set atm - do nothing + return; + } + + unset($cacheData->$entryKey); + $targetDir = $this->cachePath . '/communities'; + + if (!is_dir($targetDir)) { + mkdir($targetDir); + } + + $filePath = $this->getCachePathByKey($communityKey); + file_put_contents($filePath, json_encode($cacheData)); + chmod($filePath, 0777); + + $this->memoryCache[$communityKey] = $cacheData; + } +} diff --git a/lib/CommunityDebug.php b/lib/CommunityDebug.php new file mode 100644 index 0000000..c514a93 --- /dev/null +++ b/lib/CommunityDebug.php @@ -0,0 +1,53 @@ +debugLogCommunities[$communityData['name']])) { + $this->debugLogCommunities[$communityData['name']] = [ + 'name' => $communityData['name'], + 'apifile' => $communityData['source'], + 'message' => [] + ]; + } + + $this->debugLogCommunities[$communityData['name']]['message'][] = $message; + } + + /** + * @return array[] + */ + public function getDebugLog():array + { + return $this->debugLogCommunities; + } + + /** + * adds some basic information from the communityfile to the logging/debug-object + * + * @param object $community + * @param array $communityData + * @return void + */ + public function addBasicLogInfo(object $community, array $communityData) + { + $cName = $communityData['name']; + $this->debugLogCommunities[$cName]['claimed_nodecount'] = false; + + if (!empty($community->state) && !empty($community->state->nodes)) { + $this->debugLogCommunities[$cName]['claimed_nodecount'] = (int)$community->state->nodes; + } + + if (isset($community->metacommunity)) { + $this->debugLogCommunities[$cName]['metacommunity'] = $community->metacommunity; + } + } +} diff --git a/lib/CurlHelper.php b/lib/CurlHelper.php new file mode 100644 index 0000000..fbf7c3d --- /dev/null +++ b/lib/CurlHelper.php @@ -0,0 +1,88 @@ +callCounter; + } + + /** + * @param $url + * @return bool|string + */ + public function doCall($url) + { + $curlHandler = curl_init(); + curl_setopt($curlHandler, CURLOPT_URL, $url); + curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlHandler, CURLOPT_IPRESOLVE, CURL_VERSION_IPV6); + /* + * we often have communities that do not have their certificates in line + * this is risky though if someone wants to blow us up by sending a lot of garbage + */ + curl_setopt($curlHandler, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($curlHandler, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curlHandler, CURLOPT_MAXREDIRS, 2); + curl_setopt($curlHandler, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($curlHandler, CURLOPT_TIMEOUT, 20); + curl_setopt($curlHandler, CURLOPT_USERAGENT, 'php parser for http://www.freifunk-karte.de/'); + + $this->callCounter++; + $rawData = curl_exec($curlHandler); + $status = curl_getinfo($curlHandler); + + // a redirect was indicated + if (in_array((int)$status['http_code'], [301, 302])) { + $headerData = $this->getHeader($url); + list($header) = explode("\r\n\r\n", $headerData, 2); + + if ($header != '') { + $matches = []; + preg_match("/(Location:|URI:)[^(\n)]*/", $header, $matches); + + if (!isset($matches[0], $matches[1])) { + return false; + } + + $url = trim(str_replace($matches[1], "", $matches[0])); + $url_parsed = parse_url($url); + return (isset($url_parsed)) ? $this->doCall($url) : ''; + } + } + + curl_close($curlHandler); + + return $rawData; + } + + /** + * @param $url + * @return bool|string + */ + protected function getHeader($url) + { + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_NOBODY, true); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($curl, CURLOPT_TIMEOUT, 20); + $this->callCounter++; + $header = curl_exec($curl); + curl_close($curl); + return $header; + } +} diff --git a/lib/InfluxLog.php b/lib/InfluxLog.php new file mode 100644 index 0000000..2e155a2 --- /dev/null +++ b/lib/InfluxLog.php @@ -0,0 +1,63 @@ +dataBase = $database = Client::fromDSN($connectString); + } + + /** + * @param mixed[] $data + * @param string $measurementName + * @return bool + * @throws \InfluxDB\Exception + */ + public function logPoint( + array $data, + string $measurementName = 'parser_result_count' + ) { + if (!isset($data['value'])) { + throw new InvalidArgumentException('LogPoint data is missing a value'); + } + + $tags = isset($data['tags']) && is_array($data['tags']) ? $data['tags'] : []; + $fields = isset($data['fields']) && is_array($data['fields']) ? $data['fields'] : []; + + $points = [ + new Point( + $measurementName, + $data['value'], + $tags, + $fields, + ), + ]; + + return $this->dataBase->writePoints($points, Database::PRECISION_SECONDS); + } +} diff --git a/lib/NodeListParser.php b/lib/NodeListParser.php new file mode 100755 index 0000000..7ecd456 --- /dev/null +++ b/lib/NodeListParser.php @@ -0,0 +1,1075 @@ + the long timeout + * + * @var int + */ + private int $cacheTime = 31536000; + + private array $additionals = array(); + + private array $nodeList = array(); + private array $nodeListHashes = array(); + + private array $communityList = array(); + + private $logFile = null; + + private array $parseStatistics = array( + 'errorCommunities' => array() + ); + + private array $currentParseObject = array( + 'name' => '', + 'source' => '' + ); + + private int $maxAge = 3; + + private array $urlBlackList = array('http://map.freifunk-ruhrgebiet.de/nodes.json'); + + /** + * all communities that delivered a parsable nodelist + * + * If a valid nodelist has been found for a community, there will be no + * further parseatempts for this comm. - we will not try to load netmon or ffmap + * for those. + * + * @var array + */ + private array $nodelistCommunities = array(); + + /** + * @var CommunityCacheHandler + */ + private CommunityCacheHandler $CommunityCacheHandler; + + /** + * @var CurlHelper + */ + private CurlHelper $curlHelper; + + /** + * @var CommunityDebug + */ + private CommunityDebug $communityDebug; + + /** + * NodeListParser constructor. + * @param CommunityCacheHandler $cache + * @param CurlHelper $curlHelper + */ + public function __construct(CommunityCacheHandler $cache, CurlHelper $curlHelper) + { + $this->CommunityCacheHandler = $cache; + $this->curlHelper = $curlHelper; + $this->parseStatistics['timestamp'] = date('c'); + } + + public function __destruct() + { + if (!empty($this->logFile)) { + fclose($this->logFile); + } + } + + /** + * @param CommunityDebug $communityDebug + */ + public function setCommunityDebug(CommunityDebug $communityDebug) + { + $this->communityDebug = $communityDebug; + } + + /** + * The source url for the community API-file + * + * @param string $url + */ + public function setSource(string $url) + { + $this->sourceUrl = $url; + } + + public function setCachePath($path) + { + $this->cachePath = $path; + $this->prepareLogFile(); + } + + public function addAdditional($key, $item) + { + $this->additionals[$key] = $item; + } + + /** + * returns all node-date + * + * this will check the cache and - if needed - + * trigger a new parse of the api-files + * + * @param bool $force force reparse if true + * @return mixed + */ + public function getParsed($force = false): array + { + if ($force === true) { + $this->cacheTime = 0; + } + + $routerList = $this->fromCache('routers'); + $communities = $this->fromCache('communities'); + + if ($routerList == false) { + $this->log('need to reparse'); + $this->parseList(); + $this->log('parseList done'); + + $routerList = $this->nodeList; + $communities = $this->communityList; + + $this->toCache('routers', $this->nodeList); + $this->toCache('communities', $this->communityList); + + $this->parseStatistics['errorCommunities'] = $this->communityDebug->getDebugLog(); + $this->toCache('statistics', $this->parseStatistics); + } else { + $this->log('using cached result'); + } + + return [ + 'routerList' => $routerList, + 'communities' => $communities, + ]; + } + + /***************************** + * CACHE + */ + + /** + * get content by key from cache + * + * this will fetch the file only if older than $this->_cacheTime + * or there is no chached file + * + * @param string $key + * @return mixed|false + */ + private function fromCache(string $key) + { + $filename = $this->cachePath . 'result_' . $key . '.json'; + $changed = file_exists($filename) ? filemtime($filename) : 0; + $now = time(); + $diff = $now - $changed; + + if (!$changed || ($diff > $this->cacheTime)) { + return false; + } else { + return json_decode(file_get_contents($filename)); + } + } + + /** + * put something in the filecache + * @param string $key + * @param mixed $data + * @return bool + */ + private function toCache(string $key, $data): bool + { + $this->log('writing cache for ' . $key, false); + $filename = $this->cachePath . 'result_' . $key . '.json'; + $cache = fopen($filename, 'wb'); + $write = fwrite($cache, json_encode($data)); + fclose($cache); + + return ($write == true); + } + + /***************************** + * parsing + */ + + /** + * @return mixed + */ + private function getCommunityList() + { + $result = $this->curlHelper->doCall($this->sourceUrl); + return json_decode($result); + } + + /** + * @param string $cUrl + * @param string $cName + * @return false|mixed + */ + public function getCommunityData(string $cUrl, string $cName) + { + $cacheTimeout = '-1 day'; + $communityData = $this->CommunityCacheHandler->readCache( + $cName, + 'communityFile', + $cacheTimeout + ); + + if ($communityData) { + $this->log('using cached community data', false); + } else { + $communityFile = $this->curlHelper->doCall($cUrl); + + if ($communityFile) { + $communityData = json_decode($communityFile); + + if (is_object($communityData)) { + $this->log('caching community data', false); + $this->CommunityCacheHandler->storeCache( + $cName, + 'communityFile', + $communityData + ); + return $communityData; + } else { + $this->log('community data is not an object', false); + return false; + } + } else { + return false; + } + } + + // return cache; + return $communityData; + } + + /** + * @param $url + * @return false|string + */ + private function getUrl($url) + { + $urlParts = parse_url($url); + + if (isset($urlParts['path'])) { + $arr = explode('/', $urlParts['path']); + + if (sizeof($arr) > 1) { + if (strpos($arr[sizeof($arr) - 1], '.') !== false) { + $arr[sizeof($arr) - 1] = ''; + } + + $arr = array_filter($arr); + $urlParts['path'] = implode('/', $arr); + } + } + + if (empty($urlParts['host']) && empty($urlParts['path'])) { + return false; + } else { + if (empty($urlParts['scheme'])) { + $urlParts['scheme'] = 'http'; + } + + $preparedUrl = $urlParts['scheme'] . '://'; + + if (!empty($urlParts['host'])) { + $preparedUrl .= $urlParts['host'] . '/'; + } + + if (!empty($urlParts['path'])) { + $preparedUrl .= $urlParts['path'] . '/'; + } + } + + return $preparedUrl; + } + + /** + * search for and parse nodelists + * + * This looks for [nodeMaps] in format json - nodelist and parses them. + * This format takes priority over all other nodeMaps + * + * @param object $communityList all communities + * @return void + */ + private function parseNodeLists(object $communityList) + { + $parsedSources = array(); + + $parserNodeList = new parser\NodeList( + $this->CommunityCacheHandler, + $this->curlHelper, + $this->communityDebug, + [$this, 'addOrForget'] + ); + + $parserNodeList->setMaxAge($this->maxAge); + $this->parserNodeList = $parserNodeList; + $totalCommunityCount = count((array)$communityList); + $countIndex = 0; + + foreach ($communityList as $cName => $cUrl) { + $countIndex++; + $this->log('Step 1 of 3 - ' . $countIndex . ' / ' . $totalCommunityCount, false); + $this->log('parseNodeLists ' . $cName); + + $this->currentParseObject['name'] = $cName; + $this->currentParseObject['source'] = $cUrl; + + $communityData = $this->getCommunityData($cUrl, $cName); + + if ($communityData == false) { + $this->addCommunityMessage('got no data'); + continue; + } + + $this->addCommunityMessage('try to find nodeMaps containing [nodeList]-Format'); + $this->communityDebug->addBasicLogInfo($communityData, $this->currentParseObject); + + if (!isset($communityData->nodeMaps)) { + $this->addCommunityMessage('└ has no nodeMaps at all!'); + continue; + } + + $hasNodeList = false; + + // iterate over all nodeMaps-entries and look for nodelist + foreach ($communityData->nodeMaps as $nmEntry) { + if (isset($nmEntry->technicalType) + && isset($nmEntry->url) + && $nmEntry->technicalType == 'nodelist' + ) { + // we found one + $hasNodeList = true; + + $this->addCommunityMessage('├ found: ' . $nmEntry->url); + + if (in_array($nmEntry->url, $parsedSources)) { + // already parsed ( meta community?) + $this->addCommunityMessage('│├ already parsed - skipping - ' . $nmEntry->url); + continue; + } + + $this->addCommunityMessage('│├ parse as "nodeList"'); + $data = $parserNodeList->getFrom($this->currentParseObject, $nmEntry->url); + + if ($data) { + $communityData->shortName = $cName; + $this->addCommunity($communityData); + } + } + } + + if (!$hasNodeList) { + $this->addCommunityMessage('└ has no "technicalType": "nodeList"'); + } else { + $this->addCommunityMessage('└ finished prioritized "nodelist" parsing'); + } + } + + $this->nodelistCommunities = $parserNodeList->getNodelistCommunities(); + } + + /** + * parse all communities found in the api-file + */ + private function parseList() + { + // used to prevent duplicates + $parsedSources = array(); + $this->log('getCommunityList'); + $communityList = $this->getCommunityList(); + + /* first try to parse the nodelist-format + * we would prefer to use this. + * any community that delivers a nodelist will be skipped in the next step + */ + $this->log('_parseNodeLists'); + $this->parseNodeLists($communityList); + + $totalCommunityCount = count((array)$communityList); + $countIndex = 0; + + foreach ($communityList as $cName => $cUrl) { + $countIndex++; + $this->log('Step 2 of 3 - ' . $countIndex . ' / ' . $totalCommunityCount, false); + $this->log('parsing ' . $cName . " " . $cUrl); + $this->currentParseObject['name'] = $cName; + $this->currentParseObject['source'] = $cUrl; + + $this->addCommunityMessage('start checking for usable nodemaps'); + + // check if community has delivered a nodelist + if (in_array($cName, $this->nodelistCommunities)) { + $this->addCommunityMessage('skipping - we already have found a nodeList'); + $this->addCommunityMessage('This is good! nodeLists have priority over other map types.'); + $this->addCommunityMessage('Since we already found nodes for this community, no further search is required.'); + continue; + } + + $communityData = $this->getCommunityData($cUrl, $cName); + + if ($communityData == false) { + $this->addCommunityMessage('got no data'); + continue; + } + + $this->addCommunityMessage('try to find nodeMaps containing any parsable Format'); + $this->communityDebug->addBasicLogInfo($communityData, $this->currentParseObject); + + if (!isset($communityData->nodeMaps)) { + $this->addCommunityMessage('has no nodeMaps'); + continue; + } + + // this community belongs to a meta-community. + if (isset($communityData->metacommunity)) { + $this->addCommunityMessage('Metacommunity:' . $communityData->metacommunity); + } + + $communityData->shortName = $cName; + + $this->addCommunity($communityData); + + $data = false; + + $cachedNodeSource = $this->CommunityCacheHandler->readCache($cName, 'nodeSource', '-1 day'); + + if ($cachedNodeSource != false) { + $cachedUrl = $cachedNodeSource->url; + $cachedType = $cachedNodeSource->type; + + $this->addCommunityMessage('found cached url "'.$cachedUrl + .'" of type "'.$cachedType.'" less than one day old'); + + if (in_array($cachedUrl, $parsedSources)) { + // already parsed ( meta community?) + $this->addCommunityMessage('already parsed - skipping - ' . $cachedUrl); + continue; + } + + if (in_array(strtolower($cachedType), ['netmon', 'owm'])) { + // todo - this may be removed once cached is free of those + $this->addCommunityMessage( + '│└ Parser for '.$cachedType.' has been removed. ' + .'As of Dec 2020 it was not in active use.' + ); + } elseif (strtolower($cachedType) == 'nodelist') { + $this->addCommunityMessage('│├ type nodelist - has already been parsed'); + } else { + $parser = "getFrom" . $cachedType; + $data = $this->{$parser}($cName, $cachedUrl); + + if ($data !== false) { + // found something + $parsedSources[] = $cachedUrl; + } + } + } + + foreach ($communityData->nodeMaps as $nmEntry) { + if (isset($nmEntry->technicalType) + && isset($nmEntry->url) + ) { + $this->addCommunityMessage('├ found "'.$nmEntry->url.'" of type "'.$nmEntry->technicalType.'"'); + $url = $this->getUrl($nmEntry->url); + + if (!$url) { + // no usable url ignore this entry + $this->addCommunityMessage('│└ url broken'); + continue; + } + + if (in_array($url, $parsedSources)) { + // already parsed ( meta community?) + $this->addCommunityMessage('│├ already parsed - skipping - ' . $url); + $this->addCommunityMessage('│└ this happens if multiple communities use the same map url.'); + continue; + } + + $this->addCommunityMessage('│├ try to find parser for: ' . $url); + + if ($nmEntry->technicalType == 'netmon') { + $this->addCommunityMessage( + '│└ Parser for netmon has been removed. ' + .'As of Dec 2020 it was not in active use.' + ); + } elseif (in_array($nmEntry->technicalType, ['ffmap', 'meshviewer', 'hopglass'])) { + if (preg_match('/\.json$/', $nmEntry->url)) { + $url = $nmEntry->url; + } + + $this->addCommunityMessage('│├ parse as ffmap/meshviewer'); + $data = $this->getFromFfmap($cName, $url); + + if ($data !== false) { + $this->CommunityCacheHandler->storeCache( + $cName, + 'nodeSource', + (object) [ + 'url' => $url, + 'type' => 'Ffmap', + ] + ); + } + } elseif ($nmEntry->technicalType == 'openwifimap') { + $this->addCommunityMessage( + '│└ Parser for openwifimap has been removed. ' + .'As of Dec 2020 it was not in active use.' + ); + } elseif ($nmEntry->technicalType == 'nodelist') { + $this->addCommunityMessage('│├ type nodelist - has already been parsed'); + } else { + $this->addCommunityMessage('│├ no parser for: ' . $nmEntry->technicalType); + } + + if ($data !== false) { + // found something + $parsedSources[] = $url; + break; + } + } else { + $this->addCommunityMessage('│└ url or type missing'); + } + } + + if ($data === false) { + $this->addCommunityMessage('└ no parsable nodeMap found'); + } else { + $this->addCommunityMessage('└ parse nodeMap done'); + } + } + + + $totalCommunityCount = count((array)$this->additionals); + $countIndex = 0; + + foreach ($this->additionals as $cName => $community) { + $countIndex++; + $this->log('Step 3 of 3 - ' . $countIndex . ' / ' . $totalCommunityCount, false); + $this->log('parse fixed community ' . $cName); + $this->currentParseObject['name'] = $cName; + $this->currentParseObject['source'] = $community->url; + + $community->shortName = $cName; + $this->addCommunity($community); + + if (strtolower($community->parser) == 'nodelist') { + $data = $this->parserNodeList->getFrom($this->currentParseObject, $community->url); + } else { + $parser = "getFrom" . $community->parser; + $data = $this->{$parser}($cName, $community->url); + } + + if ($data !== false) { + // found something + $parsedSources[] = $community->url; + } + } + } + + /** + * adds a community with name and url to the communitylist for the result + * + * @param object $community + * @return bool + */ + private function addCommunity(object $community): bool + { + $thisComm = array( + 'name' => $community->name, + 'url' => $community->url, + 'meta' => false, + ); + + if (isset($community->homePage)) { + $thisComm['url'] = $community->homePage; + } + + // add metacommunity if set + if (isset($community->metacommunity)) { + $thisComm['meta'] = $community->metacommunity; + } + + if (!json_encode($thisComm)) { + $this->addCommunityMessage('name or url corrupt - ignoring'); + // error in some data - ignore community + return false; + } + + $this->communityList[$community->shortName] = $thisComm; + return true; + } + + /** + * @param string $comName + * @param string $comUrl + * @return array[] + */ + private function getNodesFromCachedFfmapUrl(string $comName, string $comUrl) + { + $routers = []; + // try readying cache + $this->addCommunityMessage('│├ checking for cached ffmap/meshviewer source URLs'); + $cachedValidSourceUrl = $this->CommunityCacheHandler->readCache( + $comName, + 'ffmapWorkingUrl'.md5($comUrl), + '-7 days' + ); + + if ($cachedValidSourceUrl !== false) { + if (is_string($cachedValidSourceUrl->resultUrl)) { + $cachedValidSourceUrl->resultUrl = [$cachedValidSourceUrl->resultUrl]; + } + + foreach ($cachedValidSourceUrl->resultUrl as $url) { + $this->addCommunityMessage('│├ cache-entry found: ' . $url); + do { + $result = $this->curlHelper->doCall($url); + + if (!$result) { + $this->addCommunityMessage('│└ ' . $url . ' returns no result'); + break; + } + + $responseObject = json_decode($result); + + if (!$responseObject) { + $this->addCommunityMessage('│└ ' . $url . ' returns no valid json'); + break; + } + + $routers = array_merge($routers, (array)$responseObject->nodes); + + if (!$routers) { + $this->addCommunityMessage('│└ ' . $url . ' contains no nodes'); + break; + } + + $this->addCommunityMessage('│├ ' . $url . ' returned something usable from cache'); + } while (false); + } + } + + if (empty($routers)) { + $this->CommunityCacheHandler->clearCacheEntry($comName, 'ffmapWorkingUrl'.md5($comUrl)); + } + + return $routers; + } + + /** + * @param string $comName + * @param string $comUrl + * @return bool + */ + private function getFromFfmap(string $comName, string $comUrl): bool + { + $urls = [ + 'options' => [], + 'must_check' => [], + ]; + + $routers = false; + + $this->getNodesFromCachedFfmapUrl($comName, $comUrl); + + $gotResult = !empty($routers); + + if (!$gotResult) { + $gotResult = false; + + if (!preg_match('/\.json$/', $comUrl)) { + // try to get config.json + $configUrl = $comUrl . '/config.json'; + $this->addCommunityMessage('Looking for config.json at ' . $configUrl); + $configResult = $this->curlHelper->doCall($configUrl); + + if (!$configResult) { + $this->addCommunityMessage($configUrl . ' returns no result'); + } else { + $responseObject = json_decode($configResult); + + if (!$responseObject) { + $this->addCommunityMessage($configUrl . ' returns no valid json'); + } elseif (empty($responseObject->dataPath)) { + $this->addCommunityMessage($configUrl . ' contains no dataPath'); + } else { + if (!is_array($responseObject->dataPath)) { + $responseObject->dataPath = array($responseObject->dataPath); + } else { + $this->addCommunityMessage('this seems to be a HopGlass-config'); + } + + foreach ($responseObject->dataPath as $path) { + $path = $path . 'nodes.json'; + + if (!preg_match('/https?:/', $path)) { + $parts = parse_url($comUrl); + $path = $parts['scheme'] . '://' . $parts['host'] . '/' . ltrim($path, '/'); + } + + $this->addCommunityMessage('adding dataPath:' . $path . ' to url-list'); + $urls['must_check'][] = $path; + } + } + } + + $urls['options'][] = $comUrl . 'nodes.json'; + $urls['options'][] = $comUrl . 'data/nodes.json'; + $urls['options'][] = $comUrl . 'json/nodes.json'; + $urls['options'][] = $comUrl . 'meshviewer/data/meshviewer.json'; + $urls['options'][] = $comUrl . 'data/meshviewer.json'; + + if (preg_match('/\/meshviewer\//', $comUrl)) { + $comUrl = str_replace('/meshviewer', '', $comUrl); + $urls['options'][] = $comUrl . 'nodes.json'; + $urls['options'][] = $comUrl . 'data/nodes.json'; + $urls['options'][] = $comUrl . 'json/nodes.json'; + $urls['options'][] = $comUrl . 'json/ffka/nodes.json'; + } + } + + $urls['options'][] = $comUrl; + $goodUrls = []; + + foreach ($urls['must_check'] as $urlTry) { + $routersFound = $this->ffmapUrlCheck($urlTry); + + if (empty($routersFound)) { + // got nothing + continue; + } + + $goodUrls[] = $urlTry; + + if (is_array($routers)) { + array_merge($routers, $routersFound); + } else { + $routers = $routersFound; + } + } + + if (!empty($routers)) { + $this->CommunityCacheHandler->storeCache( + $comName, + 'ffmapWorkingUrl' . md5($comUrl), + (object)[ + 'resultUrl' => $goodUrls, + 'mapUrl' => $comUrl + ] + ); + + // we have something to work with + $gotResult = true; + } + + if (!$gotResult) { + foreach ($urls['options'] as $urlTry) { + $routers = $this->ffmapUrlCheck($urlTry); + + if (empty($routers)) { + // got nothing + continue; + } + + $this->CommunityCacheHandler->storeCache( + $comName, + 'ffmapWorkingUrl' . md5($comUrl), + (object)[ + 'resultUrl' => [$urlTry], + 'mapUrl' => $comUrl + ] + ); + + // we have something to work with + $gotResult = true; + break; + } + } + } + + if (!$gotResult) { + $this->addCommunityMessage('│└ sorry - found no nodes.json :-('); + return false; + } + + $this->addCommunityMessage('│├ found a nodes.json - start parsing'); + + $counter = 0; + $skipped = 0; + $duplicates = 0; + $added = 0; + $dead = 0; + + foreach ($routers as $router) { + $counter++; + + if (!empty($router->nodeinfo->location)) { + // new style + if (empty($router->nodeinfo->location->latitude) + || empty($router->nodeinfo->location->longitude) + || !is_numeric($router->nodeinfo->location->latitude) + || !is_numeric($router->nodeinfo->location->longitude)) { + // router has no location + $skipped++; + continue; + } + + if (!$router->flags->online) { + if (!isset($router->lastseen)) { + $dead++; + continue; + } + + $date = date_create((string)$router->lastseen); + + // was online in last 24h ? + if ((time() - $date->getTimestamp()) > 60 * 60 * 24 * $this->maxAge) { + // router has been offline for a long time now + $dead++; + continue; + } + } + + $thisRouter = [ + 'id' => (string)$router->nodeinfo->node_id, + 'lat' => (string)$router->nodeinfo->location->latitude, + 'long' => (string)$router->nodeinfo->location->longitude, + 'name' => (string)$router->nodeinfo->hostname, + 'community' => $comName, + 'status' => $router->flags->online ? 'online' : 'offline', + 'clients' => isset($router->statistics->clients) + ? $this->getClientCount($router->statistics->clients) + : 0, + ]; + } elseif (!empty($router->location)) { + // new style + if (empty($router->location->latitude) || empty($router->location->longitude)) { + // router has no location + $skipped++; + continue; + } + + if (isset($router->flags) && !$router->flags->online) { + // router is offline and we don't know how long - skip + $dead++; + continue; + } elseif (isset($router->is_online)) { + if (!$router->is_online) { + // router is offline and we don't know how long - skip + $dead++; + continue; + } + $router->flags = new \stdClass(); + $router->flags->online = true; + } + + $thisRouter = [ + 'id' => (string)$router->node_id, + 'lat' => (string)$router->location->latitude, + 'long' => (string)$router->location->longitude, + 'name' => (string)$router->hostname, + 'community' => $comName, + 'status' => $router->flags->online ? 'online' : 'offline', + 'clients' => isset($router->clients) ? $this->getClientCount($router->clients) : 0, + ]; + } else { + // old style + if (empty($router->geo[0]) || empty($router->geo[1])) { + // router has no location + $skipped++; + continue; + } + + if (!$router->flags->online) { + // touter is offline and we don't know how long - skip + $dead++; + continue; + } + + $thisRouter = array( + 'id' => (string)$router->name, + 'lat' => (string)$router->geo[0], + 'long' => (string)$router->geo[1], + 'name' => (string)$router->name, + 'community' => $comName, + 'status' => $router->flags->online ? 'online' : 'offline', + 'clients' => 0 + ); + + if (!empty($router->clientcount)) { + $thisRouter['clients'] = (int)$router->clientcount; + } + } + + // add to routerlist for later use in JS + if ($this->addOrForget($thisRouter)) { + $added++; + } else { + $duplicates++; + } + } + + $this->addCommunityMessage('│└ parsing done. ' . + $counter . ' nodes found, ' . + $added . ' added, ' . + $skipped . ' skipped, ' . + $duplicates . ' duplicates, ' . + $dead . ' dead'); + + return true; + } + + /** + * @param string $url + * @return array|object + */ + private function ffmapUrlCheck(string $urlTry) + { + $this->addCommunityMessage($urlTry . ' try to fetch'); + + if (in_array($urlTry, $this->urlBlackList)) { + $this->addCommunityMessage($urlTry . ' is blacklisted - skipping'); + return []; + } + + $result = $this->curlHelper->doCall($urlTry); + + if (!$result) { + $this->addCommunityMessage($urlTry . ' returns no result'); + return []; + } + + $responseObject = json_decode($result); + + if (!$responseObject) { + $this->addCommunityMessage($urlTry . ' returns no valid json'); + return []; + } + + $routers = $responseObject->nodes; + + if (!$routers) { + $this->addCommunityMessage($urlTry . ' contains no nodes'); + return []; + } + + return $routers; + } + + /** + * @param $clients mixed[]|int + * @return int + */ + private function getClientCount($clients): int + { + $clientCount = 0; + + if (is_array($clients) or is_object($clients)) { + if (is_countable($clients)) { + $clientCount = sizeof($clients); + } elseif (isset($clients->total)) { + $clientCount = (int)$clients->total; + } + } elseif (is_numeric($clients)) { + $clientCount = (int)$clients; + } + + return $clientCount; + } + + /** + * add a node to the list or skip if it is already in the list + * + * a hash of name, id and location ist used for deduplication + * + * @param mixed[] $node + * @return bool + */ + public function addOrForget(array $node): bool + { + $identifier = $node['name'] . $node['id']; + + $key = md5($identifier . $node['lat'] . $node['long']); + + if (!isset($this->nodeListHashes[$key])) { + array_push($this->nodeList, $node); + $this->nodeListHashes[$key] = $this->currentParseObject['name']; + + return true; + } + + return false; + } + + /** + * @return void + */ + private function prepareLogFile() + { + $this->logFile = fopen($this->cachePath . "logfile.txt", "a"); + } + + /** + * @param string $msg + * @param bool $inlcudeStats + * @return void + */ + private function log(string $msg, bool $inlcudeStats = true) + { + echo date("d.m.Y, H:i:s", time()) . ' ' . $msg . "\n"; + + if ($inlcudeStats) { + echo 'MemoryUsage: ' . round(memory_get_peak_usage(true) / 1024 / 1024, 1) . "MB\n"; + echo "Found nodes: " . count($this->nodeList) . "\n\n"; + } + + flush(); + + if ($this->logFile) { + fputs( + $this->logFile, + date("d.m.Y, H:i:s", time()) . ' ' . + $msg . + "\n" . + 'total nodes found: ' . count($this->nodeList) . + "\n" + ); + } + } + + /** + * returns the array with info about the parseprocess + * @return mixed[] + */ + public function getParseStatistics(): array + { + return $this->parseStatistics; + } + + /** + * adds an message-entry for the current community + * @param string $message + */ + private function addCommunityMessage(string $message) + { + $this->communityDebug->addMessage($message, $this->currentParseObject); + } +} diff --git a/lib/jsv4/Jsv4.php b/lib/jsv4/Jsv4.php new file mode 100755 index 0000000..489fdda --- /dev/null +++ b/lib/jsv4/Jsv4.php @@ -0,0 +1,553 @@ +valid; + } + + static public function coerce($data, $schema) { + if (is_object($data) || is_array($data)) { + $data = unserialize(serialize($data)); + } + $result = new Jsv4($data, $schema, FALSE, TRUE); + if ($result->valid) { + $result->value = $result->data; + } + return $result; + } + + static public function pointerJoin($parts) { + $result = ""; + foreach ($parts as $part) { + $part = str_replace("~", "~0", $part); + $part = str_replace("/", "~1", $part); + $result .= "/".$part; + } + return $result; + } + + static public function recursiveEqual($a, $b) { + if (is_object($a)) { + if (!is_object($b)) { + return FALSE; + } + foreach ($a as $key => $value) { + if (!isset($b->$key)) { + return FALSE; + } + if (!self::recursiveEqual($value, $b->$key)) { + return FALSE; + } + } + foreach ($b as $key => $value) { + if (!isset($a->$key)) { + return FALSE; + } + } + return TRUE; + } + if (is_array($a)) { + if (!is_array($b)) { + return FALSE; + } + foreach ($a as $key => $value) { + if (!isset($b[$key])) { + return FALSE; + } + if (!self::recursiveEqual($value, $b[$key])) { + return FALSE; + } + } + foreach ($b as $key => $value) { + if (!isset($a[$key])) { + return FALSE; + } + } + return TRUE; + } + return $a === $b; + } + + + private $data; + private $schema; + private $firstErrorOnly; + private $coerce; + public $valid; + public $errors; + + private function __construct(&$data, $schema, $firstErrorOnly=FALSE, $coerce=FALSE) { + $this->data =& $data; + $this->schema =& $schema; + $this->firstErrorOnly = $firstErrorOnly; + $this->coerce = $coerce; + $this->valid = TRUE; + $this->errors = array(); + + try { + $this->checkTypes(); + $this->checkEnum(); + $this->checkObject(); + $this->checkArray(); + $this->checkString(); + $this->checkNumber(); + $this->checkComposite(); + } catch (Jsv4Error $e) { + } + } + + private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors=NULL) { + $this->valid = FALSE; + $error = new Jsv4Error($code, $dataPath, $schemaPath, $errorMessage, $subErrors); + $this->errors[] = $error; + if ($this->firstErrorOnly) { + throw $error; + } + } + + private function subResult(&$data, $schema, $allowCoercion=TRUE) { + return new Jsv4($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); + } + + private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) { + if (!$subResult->valid) { + $this->valid = FALSE; + foreach ($subResult->errors as $error) { + $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); + } + } + } + + private function checkTypes() { + if (isset($this->schema->type)) { + $types = $this->schema->type; + if (!is_array($types)) { + $types = array($types); + } + foreach ($types as $type) { + if ($type == "object" && is_object($this->data)) { + return; + } elseif ($type == "array" && is_array($this->data)) { + return; + } elseif ($type == "string" && is_string($this->data)) { + return; + } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { + return; + } elseif ($type == "integer" && is_int($this->data)) { + return; + } elseif ($type == "boolean" && is_bool($this->data)) { + return; + } elseif ($type == "null" && $this->data === NULL) { + return; + } + } + + if ($this->coerce) { + foreach ($types as $type) { + if ($type == "number") { + if (is_numeric($this->data)) { + $this->data = (float)$this->data; + return; + } else if (is_bool($this->data)) { + $this->data = $this->data ? 1 : 0; + return; + } + } else if ($type == "integer") { + if ((int)$this->data == $this->data) { + $this->data = (int)$this->data; + return; + } + } else if ($type == "string") { + if (is_numeric($this->data)) { + $this->data = "".$this->data; + return; + } else if (is_bool($this->data)) { + $this->data = ($this->data) ? "true" : "false"; + return; + } else if (is_null($this->data)) { + $this->data = ""; + return; + } + } else if ($type == "boolean") { + if (is_numeric($this->data)) { + $this->data = ($this->data != "0"); + return; + } else if ($this->data == "yes" || $this->data == "true") { + $this->data = TRUE; + return; + } else if ($this->data == "no" || $this->data == "false") { + $this->data = FALSE; + return; + } else if ($this->data == NULL) { + $this->data = FALSE; + return; + } + } + } + } + + $type = gettype($this->data); + if ($type == "double") { + $type = ((int)$this->data == $this->data) ? "integer" : "number"; + } else if ($type == "NULL") { + $type = "null"; + } + $this->fail(JSV4_INVALID_TYPE, "", "/type", "Invalid type: $type"); + } + } + + private function checkEnum() { + if (isset($this->schema->enum)) { + foreach ($this->schema->enum as $option) { + if (self::recursiveEqual($this->data, $option)) { + return; + } + } + $this->fail(JSV4_ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); + } + } + + private function checkObject() { + if (!is_object($this->data)) { + return; + } + if (isset($this->schema->required) && is_array($this->schema->required)) { + foreach ($this->schema->required as $index => $key) { + if (!array_key_exists($key, (array) $this->data)) { + if ($this->coerce && $this->createValueForProperty($key)) { + continue; + } + $this->fail(JSV4_OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); + } + } + } + $checkedProperties = array(); + if (isset($this->schema->properties)) { + foreach ($this->schema->properties as $key => $subSchema) { + $checkedProperties[$key] = TRUE; + if (array_key_exists($key, (array) $this->data)) { + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("properties", $key))); + } + } + } + if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + foreach ($this->data as $key => &$subValue) { + if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { + $checkedProperties[$key] = TRUE; + $subResult = $this->subResult($this->data->$key, $subSchema); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("patternProperties", $pattern))); + } + } + } + } + if (isset($this->schema->additionalProperties)) { + $additionalProperties = $this->schema->additionalProperties; + foreach ($this->data as $key => &$subValue) { + if (isset($checkedProperties[$key])) { + continue; + } + if (!$additionalProperties) { + $this->fail(JSV4_OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); + } else if (is_object($additionalProperties)) { + $subResult = $this->subResult($subValue, $additionalProperties); + $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); + } + } + } + if (isset($this->schema->dependencies)) { + foreach ($this->schema->dependencies as $key => $dep) { + if (!isset($this->data->$key)) { + continue; + } + if (is_object($dep)) { + $subResult = $this->subResult($this->data, $dep); + $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); + } else if (is_array($dep)) { + foreach ($dep as $index => $depKey) { + if (!isset($this->data->$depKey)) { + $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); + } + } + } else { + if (!isset($this->data->$dep)) { + $this->fail(JSV4_OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); + } + } + } + } + if (isset($this->schema->minProperties)) { + if (count(get_object_vars($this->data)) < $this->schema->minProperties) { + $this->fail(JSV4_OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); + } + } + if (isset($this->schema->maxProperties)) { + if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { + $this->fail(JSV4_OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); + } + } + } + + private function checkArray() { + if (!is_array($this->data)) { + return; + } + if (isset($this->schema->items)) { + $items = $this->schema->items; + if (is_array($items)) { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + if (isset($items[$index])) { + $subResult = $this->subResult($subData, $items[$index]); + $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); + } else if (isset($this->schema->additionalItems)) { + $additionalItems = $this->schema->additionalItems; + if (!$additionalItems) { + $this->fail(JSV4_ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index ".count($items)." or more) are not allowed"); + } else if ($additionalItems !== TRUE) { + $subResult = $this->subResult($subData, $additionalItems); + $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); + } + } + } + } else { + foreach ($this->data as $index => &$subData) { + if (!is_numeric($index)) { + throw new Exception("Arrays must only be numerically-indexed"); + } + $subResult = $this->subResult($subData, $items); + $this->includeSubResult($subResult, "/{$index}", "/items"); + } + } + } + if (isset($this->schema->minItems)) { + if (count($this->data) < $this->schema->minItems) { + $this->fail(JSV4_ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); + } + } + if (isset($this->schema->maxItems)) { + if (count($this->data) > $this->schema->maxItems) { + $this->fail(JSV4_ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); + } + } + if (isset($this->schema->uniqueItems)) { + foreach ($this->data as $indexA => $itemA) { + foreach ($this->data as $indexB => $itemB) { + if ($indexA < $indexB) { + if (self::recursiveEqual($itemA, $itemB)) { + $this->fail(JSV4_ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); + break 2; + } + } + } + } + } + } + + private function checkString() { + if (!is_string($this->data)) { + return; + } + if (isset($this->schema->minLength)) { + if (strlen($this->data) < $this->schema->minLength) { + $this->fail(JSV4_STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); + } + } + if (isset($this->schema->maxLength)) { + if (strlen($this->data) > $this->schema->maxLength) { + $this->fail(JSV4_STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); + } + } + if (isset($this->schema->pattern)) { + $pattern = $this->schema->pattern; + $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; + $result = preg_match("/".str_replace("/", "\\/", $pattern)."/".$patternFlags, $this->data); + if ($result === 0) { + $this->fail(JSV4_STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); + } + } + } + + private function checkNumber() { + if (is_string($this->data) || !is_numeric($this->data)) { + return; + } + if (isset($this->schema->multipleOf)) { + if (fmod($this->data/$this->schema->multipleOf, 1) != 0) { + $this->fail(JSV4_NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); + } + } + if (isset($this->schema->minimum)) { + $minimum = $this->schema->minimum; + if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { + if ($this->data <= $minimum) { + $this->fail(JSV4_NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); + } + } else { + if ($this->data < $minimum) { + $this->fail(JSV4_NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); + } + } + } + if (isset($this->schema->maximum)) { + $maximum = $this->schema->maximum; + if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { + if ($this->data >= $maximum) { + $this->fail(JSV4_NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); + } + } else { + if ($this->data > $maximum) { + $this->fail(JSV4_NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); + } + } + } + } + + private function checkComposite() { + if (isset($this->schema->allOf)) { + foreach ($this->schema->allOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + $this->includeSubResult($subResult, "", "/allOf/".(int)$index); + } + } + if (isset($this->schema->anyOf)) { + $failResults = array(); + foreach ($this->schema->anyOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + if ($subResult->valid) { + return; + } + $failResults[] = $subResult; + } + $this->fail(JSV4_ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); + } + if (isset($this->schema->oneOf)) { + $failResults = array(); + $successIndex = NULL; + foreach ($this->schema->oneOf as $index => $subSchema) { + $subResult = $this->subResult($this->data, $subSchema, FALSE); + if ($subResult->valid) { + if ($successIndex === NULL) { + $successIndex = $index; + } else { + $this->fail(JSV4_ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); + } + continue; + } + $failResults[] = $subResult; + } + if ($successIndex === NULL) { + $this->fail(JSV4_ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); + } + } + if (isset($this->schema->not)) { + $subResult = $this->subResult($this->data, $this->schema->not, FALSE); + if ($subResult->valid) { + $this->fail(JSV4_NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); + } + } + } + + private function createValueForProperty($key) { + $schema = NULL; + if (isset($this->schema->properties->$key)) { + $schema = $this->schema->properties->$key; + } else if (isset($this->schema->patternProperties)) { + foreach ($this->schema->patternProperties as $pattern => $subSchema) { + if (preg_match("/".str_replace("/", "\\/", $pattern)."/", $key)) { + $schema = $subSchema; + break; + } + } + } + if (!$schema && isset($this->schema->additionalProperties)) { + $schema = $this->schema->additionalProperties; + } + if ($schema) { + if (isset($schema->default)) { + $this->data->$key = unserialize(serialize($schema->default)); + return TRUE; + } + if (isset($schema->type)) { + $types = is_array($schema->type) ? $schema->type : array($schema->type); + if (in_array("null", $types)) { + $this->data->$key = NULL; + } elseif (in_array("boolean", $types)) { + $this->data->$key = TRUE; + } elseif (in_array("integer", $types) || in_array("number", $types)) { + $this->data->$key = 0; + } elseif (in_array("string", $types)) { + $this->data->$key = ""; + } elseif (in_array("object", $types)) { + $this->data->$key = new StdClass; + } elseif (in_array("array", $types)) { + $this->data->$key = array(); + } else { + return FALSE; + } + } + return TRUE; + } + return FALSE; + } +} + +class Jsv4Error extends Exception { + public $code; + public $dataPath; + public $schemaPath; + public $message; + + public function __construct($code, $dataPath, $schemaPath, $errorMessage, $subResults=NULL) { + parent::__construct($errorMessage); + $this->code = $code; + $this->dataPath = $dataPath; + $this->schemaPath = $schemaPath; + $this->message = $errorMessage; + if ($subResults) { + $this->subResults = $subResults; + } + } + + public function prefix($dataPrefix, $schemaPrefix) { + return new Jsv4Error($this->code, $dataPrefix.$this->dataPath, $schemaPrefix.$this->schemaPath, $this->message); + } +} + +?> diff --git a/lib/jsv4/LICENSE-MIT.txt b/lib/jsv4/LICENSE-MIT.txt old mode 100644 new mode 100755 index e5bc0b5..e7f27fa --- a/lib/jsv4/LICENSE-MIT.txt +++ b/lib/jsv4/LICENSE-MIT.txt @@ -1,7 +1,7 @@ -Copyright (C) 2013 Geraint Luff - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Copyright (C) 2013 Geraint Luff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/jsv4/LICENSE.txt b/lib/jsv4/LICENSE.txt old mode 100644 new mode 100755 index e4f4b8a..6a4fa5a --- a/lib/jsv4/LICENSE.txt +++ b/lib/jsv4/LICENSE.txt @@ -1,6 +1,6 @@ -Author: Geraint Luff -Year: 2013 - -This code is released into the "public domain" by its author(s). Anybody may use, alter and distribute the code without restriction. The author makes no guarantees, and takes no liability of any kind for use of this code. - -If you find a bug or make an improvement, it would be courteous to let the author know, but it is not compulsory. +Author: Geraint Luff +Year: 2013 + +This code is released into the "public domain" by its author(s). Anybody may use, alter and distribute the code without restriction. The author makes no guarantees, and takes no liability of any kind for use of this code. + +If you find a bug or make an improvement, it would be courteous to let the author know, but it is not compulsory. diff --git a/lib/log.php b/lib/log.php old mode 100644 new mode 100755 index 510523b..8754938 --- a/lib/log.php +++ b/lib/log.php @@ -1,45 +1,65 @@ -_db = $db; - } - - public function add($count) - { - $query = "INSERT INTO log SET `count` = ".(int)$count; - $this->_db->query($query); - } - - public function get() - { - $query = "SELECT TS, - MIN(count) AS daymin, - MAX(count) AS daymax - FROM `log` - WHERE `count` > 1000 - GROUP BY DATE(`ts`) - ORDER BY ts"; - - $result = $this->_db->query($query); - - $resultArray = array(); - - while($row = mysqli_fetch_assoc($result)) - { - $date = date_create((string)$row['TS']); - $ts = $date->getTimestamp(); - - $resultArray[] = array( - 'ts' => $ts, - 'max' => $row['daymax'], - 'min' => $row['daymin'] - ); - } - - return $resultArray; - } -} \ No newline at end of file +db = $db; + } + + /** + * @param int $count + */ + public function add(int $count) + { + $query = "INSERT INTO log SET `count` = ".(int)$count; + $this->db->query($query); + } + + /** + * @return array[] + */ + public function get(): array + { + $query = "SELECT + TS, + MIN(count) AS daymin, + MAX(count) AS daymax + FROM + `log` + WHERE + `count` > 5000 + GROUP BY DATE(`ts`) + ORDER BY ts"; + $result = $this->db->query($query); + + $resultArray = array(); + $lastMin = 0; + + while ($row = mysqli_fetch_assoc($result)) { + $date = date_create((string)$row['TS']); + $ts = $date->getTimestamp(); + + if ($row['daymin'] > ($lastMin * 0.75)) { + $resultArray[] = array( + 'ts' => $ts, + 'max' => $row['daymax'], + 'min' => $row['daymin'] + ); + } + $lastMin = $row['daymin']; + } + + return $resultArray; + } +} diff --git a/lib/parser/Base.php b/lib/parser/Base.php new file mode 100644 index 0000000..3f61845 --- /dev/null +++ b/lib/parser/Base.php @@ -0,0 +1,61 @@ +communityCacheHandler = $cache; + $this->curlHelper = $curlHelper; + $this->communityDebug = $communityDebug; + $this->perNodeCallback= $callback; + } + + /** + * @param int $maxAge + */ + public function setMaxAge(int $maxAge) + { + $this->maxAge = $maxAge; + } +} diff --git a/lib/parser/NodeList.php b/lib/parser/NodeList.php new file mode 100644 index 0000000..a141fdb --- /dev/null +++ b/lib/parser/NodeList.php @@ -0,0 +1,166 @@ +nodelistCommunities; + } + + /** + * @param array $currentParseObject + * @param string $comUrl + * @return bool + */ + public function getFrom(array $currentParseObject, string $comUrl): bool + { + $this->currentParseObject = $currentParseObject; + $comName = $this->currentParseObject['name']; + $result = $this->curlHelper->doCall($comUrl); + + $responseObject = json_decode($result); + + if (!$responseObject) { + $this->addCommunityMessage('│└ ' . $comUrl . ' returns no valid json'); + return false; + } + + $schemaString = file_get_contents(__DIR__ . '/../../schema/nodelist-schema-1.0.0.json'); + $schema = json_decode($schemaString); + $validationResult = \Jsv4::validate($responseObject, $schema); + + if (!$validationResult) { + $this->addCommunityMessage('│├ ' . $comUrl . ' is no valid nodelist'); + $this->addCommunityMessage('│└ check https://github.com/ffansbach/nodelist for nodelist-schema'); + return false; + } + + if (empty($responseObject->nodes)) { + $this->addCommunityMessage('│└ ' . $comUrl . ' contains no nodes'); + return false; + } + + $routers = $responseObject->nodes; + + // add community to the list of nodelist-communities + // this will make us skipp further search for other formats + $this->nodelistCommunities[] = $comName; + + $counter = 0; + $skipped = 0; + $duplicates = 0; + $added = 0; + $dead = 0; + + foreach ($routers as $router) { + $counter++; + + $location = $this->getLocation($router); + + if (!$location) { + // router has no location + $skipped++; + continue; + } + + $thisRouter = [ + 'id' => (string)$router->id, + 'lat' => (string)$location->lat, + 'long' => (string) $location->long, + 'name' => isset($router->name) ? (string)$router->name : (string)$router->id, + 'community' => $comName, + 'status' => 'unknown', + 'clients' => 0, + ]; + + if (isset($router->status)) { + if (isset($router->status->clients)) { + $thisRouter['clients'] = (int)$router->status->clients; + } + + if (isset($router->status->online)) { + $thisRouter['status'] = (bool)$router->status->online ? 'online' : 'offline'; + } + } + + + if ($thisRouter['status'] == 'offline') { + if (empty($router->status->lastcontact)) { + $isDead = true; + } else { + $date = date_create((string)$router->status->lastcontact); + + // was online in last days? ? + $isDead = ((time() - $date->getTimestamp()) > 60 * 60 * 24 * $this->maxAge); + } + + if ($isDead) { + $dead++; + continue; + } + } + + // add to routerlist for later use in JS + if (call_user_func($this->perNodeCallback, $thisRouter)) { + $added++; + } else { + $duplicates++; + } + } + + $this->addCommunityMessage('│└ parsing done. ' . + $counter . ' nodes found, ' . + $added . ' added, ' . + $skipped . ' skipped, ' . + $duplicates . ' duplicates, ' . + $dead . ' dead'); + + return true; + } + + /** + * @param object $router + * @return false|object + */ + protected function getLocation(object $router) + { + if (empty($router->position->lat) + || + ( + empty($router->position->lon) + && + empty($router->position->long) + ) + ) { + // router has no location + return false; + } + + return (object) [ + 'lat' => (string)$router->position->lat, + 'long' => (empty($router->position->lon) + ? (string)$router->position->long + : (string)$router->position->lon) + ]; + } + + /** + * adds an message-entry for the current community + * @param string $message + */ + private function addCommunityMessage(string $message) + { + $this->communityDebug->addMessage($message, $this->currentParseObject); + } +} diff --git a/log_to_db.php b/log_to_db.php new file mode 100644 index 0000000..1785659 --- /dev/null +++ b/log_to_db.php @@ -0,0 +1,37 @@ +connect_errno) { + echo 'Failed to connect to MySQL: (' . $db->connect_errno . ') ' . $db->connect_error; + return; +} + +$nodesJson = file_get_contents(__DIR__ . '/cache/result_routers.json'); +$nodes = json_decode($nodesJson); + +if (sizeof($nodes) > 0) { + $log = new log($db); + $log->add(sizeof($nodes)); +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..4785d31 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/schema/nodelist-schema-1.0.0.json b/schema/nodelist-schema-1.0.0.json old mode 100644 new mode 100755 diff --git a/schema/nodelist-schema-1.0.1.json b/schema/nodelist-schema-1.0.1.json old mode 100644 new mode 100755 diff --git a/templates/about.tpl b/templates/about.tpl old mode 100644 new mode 100755 index 936eb28..1a1912e --- a/templates/about.tpl +++ b/templates/about.tpl @@ -40,7 +40,6 @@
  • Leaflet-Markercluster https://github.com/Leaflet/Leaflet.markercluster
  • Bootstrap http://getbootstrap.com/
  • -
  • simpleCachedCurl https://github.com/ginader/simpleCachedCurl/
  • Attribution

    diff --git a/templates/gpxfile.tpl b/templates/gpxfile.tpl old mode 100644 new mode 100755 diff --git a/templates/stats.tpl b/templates/stats.tpl old mode 100644 new mode 100755 index e8bb18b..46d85a3 --- a/templates/stats.tpl +++ b/templates/stats.tpl @@ -3,7 +3,7 @@

    Entwicklung der Knotenzahl

    - Seit 25.02.2015 werden Knoten, die länger als 3 Tage offline sind, ausgeblendet. + Knoten, die länger als 3 Tage offline sind, werden ausgeblendet (als tot gewertet).

    Gezeigt werden die Minimal- und Maximalwerte pro Tag.
    diff --git a/tests/CommunityCacheHandlerTest.php b/tests/CommunityCacheHandlerTest.php new file mode 100644 index 0000000..7b6933e --- /dev/null +++ b/tests/CommunityCacheHandlerTest.php @@ -0,0 +1,269 @@ +cacheDirPath); + + if ($parent->exists()) { + throw new Exception('Directory "'.$this->cacheDirPath.'" already exists'); + } + + $parent->create(); + + $this->cacheDirectory = new DirectoryAlias($this->cacheDirPath.'/communities'); + $this->cacheDirectory->create(); + } + + /** + * While we do not have a mocked file-system, remove the test-directory again. + * + * @return void + */ + protected function tearDown() : void + { + $parent = new DirectoryAlias($this->cacheDirPath); + $parent->delete(); + } + + /** + * We should get a false in cases where the cache does not exist. + */ + public function testReadCacheNotExisting() : void + { + $subject = new CommunityCacheHandler($this->cacheDirPath); + + $this->assertFalse($subject->readCache( + 'berlin', + 'communityFile', + '-1 day' + )); + } + + /** + * @dataProvider readCacheExistingProvider + * @param string $cacheContent + * @param bool $expectedSuccess + * @param object $expected + */ + public function testReadCacheExisting(string $cacheContent, bool $expectedSuccess, object $expected) : void + { + $this->cacheDirectory->createFile('c_berlin.json', $cacheContent); + $subject = new CommunityCacheHandler($this->cacheDirPath); + + $result = $subject->readCache( + 'berlin', + 'communityFile', + '-1 day' + ); + + if (!$expectedSuccess) { + $this->assertFalse($result); + } else { + $this->assertInstanceOf('stdClass', $result); + $this->assertEquals($expected, $result); + } + } + + /** + * @return array[] + */ + public function readCacheExistingProvider(): array + { + $now = new DateTime(); + $nowTS = $now->format(DateTime::ATOM); + $older = new DateTime(); + $older->modify('-2 days'); + $oldTS = $older->format(DateTime::ATOM); + + return [ + 'valid and cacheHit' => [ + '{"communityFile": {"updated": "'.$nowTS.'","content": {"name": "Berlin"}}}', + true, + (object) ['name' => 'Berlin'], + ], + 'valid and cacheHit 2' => [ + '{"communityFile": {"updated": "'.$nowTS.'","content": {"name": "Berlin", "metacommunity":"Berlin"}}}', + true, + (object) ['name' => 'Berlin', "metacommunity" => "Berlin"], + ], + 'json is broken' => [ + '{"faulty json', + false, + (object) [], + ], + 'cache is outdated' => [ + // case with outdated cache + '{"communityFile": {"updated": "'.$oldTS.'","content": {"name": "Berlin"}}}', + false, + (object) [], + ], + 'searched key is missing' => [ + // case valid json but no entry for the searched "communityFile" + '{"somethingElse": {"updated": "'.$nowTS.'","content": {"name": "Berlin"}}}', + false, + (object) [], + ], + 'cache entry for key exists, but no content' => [ + '{"communityFile": {"updated": "'.$nowTS.'"}}', + false, + (object) [], + ], + ]; + } + + /** + * @dataProvider storeCacheProvider + * @param string $communityKey + * @param string $entryKey + * @param object $data + * @param object $expected + * @param string $prefill + * @throws Exception + */ + public function testStoreCache( + string $communityKey, + string $entryKey, + object $data, + object $expected, + string $prefill = '' + ) { + if ($prefill != '') { + $this->cacheDirectory->createFile('c_'.$communityKey.'.json', $prefill); + } + + $subject = new CommunityCacheHandler($this->cacheDirPath); + $subject->storeCache($communityKey, $entryKey, $data); + + $filePathName = $this->cacheDirPath.'/communities/c_'.$communityKey.'.json'; + $this->assertTrue(file_exists($filePathName)); + $actualContent = file_get_contents($filePathName); + $actualContent = json_decode($actualContent); + + // checking Timestamp + $storedDateTime = new \DateTime($actualContent->$entryKey->updated); + $storedTS = $storedDateTime->getTimestamp(); + $expectedDateTime = new \DateTime($expected->$entryKey->updated); + $expectedTS = $expectedDateTime->getTimestamp(); + // is between -1 and +1 of expected TS + $this->assertTrue(($storedTS >= ($expectedTS-1) && $storedTS <= ($expectedTS+1))); + + $actualContent->$entryKey->updated = 'checked'; + $expected->$entryKey->updated = 'checked'; + $this->assertEquals($expected, $actualContent); + } + + /** + * @return array[] + */ + public function storeCacheProvider(): array + { + $now = new DateTime(); + $nowTS = $now->format(DateTime::ATOM); + + return [ + 'trivial case' => [ + 'kassel', + 'communityFile', + (object) ['foo' => 'bar'], + (object) [ + 'communityFile' => (object) [ + 'updated' => $nowTS, + 'content' => (object) [ + 'foo' => 'bar' + ] + ] + ] + ], + 'trivial case 2' => [ + 'munich', + 'communityFile', + (object) ['foo' => 'bar', 'dog' => 'wuf'], + (object) [ + 'communityFile' => (object) [ + 'updated' => $nowTS, + 'content' => (object) [ + 'foo' => 'bar', + 'dog' => 'wuf' + ] + ] + ] + ], + 'data already exists' => [ + 'munich', + 'communityFile', + (object) ['foo' => 'bar', 'dog' => 'wuf'], + (object) [ + 'communityFile' => (object) [ + 'updated' => $nowTS, + 'content' => (object) [ + 'foo' => 'bar', + 'dog' => 'wuf' + ] + ], + 'otherData' => (object) [ + "cat" => "meow" + ] + ], + '{"otherData": {"cat": "meow"}}', + ], + 'file already exists but data is broken' => [ + 'munich', + 'communityFile', + (object) ['foo' => 'bar', 'dog' => 'wuf'], + (object) [ + 'communityFile' => (object) [ + 'updated' => $nowTS, + 'content' => (object) [ + 'foo' => 'bar', + 'dog' => 'wuf' + ] + ], + ], + '{"otherData": {"cat": ', + ], + 'data with same key exists - overwrite/update' => [ + 'munich', + 'communityFile', + (object) ['foo' => 'bar', 'dog' => 'wuf'], + (object) [ + 'communityFile' => (object) [ + 'updated' => $nowTS, + 'content' => (object) [ + 'foo' => 'bar', + 'dog' => 'wuf' + ] + ], + 'other' => (object) [ + "updated" => "2020-12-24T07:15:02+01:00", + "url" => "xyz" + ] + ], + '{"communityFile": ' + .'{"updated": "2020-12-24T07:15:02+01:00", "content": {"x": "y"}},' + .'"other" :' + .'{"updated": "2020-12-24T07:15:02+01:00", "url": "xyz"}' + .'}', + ], + + ]; + } +} diff --git a/tests/CommunityDebugTest.php b/tests/CommunityDebugTest.php new file mode 100644 index 0000000..7ceeacb --- /dev/null +++ b/tests/CommunityDebugTest.php @@ -0,0 +1,185 @@ +assertEquals([], $subject->getDebugLog()); + } + + /** + * @dataProvider addMessageProvider + * @param array $messages + * @param array $communityData + * @param array $expected + */ + public function testAddMessage(array $messages, array $communityData, array $expected) + { + $subject = new CommunityDebug(); + + foreach ($messages as $message) { + $subject->addMessage($message, $communityData); + } + + $this->assertEquals($expected, $subject->getDebugLog()); + } + + /** + * @return array[] + */ + public function addMessageProvider() : array + { + $communityData1 = [ + 'name' => 'my town', + 'source' => 'http://test.de', + ]; + + $communityData2 = [ + 'name' => 'my city', + 'source' => 'http://test.de/foo', + ]; + + return [ + 'single message 1' => [ + 'message' => ['test'], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'name' => 'my town', + 'apifile' => 'http://test.de', + 'message' => ['test'], + ]], + ], + 'single message 2' => [ + 'message' => ['all went well'], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'name' => 'my town', + 'apifile' => 'http://test.de', + 'message' => ['all went well'], + ]], + ], + 'single message 2 alternative' => [ + 'message' => ['all went well'], + 'communityData' => $communityData2, + 'expected' => ['my city' => [ + 'name' => 'my city', + 'apifile' => 'http://test.de/foo', + 'message' => ['all went well'], + ]], + ], + 'multiple messages' => [ + 'message' => [ + 'test', + 'another line of information', + 'this went well' + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'name' => 'my town', + 'apifile' => 'http://test.de', + 'message' => [ + 'test', + 'another line of information', + 'this went well' + ], + ]], + ], + ]; + } + + /** + * @dataProvider addBasicLogInfoProvider + * @param array $community + * @param array $communityData + * @param array $expected + */ + public function testAddBasicLogInfo(object $community, array $communityData, array $expected) + { + $subject = new CommunityDebug(); + + $subject->addBasicLogInfo($community, $communityData); + $this->assertEquals($expected, $subject->getDebugLog()); + } + + /** + * @return array[] + */ + public function addBasicLogInfoProvider() : array + { + $communityData1 = [ + 'name' => 'my town', + 'source' => 'http://test.de', + ]; + + return [ + 'empty case' => [ + 'community' => (object)[], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'claimed_nodecount' => false, + ]], + ], + 'nodecount int' => [ + 'community' => (object)[ + 'state' => (object)[ + 'nodes' => 1, + ], + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'claimed_nodecount' => 1, + ]], + ], + 'nodecount int bigger' => [ + 'community' => (object)[ + 'state' => (object)[ + 'nodes' => 123456, + ], + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'claimed_nodecount' => 123456, + ]], + ], + 'nodecount string' => [ + 'community' => (object)[ + 'state' => (object)[ + 'nodes' => '25', + ], + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'claimed_nodecount' => 25, + ]], + ], + 'metacommunity 1' => [ + 'community' => (object)[ + 'metacommunity' => 'mittelfranken', + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'metacommunity' => 'mittelfranken', + 'claimed_nodecount' => false + ]], + ], + 'all' => [ + 'community' => (object)[ + 'metacommunity' => 'Berlin', + 'state' => (object)[ + 'nodes' => 123, + ], + ], + 'communityData' => $communityData1, + 'expected' => ['my town' => [ + 'metacommunity' => 'Berlin', + 'claimed_nodecount' => 123 + ]], + ], + ]; + } +} diff --git a/upload_cache.php b/upload_cache.php new file mode 100644 index 0000000..e8e64ba --- /dev/null +++ b/upload_cache.php @@ -0,0 +1,66 @@ +