From 0a60b1e0301fd6f6c31939568f23bbfdcab78e97 Mon Sep 17 00:00:00 2001 From: Aaron Parecki Date: Fri, 2 Oct 2026 15:27:05 +0000 Subject: [PATCH] Remove Slim and Savant; built-in routing and rendering Atlas is now a plain library plus a tiny web app with no framework: - p3k\Atlas\App routes the seven endpoints, p3k\Atlas\View renders the plain PHP templates, p3k\Atlas\Response carries the result. Controllers are ported line for line from the Slim closures. - p3k\geo\StaticMap\render() returns the image bytes and content type instead of sending headers and echoing, so other apps can embed it. generate() remains as a wrapper. The tile size constant is namespaced. - Fix Airports::from_code() (the file handle was never kept) and the reference to a Geocoder class that never existed. - composer: type library, PHP 8.1+, ext-gd required; lib/helpers.php is no longer autoloaded so nothing global leaks into consumers. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LeX8DR83QNwGUBJDAXj8fi --- README.md | 43 ++++++++ composer.json | 28 +++-- composer.lock | 161 +++++------------------------ controllers/geocode.php | 67 ------------ controllers/main.php | 21 ---- controllers/timezone.php | 66 ------------ controllers/weather.php | 28 ----- lib/helpers.php | 45 -------- p3k/Airports.php | 34 ++++-- p3k/Atlas/App.php | 108 +++++++++++++++++++ p3k/Atlas/Controllers/Geocode.php | 75 ++++++++++++++ p3k/Atlas/Controllers/Main.php | 27 +++++ p3k/Atlas/Controllers/Timezone.php | 69 +++++++++++++ p3k/Atlas/Controllers/Weather.php | 34 ++++++ p3k/Atlas/Response.php | 44 ++++++++ p3k/Atlas/View.php | 35 +++++++ p3k/geo/StaticMap.php | 89 +++++++++++----- p3k/geo/WebMercator.php | 7 +- public/index.php | 12 +-- views/layout.php | 2 +- views/map.php | 4 + 21 files changed, 577 insertions(+), 422 deletions(-) delete mode 100644 controllers/geocode.php delete mode 100644 controllers/main.php delete mode 100644 controllers/timezone.php delete mode 100644 controllers/weather.php delete mode 100644 lib/helpers.php create mode 100644 p3k/Atlas/App.php create mode 100644 p3k/Atlas/Controllers/Geocode.php create mode 100644 p3k/Atlas/Controllers/Main.php create mode 100644 p3k/Atlas/Controllers/Timezone.php create mode 100644 p3k/Atlas/Controllers/Weather.php create mode 100644 p3k/Atlas/Response.php create mode 100644 p3k/Atlas/View.php diff --git a/README.md b/README.md index cef5f19..5631e25 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,49 @@ You'll need to pass an OpenWeatherMap.org API key in the request. Icon names ref [Full Static Maps Docs](https://atlas.p3k.io/static-maps) +## Running the web app + +Atlas has no framework dependencies. Point a web server at `public/` with +`index.php` as the front controller (an `.htaccess` for Apache is included; +for nginx use `try_files $uri /index.php?$args;`), then run `composer install`. + +Requests carrying a `token` listed in `data/apikeys.txt` (one per line) may use +custom tile URLs (`basemap=custom&tileurl=...`) and external marker icons. + +For development: `php -S 127.0.0.1:8080 -t public` + + +## Using Atlas as a library + +``` +composer require p3k/atlas +``` + +Static maps are rendered in memory; nothing is echoed and no headers are sent: + +```php +$image = p3k\geo\StaticMap\render([ + 'marker' => ['lat:45.5165;lng:-122.6764;icon:small-blue-cutout'], + 'basemap' => 'gray', + 'width' => 600, + 'height' => 240, + 'zoom' => 14, +]); +// $image['data'] is the PNG (or JPEG, with format=jpg), $image['contentType'] its MIME type +``` + +The bundled marker icons are used by default; pass a directory as the second +argument to use your own, and `true` as the third to allow custom tile URLs. + +Airports and timezones: + +```php +$airport = p3k\Airports::from_code('PDX'); // ['code','latitude','longitude','name'] or false +$timezone = p3k\Timezone::timezone_for_location($airport['latitude'], $airport['longitude']); +$result = new p3k\timezone\Result($timezone, '2026-10-02'); // ->name, ->offset ("-07:00"), ->seconds, ->localtime +``` + + ## License diff --git a/composer.json b/composer.json index e0aed61..7ffdcb1 100644 --- a/composer.json +++ b/composer.json @@ -1,22 +1,32 @@ { "name": "p3k/atlas", - "description": "Atlas", + "description": "Atlas: timezone, geocoding, weather and static map APIs. Usable as a web app or as a library.", "keywords": ["p3k", "indieweb", "maps", "geocoding", "timezone"], - "license": "Apache 2.0", - "type": "project", + "license": "Apache-2.0", + "type": "library", + "homepage": "https://github.com/aaronpk/Atlas", + "authors": [ + { + "name": "Aaron Parecki", + "homepage": "https://aaronparecki.com" + } + ], "require": { - "saltybeagle/savant3": "dev-master", - "p3k/slim-savant": ">=0.1.0", - "slim/slim": "^2.6", - "p3k/timezone": "*", - "michelf/php-markdown": "^1.9" + "php": ">=8.1", + "ext-curl": "*", + "ext-gd": "*", + "ext-json": "*", + "p3k/timezone": "^0.1", + "michelf/php-markdown": "^1.9 || ^2.0" + }, + "suggest": { + "ext-imagick": "Needed to draw paths on static maps" }, "autoload": { "psr-4": { "p3k\\": "p3k/" }, "files": [ - "lib/helpers.php", "p3k/geo/WebMercator.php", "p3k/geo/StaticMap.php" ] diff --git a/composer.lock b/composer.lock index 1ec5a3f..0222d10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,30 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d813931b318f63554477f9c4a61c7c4", + "content-hash": "1b7ed514f9566466608f5a96c4601735", "packages": [ { "name": "michelf/php-markdown", - "version": "1.9.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/michelf/php-markdown.git", - "reference": "5024d623c1a057dcd2d076d25b7d270a1d0d55f3" + "reference": "eb176f173fbac58a045aff78e55f833264b34e71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/5024d623c1a057dcd2d076d25b7d270a1d0d55f3", - "reference": "5024d623c1a057dcd2d076d25b7d270a1d0d55f3", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/eb176f173fbac58a045aff78e55f833264b34e71", + "reference": "eb176f173fbac58a045aff78e55f833264b34e71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": ">=4.3 <5.8" + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": ">=1.0", + "phpstan/phpstan-phpunit": ">=1.0", + "phpunit/phpunit": "^9.5" }, "type": "library", "autoload": { @@ -55,48 +58,9 @@ ], "support": { "issues": "https://github.com/michelf/php-markdown/issues", - "source": "https://github.com/michelf/php-markdown/tree/1.9.1" + "source": "https://github.com/michelf/php-markdown/tree/2.0.0" }, - "time": "2021-11-24T02:52:38+00:00" - }, - { - "name": "p3k/slim-savant", - "version": "0.1.0", - "source": { - "type": "git", - "url": "https://github.com/aaronpk/Slim-Savant.git", - "reference": "129291da1223f5c95ad8f4e1bca21901ae45e53e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/aaronpk/Slim-Savant/zipball/129291da1223f5c95ad8f4e1bca21901ae45e53e", - "reference": "129291da1223f5c95ad8f4e1bca21901ae45e53e", - "shasum": "" - }, - "require": { - "php": ">5.3.0", - "saltybeagle/savant3": "dev-master", - "slim/slim": ">=2.4.0" - }, - "type": "library", - "autoload": { - "files": [ - "Savant.php", - "SlimSavant.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache 2.0" - ], - "authors": [ - { - "name": "Aaron Parecki", - "homepage": "http://aaronparecki.com" - } - ], - "description": "Savant renderer for Slim", - "time": "2015-07-21T01:40:52+00:00" + "time": "2022-09-26T12:21:08+00:00" }, { "name": "p3k/timezone", @@ -138,100 +102,25 @@ "p3k", "timezone" ], - "time": "2017-01-12T17:30:08+00:00" - }, - { - "name": "saltybeagle/savant3", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/saltybeagle/Savant3.git", - "reference": "ebf4385bf44bec8c7a169571ac178f626017c466" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/saltybeagle/Savant3/zipball/ebf4385bf44bec8c7a169571ac178f626017c466", - "reference": "ebf4385bf44bec8c7a169571ac178f626017c466", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "Savant3.php", - "Savant3", - "Savant3/resources" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL" - ], - "authors": [ - { - "name": "Brett Bieber", - "email": "brett.bieber@gmail.com" - } - ], - "description": "Savant3 template engine", - "time": "2014-01-07T17:10:32+00:00" - }, - { - "name": "slim/slim", - "version": "2.6.3", - "source": { - "type": "git", - "url": "https://github.com/slimphp/Slim.git", - "reference": "9224ed81ac1c412881e8d762755e3d76ebf580c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slimphp/Slim/zipball/9224ed81ac1c412881e8d762755e3d76ebf580c0", - "reference": "9224ed81ac1c412881e8d762755e3d76ebf580c0", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "suggest": { - "ext-mcrypt": "Required for HTTP cookie encryption", - "phpseclib/mcrypt_compat": "Polyfil for mcrypt extension" - }, - "type": "library", - "autoload": { - "psr-0": { - "Slim": "." - } + "support": { + "issues": "https://github.com/aaronpk/p3k-timezone/issues", + "source": "https://github.com/aaronpk/p3k-timezone/tree/0.1.0" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Josh Lockhart", - "email": "info@joshlockhart.com", - "homepage": "http://www.joshlockhart.com/" - } - ], - "description": "Slim Framework, a PHP micro framework", - "homepage": "http://github.com/codeguy/Slim", - "keywords": [ - "microframework", - "rest", - "router" - ], - "time": "2017-01-07T12:21:41+00:00" + "time": "2017-01-12T17:30:08+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "saltybeagle/savant3": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform": { + "php": ">=8.1", + "ext-curl": "*", + "ext-gd": "*", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/controllers/geocode.php b/controllers/geocode.php deleted file mode 100644 index e370a18..0000000 --- a/controllers/geocode.php +++ /dev/null @@ -1,67 +0,0 @@ -get('/api/geocode', function() use($app) { - $params = $app->request()->params(); - - if( - (k($params, 'latitude') !== null && k($params, 'longitude') !== null) - || k($params, 'input') !== null - ) { - - $response = [ - 'latitude' => null, - 'longitude' => null, - 'locality' => null, - 'region' => null, - 'country' => null, - 'best_name' => null, - 'full_name' => null, - 'postal-code' => null, - 'timezone' => null, - 'offset' => null, - 'seconds' => null, - 'localtime' => null, - ]; - - if(k($params, 'input')) { - $adr = p3k\Geocoder::geocode($params['input']); - } else { - $lat = (float)$params['latitude']; - $lng = (float)$params['longitude']; - $response['latitude'] = $lat; - $response['longitude'] = $lng; - $adr = p3k\Geocoder::adrFromLocation($lat, $lng); - } - - if($adr) { - $response['latitude'] = $adr->latitude; - $response['longitude'] = $adr->longitude; - $response['locality'] = $adr->localityName; - $response['region'] = $adr->regionName; - $response['country'] = $adr->countryName; - $response['best_name'] = $adr->bestName; - $response['full_name'] = $adr->fullName; - $response['postal-code'] = $adr->postalCode; - } - - $tz = p3k\Timezone::timezone_for_location($response['latitude'], $response['longitude'], k($params,'date')); - $timezone = false; - if($tz) { - $timezone = new p3k\timezone\Result($tz, k($params, 'date')); - } - - if($timezone) { - $response['timezone'] = $timezone->name; - $response['offset'] = $timezone->offset; - $response['seconds'] = $timezone->seconds; - $response['localtime'] = $timezone->localtime; - } - - json_response($app, $response); - } else { - json_response($app, [ - 'error' => 'invalid_request', - 'error_description' => 'Request was missing parameters' - ], 400); - } -}); diff --git a/controllers/main.php b/controllers/main.php deleted file mode 100644 index 84860ec..0000000 --- a/controllers/main.php +++ /dev/null @@ -1,21 +0,0 @@ -get('/', function() use($app) { - return Savant\render('index'); -}); - -$app->get('/static-maps', function() use($app) { - return Savant\render('static-maps'); -}); - -$app->get('/map', function() use($app) { - return Savant\render('map'); -}); - -$app->map('/map/img', function() use($app) { - $params = $app->request()->params(); - $app->response['Content-type'] = 'image/png'; - $assetPath = dirname(__FILE__) . '/../public/map-images'; - $map = p3k\geo\StaticMap\generate($params, null, $assetPath, is_authenticated($params)); -})->via('GET','POST'); diff --git a/controllers/timezone.php b/controllers/timezone.php deleted file mode 100644 index 5e613fc..0000000 --- a/controllers/timezone.php +++ /dev/null @@ -1,66 +0,0 @@ - $timezone->name, - 'offset' => $timezone->offset, - 'seconds' => $timezone->seconds, - 'localtime' => $timezone->localtime - ]; - } else { - return [ - 'error' => 'not_found', - 'error_description' => 'No timezone was found for the requested location' - ]; - } -} - -$app->get('/api/timezone', function() use($app) { - $params = $app->request()->params(); - - if(k($params, 'latitude') !== null && k($params, 'longitude') !== null) { - - $lat = (float)$params['latitude']; - $lng = (float)$params['longitude']; - - $result = timezone_for_location($lat, $lng); - json_response($app, $result); - - } elseif(k($params, 'airport')) { - - $code = $params['airport']; - - $airport = \p3k\Airports::from_code($code); - - if($airport) { - $result = timezone_for_location($airport['latitude'], $airport['longitude']); - - if(!isset($result['error'])) { - $result['airport'] = $airport; - } - - json_response($app, $result); - - } else { - json_response($app, [ - 'error' => 'not_found', - 'error_description' => 'The airport code was not found' - ]); - } - - } else { - json_response($app, [ - 'error' => 'invalid_request', - 'error_description' => 'Request was missing parameters' - ], 400); - } -}); - diff --git a/controllers/weather.php b/controllers/weather.php deleted file mode 100644 index 7d1c377..0000000 --- a/controllers/weather.php +++ /dev/null @@ -1,28 +0,0 @@ -get('/api/weather', function() use($app) { - $params = $app->request()->params(); - - if(k($params, 'latitude') !== null && k($params, 'longitude') !== null && k($params, 'apikey') !== null) { - - $lat = (float)$params['latitude']; - $lng = (float)$params['longitude']; - $key = $params['apikey']; - - $weather = \p3k\Weather::weather_for_location($lat, $lng, $key); - - if($weather) { - json_response($app, $weather); - } else { - json_response($app, [ - 'error' => 'not_found', - 'error_description' => 'No weather information was found for the requested location, or you used an invalid API key' - ]); - } - } else { - json_response($app, [ - 'error' => 'invalid_request', - 'error_description' => 'Request was missing parameters' - ], 400); - } -}); diff --git a/lib/helpers.php b/lib/helpers.php deleted file mode 100644 index 3981e43..0000000 --- a/lib/helpers.php +++ /dev/null @@ -1,45 +0,0 @@ -response()->status($code); - $app->response()['Content-Type'] = 'application/json'; - $app->response()->body(json_encode($response)); -} - -function k($a, $k, $default=null) { - if(is_array($k)) { - $result = true; - foreach($k as $key) { - $result = $result && array_key_exists($key, $a); - } - return $result; - } else { - if(is_array($a) && array_key_exists($k, $a) && $a[$k]) - return $a[$k]; - elseif(is_object($a) && property_exists($a, $k) && $a->$k) - return $a->$k; - else - return $default; - } -} - -function is_authenticated($params) { - if(!isset($params['token'])) - return false; - - $token_file = __DIR__.'/../data/apikeys.txt'; - - if(!file_exists($token_file)) - return false; - - $valid_tokens = array_filter(file($token_file)); - array_walk($valid_tokens, function(&$val) { - $val = trim($val); - }); - - if(in_array($params['token'], $valid_tokens)) - return true; - - return false; -} diff --git a/p3k/Airports.php b/p3k/Airports.php index 303e9fb..d01a09b 100644 --- a/p3k/Airports.php +++ b/p3k/Airports.php @@ -3,29 +3,43 @@ namespace p3k; class Airports { - private static $fp = false; - + /** + * Look up an airport by its IATA code, e.g. "PDX". + * + * Returns ['code', 'latitude', 'longitude', 'name'] or false when the code + * is not in the bundled ourairports.com extract. + */ public static function from_code($code) { - if(!self::$fp) { - $fp = fopen(__DIR__.'/../data/airports-compact.csv', 'r'); + $code = strtoupper(trim((string)$code)); + if($code === '') { + return false; } - rewind($fp); + $fp = fopen(self::dataFile(), 'r'); + if(!$fp) { + return false; + } $airport = false; - while(!$airport && ($line=fgetcsv($fp))) { - if($line[0] == $code) { + while(!$airport && ($line = fgetcsv($fp, null, ',', '"', '')) !== false) { + if(isset($line[0]) && $line[0] == $code) { $airport = [ 'code' => $code, - 'latitude' => $line[1], - 'longitude' => $line[2], - 'name' => $line[3], + 'latitude' => (float)$line[1], + 'longitude' => (float)$line[2], + 'name' => $line[3] ?? '', ]; } } + fclose($fp); + return $airport; } + public static function dataFile() { + return dirname(__DIR__) . '/data/airports-compact.csv'; + } + } diff --git a/p3k/Atlas/App.php b/p3k/Atlas/App.php new file mode 100644 index 0000000..72b5518 --- /dev/null +++ b/p3k/Atlas/App.php @@ -0,0 +1,108 @@ +root = rtrim($root, '/'); + $this->view = new View($this->root . '/views'); + } + + public function root() { + return $this->root; + } + + public function view() { + return $this->view; + } + + /** + * [methods, path, [class, method]] + */ + public function routes() { + return [ + [['GET'], '/', [Main::class, 'index']], + [['GET'], '/static-maps', [Main::class, 'staticMaps']], + [['GET'], '/map', [Main::class, 'map']], + [['GET', 'POST'], '/map/img', [Main::class, 'mapImage']], + [['GET'], '/api/timezone', [Timezone::class, 'lookup']], + [['GET'], '/api/geocode', [Geocode::class, 'lookup']], + [['GET'], '/api/weather', [Weather::class, 'lookup']], + ]; + } + + public function run() { + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; + if($path !== '/') { + $path = rtrim($path, '/'); + } + $params = array_merge($_GET, $_POST); + + $this->dispatch($method, $path, $params)->send(); + } + + public function dispatch($method, $path, $params) { + if($method === 'HEAD') { + $method = 'GET'; + } + foreach($this->routes() as list($methods, $route, $handler)) { + if($route === $path) { + if(!in_array($method, $methods)) { + return Response::text('Method Not Allowed', 405); + } + list($class, $fn) = $handler; + return $class::$fn($this, $params); + } + } + return Response::text('Not Found', 404); + } + + /** + * The value of $a[$k] when it is set and truthy, else $default. The + * controllers have always treated "" and "0" as missing. + */ + public static function k($a, $k, $default = null) { + if(is_array($k)) { + $result = true; + foreach($k as $key) { + $result = $result && array_key_exists($key, $a); + } + return $result; + } + if(is_array($a) && array_key_exists($k, $a) && $a[$k]) + return $a[$k]; + elseif(is_object($a) && property_exists($a, $k) && $a->$k) + return $a->$k; + else + return $default; + } + + /** + * Requests that carry one of the API keys listed in data/apikeys.txt may + * use custom tile URLs and external marker icons. + */ + public function isAuthenticated($params) { + if(!isset($params['token'])) + return false; + + $tokenFile = $this->root . '/data/apikeys.txt'; + if(!file_exists($tokenFile)) + return false; + + $validTokens = array_map('trim', array_filter(file($tokenFile))); + + return in_array($params['token'], $validTokens, true); + } +} diff --git a/p3k/Atlas/Controllers/Geocode.php b/p3k/Atlas/Controllers/Geocode.php new file mode 100644 index 0000000..bb7f48d --- /dev/null +++ b/p3k/Atlas/Controllers/Geocode.php @@ -0,0 +1,75 @@ + null, + 'longitude' => null, + 'locality' => null, + 'region' => null, + 'country' => null, + 'best_name' => null, + 'full_name' => null, + 'postal-code' => null, + 'timezone' => null, + 'offset' => null, + 'seconds' => null, + 'localtime' => null, + ]; + + if(App::k($params, 'input')) { + $adr = \p3k\Geocoder::geocode($params['input']); + } else { + $lat = (float)$params['latitude']; + $lng = (float)$params['longitude']; + $response['latitude'] = $lat; + $response['longitude'] = $lng; + $adr = \p3k\Geocoder::adrFromLocation($lat, $lng); + } + + if($adr) { + $response['latitude'] = $adr->latitude; + $response['longitude'] = $adr->longitude; + $response['locality'] = $adr->localityName; + $response['region'] = $adr->regionName; + $response['country'] = $adr->countryName; + $response['best_name'] = $adr->bestName; + $response['full_name'] = $adr->fullName; + $response['postal-code'] = $adr->postalCode; + } + + $timezone = false; + if($response['latitude'] !== null && $response['longitude'] !== null) { + $tz = \p3k\Timezone::timezone_for_location($response['latitude'], $response['longitude']); + if($tz) { + $timezone = new \p3k\timezone\Result($tz, App::k($params, 'date')); + } + } + + if($timezone) { + $response['timezone'] = $timezone->name; + $response['offset'] = $timezone->offset; + $response['seconds'] = $timezone->seconds; + $response['localtime'] = $timezone->localtime; + } + + return Response::json($response); + } else { + return Response::json([ + 'error' => 'invalid_request', + 'error_description' => 'Request was missing parameters' + ], 400); + } + } + +} diff --git a/p3k/Atlas/Controllers/Main.php b/p3k/Atlas/Controllers/Main.php new file mode 100644 index 0000000..08cc84a --- /dev/null +++ b/p3k/Atlas/Controllers/Main.php @@ -0,0 +1,27 @@ +view()->render('index')); + } + + public static function staticMaps(App $app, $params) { + return Response::html($app->view()->render('static-maps')); + } + + public static function map(App $app, $params) { + return Response::html($app->view()->render('map')); + } + + public static function mapImage(App $app, $params) { + $assetPath = $app->root() . '/public/map-images'; + $image = \p3k\geo\StaticMap\render($params, $assetPath, $app->isAuthenticated($params)); + return Response::image($image['data'], $image['contentType']); + } + +} diff --git a/p3k/Atlas/Controllers/Timezone.php b/p3k/Atlas/Controllers/Timezone.php new file mode 100644 index 0000000..93a3d25 --- /dev/null +++ b/p3k/Atlas/Controllers/Timezone.php @@ -0,0 +1,69 @@ + $timezone->name, + 'offset' => $timezone->offset, + 'seconds' => $timezone->seconds, + 'localtime' => $timezone->localtime + ]; + } else { + return [ + 'error' => 'not_found', + 'error_description' => 'No timezone was found for the requested location' + ]; + } + } + + public static function lookup(App $app, $params) { + if(App::k($params, 'latitude') !== null && App::k($params, 'longitude') !== null) { + + $lat = (float)$params['latitude']; + $lng = (float)$params['longitude']; + + return Response::json(self::forLocation($lat, $lng)); + + } elseif(App::k($params, 'airport')) { + + $code = $params['airport']; + + $airport = \p3k\Airports::from_code($code); + + if($airport) { + $result = self::forLocation($airport['latitude'], $airport['longitude']); + + if(!isset($result['error'])) { + $result['airport'] = $airport; + } + + return Response::json($result); + + } else { + return Response::json([ + 'error' => 'not_found', + 'error_description' => 'The airport code was not found' + ]); + } + + } else { + return Response::json([ + 'error' => 'invalid_request', + 'error_description' => 'Request was missing parameters' + ], 400); + } + } + +} diff --git a/p3k/Atlas/Controllers/Weather.php b/p3k/Atlas/Controllers/Weather.php new file mode 100644 index 0000000..0553886 --- /dev/null +++ b/p3k/Atlas/Controllers/Weather.php @@ -0,0 +1,34 @@ + 'not_found', + 'error_description' => 'No weather information was found for the requested location, or you used an invalid API key' + ]); + } + } else { + return Response::json([ + 'error' => 'invalid_request', + 'error_description' => 'Request was missing parameters' + ], 400); + } + } + +} diff --git a/p3k/Atlas/Response.php b/p3k/Atlas/Response.php new file mode 100644 index 0000000..d807774 --- /dev/null +++ b/p3k/Atlas/Response.php @@ -0,0 +1,44 @@ +body = $body; + $this->status = $status; + $this->headers = $headers; + } + + public static function html($html, $status = 200) { + return new self($html, $status, ['Content-Type' => 'text/html; charset=utf-8']); + } + + public static function json($data, $status = 200) { + return new self(json_encode($data), $status, ['Content-Type' => 'application/json']); + } + + public static function image($data, $contentType) { + return new self($data, 200, [ + 'Content-Type' => $contentType, + 'Cache-Control' => 'max-age=' . (60*60*24*30) . ', public', + ]); + } + + public static function text($text, $status = 200) { + return new self($text, $status, ['Content-Type' => 'text/plain; charset=utf-8']); + } + + public function send() { + http_response_code($this->status); + foreach($this->headers as $name => $value) { + header($name . ': ' . $value); + } + echo $this->body; + } +} diff --git a/p3k/Atlas/View.php b/p3k/Atlas/View.php new file mode 100644 index 0000000..e665c2e --- /dev/null +++ b/p3k/Atlas/View.php @@ -0,0 +1,35 @@ +directory = rtrim($directory, '/'); + } + + public function render($page, $vars = []) { + if(!preg_match('/^[a-z0-9_-]+$/i', $page)) { + throw new \InvalidArgumentException('Invalid template name'); + } + $content = $this->fetch($page, $vars); + return $this->fetch('layout', ['content' => $content] + $vars); + } + + public function fetch($page, $vars = []) { + $file = $this->directory . '/' . $page . '.php'; + if(!is_file($file)) { + throw new \RuntimeException('Template not found: ' . $page); + } + ob_start(); + (function($__file, $__vars) { + extract($__vars, EXTR_SKIP); + require $__file; + })($file, $vars); + return ob_get_clean(); + } +} diff --git a/p3k/geo/StaticMap.php b/p3k/geo/StaticMap.php index fb5d378..5668438 100644 --- a/p3k/geo/StaticMap.php +++ b/p3k/geo/StaticMap.php @@ -3,7 +3,28 @@ namespace p3k\geo\StaticMap; use p3k\geo\WebMercator, p3k\Geocoder; use Imagick, ImagickPixel, ImagickDraw; -function generate($params, $filename, $assetPath, $is_authenticated=false) { +/** + * The directory holding the built-in marker icons and attribution logos. + */ +function defaultAssetPath() { + return dirname(__DIR__, 2) . '/public/map-images'; +} + +/** + * Render a static map and return it as a string of image bytes. + * + * $params is the same array the /map/img endpoint accepts (marker[], path[], + * latitude/longitude, width, height, zoom, maxzoom, basemap, tileurl, + * attribution, format, quality, bezier). Nothing is written to the output + * buffer and no headers are sent, so this is safe to call from any app. + * + * Returns ['data' => string, 'contentType' => 'image/png'|'image/jpeg']. + */ +function render($params, $assetPath = null, $isAuthenticated = false) { + if ($assetPath === null) { + $assetPath = defaultAssetPath(); + } + $is_authenticated = $isAuthenticated; $bounds = array( 'minLat' => 90, @@ -135,17 +156,13 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { $latitude = k($params,'latitude'); $longitude = k($params,'longitude'); } elseif(k($params,'location') !== false) { - $result = ArcGISGeocoder::geocode(k($params,'location')); - if(!$result->success) { + $result = Geocoder::geocode(k($params,'location')); + if(!$result || $result->latitude === null) { $latitude = $defaultLatitude; $longitude = $defaultLongitude; - #header('X-Geocode: error'); - #header('X-Geocode-Result: ' . $result->raw); } else { $latitude = $result->latitude; $longitude = $result->longitude; - #header('X-Geocode: success'); - #header('X-Geocode-Result: ' . $latitude . ', ' . $longitude); } } else { $latitude = $defaultLatitude; @@ -261,14 +278,6 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { $overlayURL = false; } - function urlForTile($x, $y, $z, $tileURL) { - return str_replace(array( - '{X}', '{Y}', '{Z}', '{x}', '{y}', '{z}' - ), array( - $x, $y, $z, $x, $y, $z - ), $tileURL); - } - @@ -302,6 +311,7 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { $tiles = array(); $overlays = array(); $chs = array(); + $ochs = array(); $mh = curl_multi_init(); $numTiles = 0; @@ -376,8 +386,8 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { $x = intval($x); $y = intval($y); - $ox = (($x - $tilePos['x']) * TILE_SIZE) - $pos['x'] + ($width/2); - $oy = (($y - $tilePos['y']) * TILE_SIZE) - $pos['y'] + ($height/2); + $ox = (($x - $tilePos['x']) * WebMercator\TILE_SIZE) - $pos['x'] + ($width/2); + $oy = (($y - $tilePos['y']) * WebMercator\TILE_SIZE) - $pos['y'] + ($height/2); imagecopy($im, $tile, $ox,$oy, 0,0, imagesx($tile),imagesy($tile)); } @@ -389,8 +399,8 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { $x = intval($x); $y = intval($y); - $ox = (($x - $tilePos['x']) * TILE_SIZE) - $pos['x'] + ($width/2); - $oy = (($y - $tilePos['y']) * TILE_SIZE) - $pos['y'] + ($height/2); + $ox = (($x - $tilePos['x']) * WebMercator\TILE_SIZE) - $pos['x'] + ($width/2); + $oy = (($y - $tilePos['y']) * WebMercator\TILE_SIZE) - $pos['y'] + ($height/2); imagecopy($im, $tile, $ox,$oy, 0,0, imagesx($tile),imagesy($tile)); } @@ -558,30 +568,53 @@ function generate($params, $filename, $assetPath, $is_authenticated=false) { } - #header('Cache-Control: max-age=' . (60*60*24*30) . ', public'); - #header('X-Tiles-Downloaded: ' . $numTiles); - - // TODO: add caching $fmt = k($params,'format', 'png'); + ob_start(); switch($fmt) { case "jpg": case "jpeg": - header('Content-type: image/jpg'); - $quality = k($params, 'quality', 75); - imagejpeg($im, $filename, $quality); + $contentType = 'image/jpeg'; + $quality = (int)k($params, 'quality', 75); + imagejpeg($im, null, $quality); break; case "png": default: - header('Content-type: image/png'); - imagepng($im, $filename); + $contentType = 'image/png'; + imagepng($im); break; } + $data = ob_get_clean(); imagedestroy($im); /** * http://msdn.microsoft.com/en-us/library/bb259689.aspx * http://derickrethans.nl/php-mapping.html */ + return array('data' => $data, 'contentType' => $contentType); +} + +/** + * The original entry point, kept for existing callers: writes the image to + * $filename, or sends it to the browser with a Content-type header when + * $filename is null. + */ +function generate($params, $filename, $assetPath, $is_authenticated=false) { + $image = render($params, $assetPath, $is_authenticated); + if($filename) { + file_put_contents($filename, $image['data']); + } else { + header('Content-type: ' . $image['contentType']); + echo $image['data']; + } + return $image; +} + +function urlForTile($x, $y, $z, $tileURL) { + return str_replace(array( + '{X}', '{Y}', '{Z}', '{x}', '{y}', '{z}' + ), array( + $x, $y, $z, $x, $y, $z + ), $tileURL); } function k($a, $k, $default=false) { diff --git a/p3k/geo/WebMercator.php b/p3k/geo/WebMercator.php index 143baee..b74334b 100644 --- a/p3k/geo/WebMercator.php +++ b/p3k/geo/WebMercator.php @@ -1,7 +1,12 @@ run(); +(new p3k\Atlas\App(dirname(__DIR__)))->run(); diff --git a/views/layout.php b/views/layout.php index d140064..a805e6c 100644 --- a/views/layout.php +++ b/views/layout.php @@ -10,7 +10,7 @@ - fetch($this->page . '.php') ?> +