Browse Source

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeX8DR83QNwGUBJDAXj8fi
main
Aaron Parecki 2 days ago
parent
commit
0a60b1e030
21 changed files with 577 additions and 422 deletions
  1. +43
    -0
      README.md
  2. +19
    -9
      composer.json
  3. +25
    -136
      composer.lock
  4. +0
    -67
      controllers/geocode.php
  5. +0
    -21
      controllers/main.php
  6. +0
    -66
      controllers/timezone.php
  7. +0
    -28
      controllers/weather.php
  8. +0
    -45
      lib/helpers.php
  9. +24
    -10
      p3k/Airports.php
  10. +108
    -0
      p3k/Atlas/App.php
  11. +75
    -0
      p3k/Atlas/Controllers/Geocode.php
  12. +27
    -0
      p3k/Atlas/Controllers/Main.php
  13. +69
    -0
      p3k/Atlas/Controllers/Timezone.php
  14. +34
    -0
      p3k/Atlas/Controllers/Weather.php
  15. +44
    -0
      p3k/Atlas/Response.php
  16. +35
    -0
      p3k/Atlas/View.php
  17. +61
    -28
      p3k/geo/StaticMap.php
  18. +6
    -1
      p3k/geo/WebMercator.php
  19. +2
    -10
      public/index.php
  20. +1
    -1
      views/layout.php
  21. +4
    -0
      views/map.php

+ 43
- 0
README.md View File

@ -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

+ 19
- 9
composer.json View File

@ -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"
]

+ 25
- 136
composer.lock View File

@ -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"
}

+ 0
- 67
controllers/geocode.php View File

@ -1,67 +0,0 @@
<?php
$app->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);
}
});

+ 0
- 21
controllers/main.php View File

@ -1,21 +0,0 @@
<?php
use \Slim\Savant;
$app->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');

+ 0
- 66
controllers/timezone.php View File

@ -1,66 +0,0 @@
<?php
function timezone_for_location($lat, $lng) {
$tz = \p3k\Timezone::timezone_for_location($lat, $lng);
$timezone = false;
if($tz) {
$timezone = new p3k\timezone\Result($tz);
}
if($timezone) {
return [
'timezone' => $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);
}
});

+ 0
- 28
controllers/weather.php View File

@ -1,28 +0,0 @@
<?php
$app->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);
}
});

+ 0
- 45
lib/helpers.php View File

@ -1,45 +0,0 @@
<?php
error_reporting(E_ALL ^ E_DEPRECATED);
function json_response(&$app, $response, $code=200) {
$app->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;
}

+ 24
- 10
p3k/Airports.php View File

@ -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';
}
}

+ 108
- 0
p3k/Atlas/App.php View File

@ -0,0 +1,108 @@
<?php
namespace p3k\Atlas;
use p3k\Atlas\Controllers\Main;
use p3k\Atlas\Controllers\Timezone;
use p3k\Atlas\Controllers\Geocode;
use p3k\Atlas\Controllers\Weather;
/**
* The Atlas web app: a route table, request parameters and the shared
* helpers the controllers use. No framework.
*/
class App {
private $root;
private $view;
public function __construct($root) {
$this->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);
}
}

+ 75
- 0
p3k/Atlas/Controllers/Geocode.php View File

@ -0,0 +1,75 @@
<?php
namespace p3k\Atlas\Controllers;
use p3k\Atlas\App;
use p3k\Atlas\Response;
class Geocode {
public static function lookup(App $app, $params) {
if(
(App::k($params, 'latitude') !== null && App::k($params, 'longitude') !== null)
|| App::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(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);
}
}
}

+ 27
- 0
p3k/Atlas/Controllers/Main.php View File

@ -0,0 +1,27 @@
<?php
namespace p3k\Atlas\Controllers;
use p3k\Atlas\App;
use p3k\Atlas\Response;
class Main {
public static function index(App $app, $params) {
return Response::html($app->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']);
}
}

+ 69
- 0
p3k/Atlas/Controllers/Timezone.php View File

@ -0,0 +1,69 @@
<?php
namespace p3k\Atlas\Controllers;
use p3k\Atlas\App;
use p3k\Atlas\Response;
class Timezone {
public static function forLocation($lat, $lng) {
$tz = \p3k\Timezone::timezone_for_location($lat, $lng);
$timezone = false;
if($tz) {
$timezone = new \p3k\timezone\Result($tz);
}
if($timezone) {
return [
'timezone' => $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);
}
}
}

+ 34
- 0
p3k/Atlas/Controllers/Weather.php View File

@ -0,0 +1,34 @@
<?php
namespace p3k\Atlas\Controllers;
use p3k\Atlas\App;
use p3k\Atlas\Response;
class Weather {
public static function lookup(App $app, $params) {
if(App::k($params, 'latitude') !== null && App::k($params, 'longitude') !== null && App::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) {
return Response::json($weather);
} else {
return Response::json([
'error' => '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);
}
}
}

+ 44
- 0
p3k/Atlas/Response.php View File

@ -0,0 +1,44 @@
<?php
namespace p3k\Atlas;
/**
* A minimal HTTP response: status, headers and a body, sent with send().
*/
class Response {
public $status;
public $headers;
public $body;
public function __construct($body = '', $status = 200, $headers = []) {
$this->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;
}
}

+ 35
- 0
p3k/Atlas/View.php View File

@ -0,0 +1,35 @@
<?php
namespace p3k\Atlas;
/**
* Plain PHP templates: the page is rendered first, then wrapped in layout.php
* which receives it as $content. Templates get the variables passed in.
*/
class View {
private $directory;
public function __construct($directory) {
$this->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();
}
}

+ 61
- 28
p3k/geo/StaticMap.php View File

@ -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) {

+ 6
- 1
p3k/geo/WebMercator.php View File

@ -1,7 +1,12 @@
<?php
namespace p3k\geo\WebMercator;
define('TILE_SIZE', 256);
const TILE_SIZE = 256;
// Kept for code that still refers to the old global constant.
if(!defined('TILE_SIZE')) {
define('TILE_SIZE', TILE_SIZE);
}
function totalPixelsForZoomLevel($zoom) {
return pow(2, $zoom) * TILE_SIZE;

+ 2
- 10
public/index.php View File

@ -1,12 +1,4 @@
<?php
chdir('..');
require 'vendor/autoload.php';
require __DIR__ . '/../vendor/autoload.php';
\Slim\Savant\init();
require 'controllers/main.php';
require 'controllers/timezone.php';
require 'controllers/geocode.php';
require 'controllers/weather.php';
$app->run();
(new p3k\Atlas\App(dirname(__DIR__)))->run();

+ 1
- 1
views/layout.php View File

@ -10,7 +10,7 @@
</head>
<body>
<?= $this->fetch($this->page . '.php') ?>
<?= $content ?>
<footer>
<div class="right">

+ 4
- 0
views/map.php View File

@ -0,0 +1,4 @@
<header>
<h1>Atlas Static Maps</h1>
<p>See the <a href="/static-maps">Static Maps API docs</a>.</p>
</header>

Loading…
Cancel
Save