Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1297 lines (1195 sloc) 41.7 KB
<?php
/*
* TileServer.php project
* ======================
* https://github.com/klokantech/tileserver-php/
* Copyright (C) 2016 - Klokan Technologies GmbH
*/
global $config;
$config['serverTitle'] = 'Maps hosted with TileServer-php v2.0';
$config['availableFormats'] = array('png', 'jpg', 'jpeg', 'gif', 'webp', 'pbf', 'hybrid');
$config['dataRoot'] = '';
//$config['template'] = 'template.php';
//$config['baseUrls'] = array('t0.server.com', 't1.server.com');
Router::serve(array(
'/' => 'Server:getHtml',
'/maps' => 'Server:getInfo',
'/html' => 'Server:getHtml',
'/:alpha/:number/:number/:number.grid.json' => 'Json:getUTFGrid',
'/:alpha.json' => 'Json:getJson',
'/:alpha.jsonp' => 'Json:getJsonp',
'/wmts' => 'Wmts:get',
'/wmts/1.0.0/WMTSCapabilities.xml' => 'Wmts:get',
'/wmts/:alpha/:number/:number/:alpha' => 'Wmts:getTile',
'/wmts/:alpha/:alpha/:number/:number/:alpha' => 'Wmts:getTile',
'/wmts/:alpha/:alpha/:alpha/:number/:number/:alpha' => 'Wmts:getTile',
'/:alpha/:number/:number/:alpha' => 'Wmts:getTile',
'/tms' => 'Tms:getCapabilities',
'/tms/:alpha' => 'Tms:getLayerCapabilities',
));
/**
* Server base
*/
class Server {
/**
* Configuration of TileServer [baseUrls, serverTitle]
* @var array
*/
public $config;
/**
* Datasets stored in file structure
* @var array
*/
public $fileLayer = array();
/**
* Datasets stored in database
* @var array
*/
public $dbLayer = array();
/**
* PDO database connection
* @var object
*/
public $db;
/**
* Set config
*/
public function __construct() {
$this->config = $GLOBALS['config'];
//Get config from enviroment
$envServerTitle = getenv('serverTitle');
if($envServerTitle !== FALSE){
$this->config['serverTitle'] = $envServerTitle;
}
$envBaseUrls = getenv('baseUrls');
if($envBaseUrls !== FALSE){
$this->config['baseUrls'] = is_array($envBaseUrls) ?
$envBaseUrls : explode(',', $envBaseUrls);
}
$envTemplate = getenv('template');
if($envBaseUrls !== FALSE){
$this->config['template'] = $envTemplate;
}
}
/**
* Looks for datasets
*/
public function setDatasets() {
$mjs = glob('*/metadata.json');
$mbts = glob($this->config['dataRoot'] . '*.mbtiles');
if ($mjs) {
foreach (array_filter($mjs, 'is_readable') as $mj) {
$layer = $this->metadataFromMetadataJson($mj);
array_push($this->fileLayer, $layer);
}
}
if ($mbts) {
foreach (array_filter($mbts, 'is_readable') as $mbt) {
$this->dbLayer[] = $this->metadataFromMbtiles($mbt);
}
}
}
/**
* Processing params from router <server>/<layer>/<z>/<x>/<y>.ext
* @param array $params
*/
public function setParams($params) {
if (isset($params[1])) {
$this->layer = $params[1];
}
$params = array_reverse($params);
if (isset($params[2])) {
$this->z = $params[2];
$this->x = $params[1];
$file = explode('.', $params[0]);
$this->y = $file[0];
$this->ext = isset($file[1]) ? $file[1] : NULL;
}
}
/**
* Get variable don't independent on sensitivity
* @param string $key
* @return boolean
*/
public function getGlobal($isKey) {
$get = $_GET;
foreach ($get as $key => $value) {
if (strtolower($isKey) == strtolower($key)) {
return $value;
}
}
return FALSE;
}
/**
* Testing if is a database layer
* @param string $layer
* @return boolean
*/
public function isDBLayer($layer) {
if (is_file($this->config['dataRoot'] . $layer . '.mbtiles')) {
return TRUE;
} else {
return FALSE;
}
}
/**
* Testing if is a file layer
* @param string $layer
* @return boolean
*/
public function isFileLayer($layer) {
if (is_dir($layer)) {
return TRUE;
} else {
return FALSE;
}
}
/**
* Get metadata from metadataJson
* @param string $jsonFileName
* @return array
*/
public function metadataFromMetadataJson($jsonFileName) {
$metadata = json_decode(file_get_contents($jsonFileName), true);
$metadata['basename'] = str_replace('/metadata.json', '', $jsonFileName);
return $this->metadataValidation($metadata);
}
/**
* Loads metadata from MBtiles
* @param string $mbt
* @return object
*/
public function metadataFromMbtiles($mbt) {
$metadata = array();
$this->DBconnect($mbt);
$result = $this->db->query('select * from metadata');
$resultdata = $result->fetchAll();
foreach ($resultdata as $r) {
$value = preg_replace('/(\\n)+/', '', $r['value']);
$metadata[$r['name']] = addslashes($value);
}
if (!array_key_exists('minzoom', $metadata)
|| !array_key_exists('maxzoom', $metadata)
) {
// autodetect minzoom and maxzoom
$result = $this->db->query('select min(zoom_level) as min, max(zoom_level) as max from tiles');
$resultdata = $result->fetchAll();
if (!array_key_exists('minzoom', $metadata)){
$metadata['minzoom'] = $resultdata[0]['min'];
}
if (!array_key_exists('maxzoom', $metadata)){
$metadata['maxzoom'] = $resultdata[0]['max'];
}
}
// autodetect format using JPEG magic number FFD8
if (!array_key_exists('format', $metadata)) {
$result = $this->db->query('select hex(substr(tile_data,1,2)) as magic from tiles limit 1');
$resultdata = $result->fetchAll();
$metadata['format'] = ($resultdata[0]['magic'] == 'FFD8')
? 'jpg'
: 'png';
}
// autodetect bounds
if (!array_key_exists('bounds', $metadata)) {
$result = $this->db->query('select min(tile_column) as w, max(tile_column) as e, min(tile_row) as s, max(tile_row) as n from tiles where zoom_level='.$metadata['maxzoom']);
$resultdata = $result->fetchAll();
$w = -180 + 360 * ($resultdata[0]['w'] / pow(2, $metadata['maxzoom']));
$e = -180 + 360 * ((1 + $resultdata[0]['e']) / pow(2, $metadata['maxzoom']));
$n = $this->row2lat($resultdata[0]['n'], $metadata['maxzoom']);
$s = $this->row2lat($resultdata[0]['s'] - 1, $metadata['maxzoom']);
$metadata['bounds'] = implode(',', array($w, $s, $e, $n));
}
$mbt = explode('.', $mbt);
$metadata['basename'] = $mbt[0];
$metadata = $this->metadataValidation($metadata);
return $metadata;
}
/**
* Convert row number to latitude of the top of the row
* @param integer $r
* @param integer $zoom
* @return integer
*/
public function row2lat($r, $zoom) {
$y = $r / pow(2, $zoom - 1 ) - 1;
return rad2deg(2.0 * atan(exp(3.191459196 * $y)) - 1.57079632679489661922);
}
/**
* Valids metaJSON
* @param object $metadata
* @return object
*/
public function metadataValidation($metadata) {
if (!array_key_exists('bounds', $metadata)) {
$metadata['bounds'] = array(-180, -85.06, 180, 85.06);
} elseif (!is_array($metadata['bounds'])) {
$metadata['bounds'] = array_map('floatval', explode(',', $metadata['bounds']));
}
if (!array_key_exists('profile', $metadata)) {
$metadata['profile'] = 'mercator';
}
if (array_key_exists('minzoom', $metadata)){
$metadata['minzoom'] = intval($metadata['minzoom']);
}else{
$metadata['minzoom'] = 0;
}
if (array_key_exists('maxzoom', $metadata)){
$metadata['maxzoom'] = intval($metadata['maxzoom']);
}else{
$metadata['maxzoom'] = 18;
}
if (!array_key_exists('format', $metadata)) {
if(array_key_exists('tiles', $metadata)){
$pos = strrpos($metadata['tiles'][0], '.');
$metadata['format'] = trim(substr($metadata['tiles'][0], $pos + 1));
}
}
$formats = $this->config['availableFormats'];
if(!in_array(strtolower($metadata['format']), $formats)){
$metadata['format'] = 'png';
}
if (!array_key_exists('scale', $metadata)) {
$metadata['scale'] = 1;
}
if(!array_key_exists('tiles', $metadata)){
$tiles = array();
foreach ($this->config['baseUrls'] as $url) {
$url = '' . $this->config['protocol'] . '://' . $url . '/' .
$metadata['basename'] . '/{z}/{x}/{y}';
if(strlen($metadata['format']) <= 4){
$url .= '.' . $metadata['format'];
}
$tiles[] = $url;
}
$metadata['tiles'] = $tiles;
}
return $metadata;
}
/**
* SQLite connection
* @param string $tileset
*/
public function DBconnect($tileset) {
try {
$this->db = new PDO('sqlite:' . $tileset, '', '', array(PDO::ATTR_PERSISTENT => true));
} catch (Exception $exc) {
echo $exc->getTraceAsString();
die;
}
if (!isset($this->db)) {
header('Content-type: text/plain');
echo 'Incorrect tileset name: ' . $tileset;
die;
}
}
/**
* Check if file is modified and set Etag headers
* @param string $filename
* @return boolean
*/
public function isModified($filename) {
$filename = $this->config['dataRoot'] . $filename . '.mbtiles';
$lastModifiedTime = filemtime($filename);
$eTag = md5($lastModifiedTime);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModifiedTime) . ' GMT');
header('Etag:' . $eTag);
if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime ||
@trim($_SERVER['HTTP_IF_NONE_MATCH']) == $eTag) {
return TRUE;
} else {
return FALSE;
}
}
/**
* Returns tile of dataset
* @param string $tileset
* @param integer $z
* @param integer $y
* @param integer $x
* @param string $ext
*/
public function renderTile($tileset, $z, $y, $x, $ext) {
if ($this->isDBLayer($tileset)) {
if ($this->isModified($tileset) == TRUE) {
header('Access-Control-Allow-Origin: *');
header('HTTP/1.1 304 Not Modified');
die;
}
$this->DBconnect($this->config['dataRoot'] . $tileset . '.mbtiles');
$z = floatval($z);
$y = floatval($y);
$x = floatval($x);
$flip = true;
if ($flip) {
$y = pow(2, $z) - 1 - $y;
}
$result = $this->db->query('select tile_data as t from tiles where zoom_level=' . $z . ' and tile_column=' . $x . ' and tile_row=' . $y);
$data = $result->fetchColumn();
if (!isset($data) || $data === FALSE) {
//if tile doesn't exist
//select scale of tile (for retina tiles)
$result = $this->db->query('select value from metadata where name="scale"');
$resultdata = $result->fetchColumn();
$scale = isset($resultdata) && $resultdata !== FALSE ? $resultdata : 1;
$this->getCleanTile($scale, $ext);
} else {
$result = $this->db->query('select value from metadata where name="format"');
$resultdata = $result->fetchColumn();
$format = isset($resultdata) && $resultdata !== FALSE ? $resultdata : 'png';
if ($format == 'jpg') {
$format = 'jpeg';
}
if ($format == 'pbf') {
header('Content-type: application/x-protobuf');
header('Content-Encoding:gzip');
} else {
header('Content-type: image/' . $format);
}
header('Access-Control-Allow-Origin: *');
echo $data;
}
} elseif ($this->isFileLayer($tileset)) {
$name = './' . $tileset . '/' . $z . '/' . $x . '/' . $y;
$mime = 'image/';
if($ext != NULL){
$name .= '.' . $ext;
}
if ($fp = @fopen($name, 'rb')) {
if($ext != NULL){
$mime .= $ext;
}else{
//detect image type from file
$mimetypes = array('gif', 'jpeg', 'png');
$mime .= $mimetypes[exif_imagetype($name) - 1];
}
header('Access-Control-Allow-Origin: *');
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($name));
fpassthru($fp);
die;
} else {
//scale of tile (for retina tiles)
$meta = json_decode(file_get_contents($tileset . '/metadata.json'));
if(!isset($meta->scale)){
$meta->scale = 1;
}
}
$this->getCleanTile($meta->scale, $ext);
} else {
header('HTTP/1.1 404 Not Found');
echo 'Server: Unknown or not specified dataset "' . $tileset . '"';
die;
}
}
/**
* Returns clean tile
* @param integer $scale Default 1
*/
public function getCleanTile($scale = 1, $format = 'png') {
switch ($format) {
case 'pbf':
header('Access-Control-Allow-Origin: *');
header('HTTP/1.1 204 No Content');
header('Content-Type: application/json; charset=utf-8');
echo '{"message":"Tile does not exist"}';
break;
case 'webp':
header('Access-Control-Allow-Origin: *');
header('Content-type: image/webp');
echo base64_decode('UklGRhIAAABXRUJQVlA4TAYAAAAvQWxvAGs=');
break;
case 'jpg':
header('Access-Control-Allow-Origin: *');
header('Content-type: image/jpg');
echo base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=');
break;
case 'png':
default:
header('Access-Control-Allow-Origin: *');
header('Content-type: image/png');
// 256x256 transparent optimised png tile
echo unpack('H', '89504e470d0a1a0a0000000d494844520000010000000100010300000066bc3a2500000003504c5445000000a77a3dda0000000174524e530040e6d8660000001f494441541819edc1010d000000c220fba77e0e37600000000000000000e70221000001f5a2bd040000000049454e44ae426082');
break;
}
die;
}
/**
* Returns tile's UTFGrid
* @param string $tileset
* @param integer $z
* @param integer $y
* @param integer $x
*/
public function renderUTFGrid($tileset, $z, $y, $x, $flip = TRUE) {
if ($this->isDBLayer($tileset)) {
if ($this->isModified($tileset) == TRUE) {
header('HTTP/1.1 304 Not Modified');
}
if ($flip) {
$y = pow(2, $z) - 1 - $y;
}
try {
$this->DBconnect($this->config['dataRoot'] . $tileset . '.mbtiles');
$query = 'SELECT grid FROM grids WHERE tile_column = ' . $x . ' AND '
. 'tile_row = ' . $y . ' AND zoom_level = ' . $z;
$result = $this->db->query($query);
$data = $result->fetch(PDO::FETCH_ASSOC);
if ($data !== FALSE) {
$grid = gzuncompress($data['grid']);
$grid = substr(trim($grid), 0, -1);
//adds legend (data) to output
$grid .= ',"data":{';
$kquery = 'SELECT key_name as key, key_json as json FROM grid_data '
. 'WHERE zoom_level=' . $z . ' and '
. 'tile_column=' . $x . ' and tile_row=' . $y;
$result = $this->db->query($kquery);
while ($r = $result->fetch(PDO::FETCH_ASSOC)) {
$grid .= '"' . $r['key'] . '":' . $r['json'] . ',';
}
$grid = rtrim($grid, ',') . '}}';
header('Access-Control-Allow-Origin: *');
if (isset($_GET['callback']) && !empty($_GET['callback'])) {
header('Content-Type:text/javascript charset=utf-8');
echo $_GET['callback'] . '(' . $grid . ');';
} else {
header('Content-Type:text/javascript; charset=utf-8');
echo $grid;
}
} else {
header('Access-Control-Allow-Origin: *');
echo '{}';
die;
}
} catch (Exception $e) {
header('Content-type: text/plain');
print 'Error querying the database: ' . $e->getMessage();
}
} else {
echo 'Server: no MBTiles tileset';
die;
}
}
/**
* Returns server info
*/
public function getInfo() {
$this->setDatasets();
$maps = array_merge($this->fileLayer, $this->dbLayer);
header('Content-Type: text/html;charset=UTF-8');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>' . $this->config['serverTitle'] . '</title></head><body>' .
'<h1>' . $this->config['serverTitle'] . '</h1>' .
'TileJSON service: <a href="//' . $this->config['baseUrls'][0] . '/index.json">' . $this->config['baseUrls'][0] . '/index.json</a><br>' .
'WMTS service: <a href="//' . $this->config['baseUrls'][0] . '/wmts">' . $this->config['baseUrls'][0] . '/wmts</a><br>' .
'TMS service: <a href="//' . $this->config['baseUrls'][0] . '/tms">' . $this->config['baseUrls'][0] . '/tms</a>';
foreach ($maps as $map) {
$extend = '[' . implode($map['bounds'], ', ') . ']';
echo '<p>Tileset: <b>' . $map['basename'] . '</b><br>' .
'Metadata: <a href="//' . $this->config['baseUrls'][0] . '/' . $map['basename'] . '.json">' .
$this->config['baseUrls'][0] . '/' . $map['basename'] . '.json</a><br>' .
'Bounds: ' . $extend ;
if(isset($map['crs'])){echo '<br>CRS: ' . $map['crs'];}
echo '</p>';
}
echo '<p>Copyright (C) 2016 - Klokan Technologies GmbH</p>';
echo '</body></html>';
}
/**
* Returns html viewer
*/
public function getHtml() {
$this->setDatasets();
$maps = array_merge($this->fileLayer, $this->dbLayer);
if (isset($this->config['template']) && file_exists($this->config['template'])) {
$baseUrls = $this->config['baseUrls'];
$serverTitle = $this->config['serverTitle'];
include_once $this->config['template'];
} else {
header('Content-Type: text/html;charset=UTF-8');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>' . $this->config['serverTitle'] . '</title>';
echo '<link rel="stylesheet" type="text/css" href="//cdn.klokantech.com/tileviewer/v1/index.css" />
<script src="//cdn.klokantech.com/tileviewer/v1/index.js"></script><body>
<script>tileserver({index:"' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/index.json", tilejson:"' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/%n.json", tms:"' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/tms", wmts:"' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts"});</script>
<h1>Welcome to ' . $this->config['serverTitle'] . '</h1>
<p>This server distributes maps to desktop, web, and mobile applications.</p>
<p>The mapping data are available as OpenGIS Web Map Tiling Service (OGC WMTS), OSGEO Tile Map Service (TMS), and popular XYZ urls described with TileJSON metadata.</p>';
if (!isset($maps)) {
echo '<h3 style="color:darkred;">No maps available yet</h3>
<p style="color:darkred; font-style: italic;">
Ready to go - just upload some maps into directory:' . getcwd() . '/ on this server.</p>
<p>Note: The maps can be a directory with tiles in XYZ format with metadata.json file.<br/>
You can easily convert existing geodata (GeoTIFF, ECW, MrSID, etc) to this tile structure with <a href="http://www.maptiler.com">MapTiler Cluster</a> or open-source projects such as <a href="http://www.klokan.cz/projects/gdal2tiles/">GDAL2Tiles</a> or <a href="http://www.maptiler.org/">MapTiler</a> or simply upload any maps in MBTiles format made by <a href="http://www.tilemill.com/">TileMill</a>. Helpful is also the <a href="https://github.com/mapbox/mbutil">mbutil</a> tool. Serving directly from .mbtiles files is supported, but with decreased performance.</p>';
} else {
echo '<ul>';
foreach ($maps as $map) {
echo "<li>" . $map['name'] . '</li>';
}
echo '</ul>';
}
echo '</body></html>';
}
}
}
/**
* JSON service
*/
class Json extends Server {
/**
* Callback for JSONP default grid
* @var string
*/
private $callback = 'grid';
/**
* @param array $params
*/
public $layer = 'index';
/**
* @var integer
*/
public $z;
/**
* @var integer
*/
public $y;
/**
* @var integer
*/
public $x;
/**
* @var string
*/
public $ext;
/**
*
* @param array $params
*/
public function __construct($params) {
parent::__construct();
parent::setParams($params);
if (isset($_GET['callback']) && !empty($_GET['callback'])) {
$this->callback = $_GET['callback'];
}
}
/**
* Adds metadata about layer
* @param array $metadata
* @return array
*/
public function metadataTileJson($metadata) {
$metadata['tilejson'] = '2.0.0';
$metadata['scheme'] = 'xyz';
if ($this->isDBLayer($metadata['basename'])) {
$this->DBconnect($this->config['dataRoot'] . $metadata['basename'] . '.mbtiles');
$res = $this->db->query('SELECT name FROM sqlite_master WHERE name="grids";');
if ($res) {
foreach ($this->config['baseUrls'] as $url) {
$grids[] = '' . $this->config['protocol'] . '://' . $url . '/' . $metadata['basename'] . '/{z}/{x}/{y}.grid.json';
}
$metadata['grids'] = $grids;
}
}
if (array_key_exists('json', $metadata)) {
$mjson = json_decode(stripslashes($metadata['json']));
foreach ($mjson as $key => $value) {
$metadata[$key] = $value;
}
unset($metadata['json']);
}
return $metadata;
}
/**
* Creates JSON from array
* @param string $basename
* @return string
*/
private function createJson($basename) {
$maps = array_merge($this->fileLayer, $this->dbLayer);
if ($basename == 'index') {
$output = '[';
foreach ($maps as $map) {
$output = $output . json_encode($this->metadataTileJson($map)) . ',';
}
if (strlen($output) > 1) {
$output = substr_replace($output, ']', -1);
} else {
$output = $output . ']';
}
} else {
foreach ($maps as $map) {
if (strpos($map['basename'], $basename) !== false) {
$output = json_encode($this->metadataTileJson($map));
break;
}
}
}
if (!isset($output)) {
echo 'TileServer: unknown map ' . $basename;
die;
}
return stripslashes($output);
}
/**
* Returns JSON with callback
*/
public function getJson() {
parent::setDatasets();
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json; charset=utf-8');
if ($this->callback !== 'grid') {
echo $this->callback . '(' . $this->createJson($this->layer) . ');'; die;
} else {
echo $this->createJson($this->layer); die;
}
}
/**
* Returns JSONP with callback
*/
public function getJsonp() {
parent::setDatasets();
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/javascript; charset=utf-8');
echo $this->callback . '(' . $this->createJson($this->layer) . ');';
}
/**
* Returns UTFGrid in JSON format
*/
public function getUTFGrid() {
parent::renderUTFGrid($this->layer, $this->z, $this->y, $this->x);
}
}
/**
* Web map tile service
*/
class Wmts extends Server {
/**
* @param array $params
*/
public $layer;
/**
* @var integer
*/
public $z;
/**
* @var integer
*/
public $y;
/**
* @var integer
*/
public $x;
/**
* @var string
*/
public $ext;
/**
*
* @param array $params
*/
public function __construct($params) {
parent::__construct();
if (isset($params)) {
parent::setParams($params);
}
}
/**
* Tests request from url and call method
*/
public function get() {
$request = $this->getGlobal('Request');
if ($request !== FALSE && $request == 'gettile') {
$this->getTile();
} else {
parent::setDatasets();
$this->getCapabilities();
}
}
/**
* Validates tilematrixset, calculates missing params
* @param Object $tileMatrix
* @return Object
*/
public function parseTileMatrix($layer, $tileMatrix){
//process projection
if(isset($layer['proj4'])){
preg_match_all("/([^+= ]+)=([^= ]+)/", $layer['proj4'], $res);
$proj4 = array_combine($res[1], $res[2]);
}
for($i = 0; $i < count($tileMatrix); $i++){
if(!isset($tileMatrix[$i]['id'])){
$tileMatrix[$i]['id'] = (string) $i;
}
if (!isset($tileMatrix[$i]['extent']) && isset($layer['extent'])) {
$tileMatrix[$i]['extent'] = $layer['extent'];
}
if (!isset($tileMatrix[$i]['matrix_size'])) {
$tileExtent = $this->tilesOfExtent(
$tileMatrix[$i]['extent'],
$tileMatrix[$i]['origin'],
$tileMatrix[$i]['pixel_size'],
$tileMatrix[$i]['tile_size']
);
$tileMatrix[$i]['matrix_size'] = array(
$tileExtent[2] + 1,
$tileExtent[1] + 1
);
}
if(!isset($tileMatrix[$i]['origin']) && isset($tileMatrix[$i]['extent'])){
$tileMatrix[$i]['origin'] = array(
$tileMatrix[$i]['extent'][0], $tileMatrix[$i]['extent'][3]
);
}
// Origins of geographic coordinate systems are setting in opposite order
if (isset($proj4) && $proj4['proj'] === 'longlat') {
$tileMatrix[$i]['origin'] = array_reverse($tileMatrix[$i]['origin']);
}
if(!isset($tileMatrix[$i]['scale_denominator'])){
$tileMatrix[$i]['scale_denominator'] = count($tileMatrix) - $i;
}
if(!isset($tileMatrix[$i]['tile_size'])){
$tileSize = 256 * (int) $layer['scale'];
$tileMatrix[$i]['tile_size'] = array($tileSize, $tileSize);
}
}
return $tileMatrix;
}
/**
* Calculates corners of tilematrix
* @param array $extent
* @param array $origin
* @param array $pixel_size
* @param array $tile_size
* @return array
*/
public function tilesOfExtent($extent, $origin, $pixel_size, $tile_size) {
$tiles = array(
$this->minsample($extent[0] - $origin[0], $pixel_size[0] * $tile_size[0]),
$this->minsample($extent[1] - $origin[1], $pixel_size[1] * $tile_size[1]),
$this->maxsample($extent[2] - $origin[0], $pixel_size[0] * $tile_size[0]),
$this->maxsample($extent[3] - $origin[1], $pixel_size[1] * $tile_size[1]),
);
return $tiles;
}
private function minsample($x, $f){
return $f > 0 ? floor($x / $f) : ceil(($x / $f) - 1);
}
private function maxsample($x, $f){
return $f < 0 ? floor($x / $f) : ceil(($x / $f) - 1);
}
/**
* Default TileMetrixSet for Pseudo Mercator projection 3857
* @param ?number $maxZoom
* @return string TileMatrixSet xml
*/
public function getMercatorTileMatrixSet($maxZoom = 18){
$denominatorBase = 559082264.0287178;
$extent = array(-20037508.34,-20037508.34,20037508.34,20037508.34);
$tileMatrixSet = array();
for($i = 0; $i <= $maxZoom; $i++){
$matrixSize = pow(2, $i);
$tileMatrixSet[] = array(
'extent' => $extent,
'id' => (string) $i,
'matrix_size' => array($matrixSize, $matrixSize),
'origin' => array($extent[0], $extent[3]),
'scale_denominator' => $denominatorBase / pow(2, $i),
'tile_size' => array(256, 256)
);
}
return $this->getTileMatrixSet('GoogleMapsCompatible', $tileMatrixSet, 'EPSG:3857');
}
/**
* Default TileMetrixSet for WGS84 projection 4326
* @return string Xml
*/
public function getWGS84TileMatrixSet(){
$extent = array(-180.000000, -90.000000, 180.000000, 90.000000);
$scaleDenominators = array(279541132.01435887813568115234, 139770566.00717943906784057617,
69885283.00358971953392028809, 34942641.50179485976696014404, 17471320.75089742988348007202,
8735660.37544871494174003601, 4367830.18772435747087001801, 2183915.09386217873543500900,
1091957.54693108936771750450, 545978.77346554468385875225, 272989.38673277234192937613,
136494.69336638617096468806, 68247.34668319308548234403, 34123.67334159654274117202,
17061.83667079825318069197, 8530.91833539912659034599, 4265.45916769956329517299,
2132.72958384978574031265);
$tileMatrixSet = array();
for($i = 0; $i <= 17; $i++){
$matrixSize = pow(2, $i);
$tileMatrixSet[] = array(
'extent' => $extent,
'id' => (string) $i,
'matrix_size' => array($matrixSize * 2, $matrixSize),
'origin' => array($extent[3], $extent[0]),
'scale_denominator' => $scaleDenominators[$i],
'tile_size' => array(256, 256)
);
}
return $this->getTileMatrixSet('WGS84', $tileMatrixSet, 'EPSG:4326');
}
/**
* Prints WMTS TileMatrixSet
* @param string $name
* @param array $tileMatrixSet Array of levels
* @param string $crs Code of crs eg: EPSG:3857
* @return string TileMatrixSet xml
*/
public function getTileMatrixSet($name, $tileMatrixSet, $crs = 'EPSG:3857'){
$srs = explode(':', $crs);
$TileMatrixSet = '<TileMatrixSet>
<ows:Title>' . $name . '</ows:Title>
<ows:Abstract>' . $name . ' '. $crs .'</ows:Abstract>
<ows:Identifier>' . $name . '</ows:Identifier>
<ows:SupportedCRS>urn:ogc:def:crs:'.$srs[0].'::'.$srs[1].'</ows:SupportedCRS>';
// <WellKnownScaleSet>urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible</WellKnownScaleSet>;
foreach($tileMatrixSet as $level){
$TileMatrixSet .= '
<TileMatrix>
<ows:Identifier>' . $level['id'] . '</ows:Identifier>
<ScaleDenominator>' . $level['scale_denominator'] . '</ScaleDenominator>
<TopLeftCorner>'. $level['origin'][0] . ' ' . $level['origin'][1] .'</TopLeftCorner>
<TileWidth>' . $level['tile_size'][0] . '</TileWidth>
<TileHeight>' . $level['tile_size'][1] . '</TileHeight>
<MatrixWidth>' . $level['matrix_size'][0] . '</MatrixWidth>
<MatrixHeight>' . $level['matrix_size'][1] . '</MatrixHeight>
</TileMatrix>';
}
$TileMatrixSet .= '</TileMatrixSet>';
return $TileMatrixSet;
}
/**
* Returns tilesets getCapabilities
*/
public function getCapabilities() {
$layers = array_merge($this->fileLayer, $this->dbLayer);
//if TileMatrixSet is provided validate it
for($i = 0; $i < count($layers); $i++){
if($layers[$i]['profile'] == 'custom'){
$layers[$i]['tile_matrix'] = $this->parseTileMatrix(
$layers[$i],
$layers[$i]['tile_matrix']
);
}
}
header('Content-type: application/xml');
echo '<?xml version="1.0" encoding="UTF-8" ?>
<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" version="1.0.0">
<!-- Service Identification -->
<ows:ServiceIdentification>
<ows:Title>tileserverphp</ows:Title>
<ows:ServiceType>OGC WMTS</ows:ServiceType>
<ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion>
</ows:ServiceIdentification>
<!-- Operations Metadata -->
<ows:OperationsMetadata>
<ows:Operation name="GetCapabilities">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts/1.0.0/WMTSCapabilities.xml">
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>RESTful</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
<!-- add KVP binding in 10.1 -->
<ows:Get xlink:href="' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts?">
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>KVP</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
</ows:HTTP>
</ows:DCP>
</ows:Operation>
<ows:Operation name="GetTile">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts/">
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>RESTful</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
<ows:Get xlink:href="' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts?">
<ows:Constraint name="GetEncoding">
<ows:AllowedValues>
<ows:Value>KVP</ows:Value>
</ows:AllowedValues>
</ows:Constraint>
</ows:Get>
</ows:HTTP>
</ows:DCP>
</ows:Operation>
</ows:OperationsMetadata>
<Contents>';
$customtileMatrixSets = '';
$maxMercatorZoom = 18;
//layers
foreach ($layers as $m) {
$basename = $m['basename'];
$title = (array_key_exists('name', $m)) ? $m['name'] : $basename;
$profile = $m['profile'];
$bounds = $m['bounds'];
$format = $m['format'] == 'hybrid' ? 'jpgpng' : $m['format'];
$mime = ($format == 'jpg') ? 'image/jpeg' : 'image/' . $format;
if ($profile == 'geodetic') {
$tileMatrixSet = 'WGS84';
}elseif ($m['profile'] == 'custom') {
$crs = explode(':', $m['crs']);
$tileMatrixSet = 'custom' . $crs[1] . $m['basename'];
$customtileMatrixSets .= $this->getTileMatrixSet(
$tileMatrixSet,
$m['tile_matrix'],
$m['crs']
);
} else {
$tileMatrixSet = 'GoogleMapsCompatible';
$maxMercatorZoom = max($maxMercatorZoom, $m['maxzoom']);
}
$wmtsHost = substr($m['tiles'][0], 0, strpos($m['tiles'][0], $m['basename']));
$resourceUrlTemplate = $wmtsHost . $basename
. '/{TileMatrix}/{TileCol}/{TileRow}';
if(strlen($format) <= 4){
$resourceUrlTemplate .= '.' . $format;
}
echo'
<Layer>
<ows:Title>' . $title . '</ows:Title>
<ows:Identifier>' . $basename . '</ows:Identifier>
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
<ows:LowerCorner>' . $bounds[0] . ' ' . $bounds[1] . '</ows:LowerCorner>
<ows:UpperCorner>' . $bounds[2] . ' ' . $bounds[3] . '</ows:UpperCorner>
</ows:WGS84BoundingBox>
<Style isDefault="true">
<ows:Identifier>default</ows:Identifier>
</Style>
<Format>' . $mime . '</Format>
<TileMatrixSetLink>
<TileMatrixSet>' . $tileMatrixSet . '</TileMatrixSet>
</TileMatrixSetLink>
<ResourceURL format="' . $mime . '" resourceType="tile" template="' . $resourceUrlTemplate . '"/>
</Layer>';
}
// Print custom TileMatrixSets
if (strlen($customtileMatrixSets) > 0) {
echo $customtileMatrixSets;
}
// Print PseudoMercator TileMatrixSet
echo $this->getMercatorTileMatrixSet($maxMercatorZoom);
// Print WGS84 TileMatrixSet
echo $this->getWGS84TileMatrixSet();
echo '</Contents>
<ServiceMetadataURL xlink:href="' . $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/wmts/1.0.0/WMTSCapabilities.xml"/>
</Capabilities>';
}
/**
* Returns tile via WMTS specification
*/
public function getTile() {
$request = $this->getGlobal('Request');
if ($request) {
if (strpos('/', $_GET['Format']) !== FALSE) {
$format = explode('/', $_GET['Format']);
$format = $format[1];
} else {
$format = $this->getGlobal('Format');
}
parent::renderTile(
$this->getGlobal('Layer'),
$this->getGlobal('TileMatrix'),
$this->getGlobal('TileRow'),
$this->getGlobal('TileCol'),
$format
);
} else {
parent::renderTile($this->layer, $this->z, $this->y, $this->x, $this->ext);
}
}
}
/**
* Tile map service
*/
class Tms extends Server {
/**
* @param array $params
*/
public $layer;
/**
* @var integer
*/
public $z;
/**
* @var integer
*/
public $y;
/**
* @var integer
*/
public $x;
/**
* @var string
*/
public $ext;
/**
*
* @param array $params
*/
public function __construct($params) {
parent::__construct();
parent::setParams($params);
}
/**
* Returns getCapabilities metadata request
*/
public function getCapabilities() {
parent::setDatasets();
$maps = array_merge($this->fileLayer, $this->dbLayer);
header('Content-type: application/xml');
echo'<TileMapService version="1.0.0"><TileMaps>';
foreach ($maps as $m) {
$basename = $m['basename'];
$title = (array_key_exists('name', $m) ) ? $m['name'] : $basename;
$profile = $m['profile'];
if ($profile == 'geodetic') {
$srs = 'EPSG:4326';
} else {
$srs = 'EPSG:3857';
}
$url = $this->config['protocol'] . '://' . $this->config['baseUrls'][0]
. '/tms/' . $basename;
echo '<TileMap title="' . $title . '" srs="' . $srs
. '" type="InvertedTMS" ' . 'profile="global-' . $profile
. '" href="' . $url . '" />';
}
echo '</TileMaps></TileMapService>';
}
/**
* Prints metadata about layer
*/
public function getLayerCapabilities() {
parent::setDatasets();
$maps = array_merge($this->fileLayer, $this->dbLayer);
foreach ($maps as $map) {
if (strpos($map['basename'], $this->layer) !== false) {
$m = $map;
break;
}
}
$title = (array_key_exists('name', $m)) ? $m['name'] : $m['basename'];
$description = (array_key_exists('description', $m)) ? $m['description'] : "";
$bounds = $m['bounds'];
if ($m['profile'] == 'geodetic') {
$srs = 'EPSG:4326';
$initRes = 0.703125;
} elseif ($m['profile'] == 'custom') {
$srs = $m['crs'];
$bounds = $m['extent'];
if(isset($m['tile_matrix'][0]['pixel_size'][0])){
$initRes = $m['tile_matrix'][0]['pixel_size'][0];
}else{
$initRes = 1;
}
} else {
$srs = 'EPSG:3857';
$bounds = array(-20037508.34,-20037508.34,20037508.34,20037508.34);
$initRes = 156543.03392804062;
}
$mime = ($m['format'] == 'jpg') ? 'image/jpeg' : 'image/png';
header("Content-type: application/xml");
$serviceUrl = $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/' . $m['basename'];
echo '<TileMap version="1.0.0" tilemapservice="' . $serviceUrl . '" type="InvertedTMS">
<Title>' . htmlspecialchars($title) . '</Title>
<Abstract>' . htmlspecialchars($description) . '</Abstract>
<SRS>' . $srs . '</SRS>
<BoundingBox minx="' . $bounds[0] . '" miny="' . $bounds[1] . '" maxx="' . $bounds[2] . '" maxy="' . $bounds[3] . '" />
<Origin x="' . $bounds[0] . '" y="' . $bounds[1] . '"/>
<TileFormat width="256" height="256" mime-type="' . $mime . '" extension="' . $m['format'] . '"/>
<TileSets profile="global-' . $m['profile'] . '">';
for ($zoom = $m['minzoom']; $zoom < $m['maxzoom'] + 1; $zoom++) {
$res = $initRes / pow(2, $zoom);
$url = $this->config['protocol'] . '://' . $this->config['baseUrls'][0] . '/' . $m['basename'] . '/' . $zoom;
echo '<TileSet href="' . $url . '" units-per-pixel="' . $res . '" order="' . $zoom . '" />';
}
echo'</TileSets></TileMap>';
}
/**
* Process getTile request
*/
public function getTile() {
parent::renderTile($this->layer, $this->z, $this->y, $this->x, $this->ext);
}
}
/**
* Simple router
*/
class Router {
/**
* @param array $routes
*/
public static function serve($routes) {
$request_method = strtolower($_SERVER['REQUEST_METHOD']);
$path_info = '/';
global $config;
$config['protocol'] = ( isset($_SERVER["HTTPS"]) or $_SERVER['SERVER_PORT'] == '443') ? "https" : "http";
if (!empty($_SERVER['PATH_INFO'])) {
$path_info = $_SERVER['PATH_INFO'];
} else if (!empty($_SERVER['ORIG_PATH_INFO']) && strpos($_SERVER['ORIG_PATH_INFO'], 'tileserver.php') === false) {
$path_info = $_SERVER['ORIG_PATH_INFO'];
} else if (!empty($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/tileserver.php') !== false) {
$path_info = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$config['baseUrls'][0] = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . '?';
} else {
if (!empty($_SERVER['REQUEST_URI'])) {
$path_info = (strpos($_SERVER['REQUEST_URI'], '?') > 0) ? strstr($_SERVER['REQUEST_URI'], '?', true) : $_SERVER['REQUEST_URI'];
}
}
$discovered_handler = null;
$regex_matches = array();
if ($routes) {
$tokens = array(
':string' => '([a-zA-Z]+)',
':number' => '([0-9]+)',
':alpha' => '([a-zA-Z0-9-_@\.]+)'
);
//global $config;
foreach ($routes as $pattern => $handler_name) {
$pattern = strtr($pattern, $tokens);
if (preg_match('#/?' . $pattern . '/?$#', $path_info, $matches)) {
if (!isset($config['baseUrls'])) {
$config['baseUrls'][0] = $_SERVER['HTTP_HOST'] . preg_replace('#/?' . $pattern . '/?$#', '', $path_info);
}
$discovered_handler = $handler_name;
$regex_matches = $matches;
break;
}
}
}
$handler_instance = null;
if ($discovered_handler) {
if (is_string($discovered_handler)) {
if (strpos($discovered_handler, ':') !== false) {
$discoverered_class = explode(':', $discovered_handler);
$discoverered_method = explode(':', $discovered_handler);
$handler_instance = new $discoverered_class[0]($regex_matches);
call_user_func(array($handler_instance, $discoverered_method[1]));
} else {
$handler_instance = new $discovered_handler($regex_matches);
}
} elseif (is_callable($discovered_handler)) {
$handler_instance = $discovered_handler();
}
} else {
if (!isset($config['baseUrls'][0])) {
$config['baseUrls'][0] = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
if (strpos($_SERVER['REQUEST_URI'], '=') != FALSE) {
$kvp = explode('=', $_SERVER['REQUEST_URI']);
$_GET['callback'] = $kvp[1];
$params[0] = 'index';
$handler_instance = new Json($params);
$handler_instance->getJson();
}
$handler_instance = new Server;
$handler_instance->getHtml();
}
}
}