diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..8d3a3f2
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,10 @@
+
+ Order allow,deny
+ Deny from all
+
+
+RewriteEngine On
+#RewriteBase /
+
+# {r}/{z}/{x}/{y}.png --> index.php?r={r}&z={z}&x={x}&y={y}
+RewriteRule ^([^\/]+)/([0-9]+)/([0-9]+)/([0-9]+).png$ index.php?r=$1&z=$2&x=$3&y=$4 [L]
diff --git a/config.php b/config.php
new file mode 100644
index 0000000..f32bbbf
--- /dev/null
+++ b/config.php
@@ -0,0 +1,53 @@
+
+ * @link https://github.com/Raruto/tile-proxy-php
+ * @license https://www.gnu.org/licenses/gpl-3.0.txt GNU/GPLv3
+ */
+
+/**
+ * Whitelist of supported tile servers
+ *
+ * @var array
+ */
+$tiles_config['servers'] = array(
+ 'osm' => 'https://{switch:a,b,c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ 'otm' => 'https://{switch:a,b,c}.tile.opentopomap.org/{z}/{x}/{y}.png',
+);
+
+/**
+ * Cached Bounding Box
+ *
+ * BBox = left, bottom, right, top
+ * BBox = min_lng, min_lat, max_lng, max_lat
+ *
+ * Planet: bbox = '-180,-90,180,90'
+ *
+ * @link https://openmaptiles.com/extracts/
+ *
+ * @var string
+ */
+$tiles_config['bbox'] = '6.602696,35.07638,19.12499,47.10169'; // Italy.
+
+/**
+ * Cache timeout in seconds
+ *
+ * 12 hour = 43200 sec
+ * 1 day = 86400 sec
+ * 1 month = 2629800 sec
+ *
+ * @var int
+ */
+$tiles_config['ttl'] = 86400;
+
+/**
+ * Custom Proxy Server headers
+ *
+ * @var string
+ */
+$tiles_config['headers'] = array(
+ 'Access-Control-Allow-Origin:' => '*',
+);
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..e3cbf9d
--- /dev/null
+++ b/index.php
@@ -0,0 +1,326 @@
+
+ * @link https://github.com/Raruto/tile-proxy-php
+ * @license https://www.gnu.org/licenses/gpl-3.0.txt GNU/GPLv3
+ */
+
+// User configs.
+require_once 'config.php';
+
+// Default configs.
+$servers = @$tiles_config['servers'] ?: array(
+ 'osm' => 'https://{switch:a,b,c}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ 'otm' => 'https://{switch:a,b,c}.tile.opentopomap.org/{z}/{x}/{y}.png',
+);
+$bbox = @$tiles_config['bbox'] ?: '-180,-90,180,90';
+$ttl = @$tiles_config['ttl'] ?: 86400;
+$headers = array_change_key_case(
+ @$tiles_config['headers'] ?: array(
+ 'Access-Control-Allow-Origin:' => '*',
+ ), CASE_LOWER
+);
+
+$bbox = empty( $_GET['bbox'] ) ? $bbox : $_GET['bbox'];
+$z = intval( $_GET['z'] );
+$x = intval( $_GET['x'] );
+$y = intval( $_GET['y'] );
+$r = strip_tags( $_GET['r'] );
+
+// allow only servers wich are defined within the tiles-config.php.
+if ( ! isset( $servers[ $r ] ) ) {
+ print_404_page();
+ exit;
+}
+
+/* // proxy //////////////////////////////////////////////////////////// */
+
+$url = generate_tile_server_url( $servers[ $r ] );
+
+$folder = "${r}/${z}/${x}";
+$file = $folder . "/${y}.png";
+
+$bbox = explode( ',', $bbox );
+$bbox_left = $bbox[0];
+$bbox_bottom = $bbox[1];
+$bbox_right = $bbox[2];
+$bbox_top = $bbox[3];
+
+// useful parameters to detect which tiles are within the bbox area.
+$min_tile_x = lng_to_tile_x( $bbox_left, $z );
+$max_tile_x = lng_to_tile_x( $bbox_right, $z );
+$min_tile_y = lat_to_tile_y( $bbox_top, $z );
+$max_tile_y = lat_to_tile_y( $bbox_bottom, $z );
+$num_tiles_row = ( $max_tile_x - $min_tile_x ) + 1;
+$num_tiles_col = ( $max_tile_y - $min_tile_y ) + 1;
+$num_tiles_bbox = $num_tiles_row * $num_tiles_col;
+
+// if (x, y, z) out bbox range: we don't cache it on this server.
+if ( ! in_range( $x, $min_tile_x, $max_tile_x ) || ! in_range( $y, $min_tile_y, $max_tile_y ) ) {
+ proxy_remote_file( $url );
+ exit;
+}
+
+// if (x, y, z) in bbox range: we cache it on this server.
+if ( ! is_file( $file ) || ( is_file_expired( $file ) && remote_file_exists( $url ) ) ) {
+ download_remote_file( $url, $folder, $file );
+}
+
+// Send to browser any previously cached tile.
+output_local_file( $file );
+exit;
+
+/* // functions ///////////////////////////////////////////////////////////// */
+
+/**
+ * Generate a real tile server url
+ *
+ * @param string $url eg: 'https://{switch:a,b,c}.tile.openstreetmap.org/{z}/{x}/{y}.png'.
+ * @return string eg: 'https://a.tile.openstreetmap.org/0/0/0.png'.
+ */
+function generate_tile_server_url( $url ) {
+ global $z, $x, $y;
+ $has_matches = preg_match( '/{switch:(.*?)}/', $url, $domains );
+ if ( ! $has_matches ) {
+ $domains = [ '', '' ];
+ }
+ $domains = explode( ',', $domains[1] );
+ $url = preg_replace( '/{switch:(.*?)}/', '{s}', $url );
+ $url = preg_replace( '/{s}/', $domains[ array_rand( $domains ) ], $url );
+ $url = preg_replace( '/{z}/', $z, $url );
+ $url = preg_replace( '/{x}/', $x, $url );
+ $url = preg_replace( '/{y}/', $y, $url );
+ return $url;
+}
+
+/**
+ * Convert Map Degrees to Radiant
+ *
+ * @param float $deg map degrees.
+ * @return float
+ */
+function deg_to_rad( $deg ) {
+ return $deg * M_PI / 180;
+}
+/**
+ * Convert Map Longitude to Tile-X Coordinates
+ *
+ * @param float $lng map longitude.
+ * @param int $zoom map zoom level.
+ * @return int
+ */
+function lng_to_tile_x( $lng, $zoom ) {
+ return floor( ( ( $lng + 180 ) / 360 ) * pow( 2, $zoom ) );
+}
+
+/**
+ * Convert Map Latitude to Tile-Y Coordinates
+ *
+ * @param float $lat map latitude.
+ * @param int $zoom map zoom level.
+ * @return int
+ */
+function lat_to_tile_y( $lat, $zoom ) {
+ return floor( ( 1 - log( tan( deg_to_rad( $lat ) ) + 1 / cos( deg_to_rad( $lat ) ) ) / M_PI ) / 2 * pow( 2, $zoom ) );
+}
+/**
+ * Finds if a number is within a given range
+ *
+ * @param float $number number to check.
+ * @param float $min left range number.
+ * @param float $max right range number.
+ * @return bool
+ */
+function in_range( $number, $min, $max ) {
+ return $number >= $min && $number <= $max;
+}
+
+/**
+ * Check if cached file is expired
+ *
+ * @param string $file filename path.
+ * @return bool
+ */
+function is_file_expired( $file ) {
+ global $ttl;
+ return filemtime( $file ) < time() - ( $ttl * 30 );
+}
+
+/**
+ * Check if a remote resource exists
+ *
+ * @param string $url remote file url.
+ * @return bool
+ */
+function remote_file_exists( $url ) {
+ $ch = curl_init( $url );
+ curl_setopt( $ch, CURLOPT_NOBODY, true );
+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
+ curl_exec( $ch );
+ $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ curl_close( $ch );
+ if ( $http_code == 200 ) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Download a remote resource
+ *
+ * @param string $url remote file url.
+ * @param string $folder folder where to download the remote resource.
+ * @param string $file file onto download the remote resource.
+ * @return void
+ */
+function download_remote_file( $url, $folder, $file ) {
+ if ( ! is_dir( $folder ) ) {
+ mkdir( $folder, 0755, true );
+ }
+
+ $fp = fopen( $file, 'wb' );
+
+ $ch = curl_init( $url );
+ curl_setopt( $ch, CURLOPT_FILE, $fp );
+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
+ curl_setopt( $ch, CURLOPT_HEADER, 0 );
+ curl_exec( $ch );
+ curl_close( $ch );
+
+ fflush( $fp ); // need to insert this line for proper output when tile is first requested.
+ fclose( $fp );
+}
+
+/**
+ * Output a local file
+ *
+ * @param string $file file of the local resource.
+ * @return void
+ */
+function output_local_file( $file ) {
+ print_local_file_headers( $file );
+ readfile( $file );
+}
+
+/**
+ * Proxy a remote resource
+ *
+ * @param string $url remote file url.
+ * @return void
+ */
+function proxy_remote_file( $url ) {
+ $fp = @fopen( $url, 'rb' );
+ if ( ! $fp ) {
+ print_404_page();
+ return;
+ }
+ print_remote_file_headers( $url );
+ fpassthru( $fp );
+}
+
+/**
+ * Get remote file headers, similar to: get_headers( $url )
+ *
+ * @param string $url remote file url.
+ * @return array
+ * @link https://stackoverflow.com/a/41135574
+ */
+function get_remote_file_headers( $url ) {
+ $headers = [];
+ $ch = curl_init();
+
+ curl_setopt( $ch, CURLOPT_URL, $url );
+ curl_setopt( $ch, CURLOPT_HEADER, true );
+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
+
+ // this function is called by curl for each header received.
+ curl_setopt(
+ $ch, CURLOPT_HEADERFUNCTION,
+ function( $curl, $header ) use ( &$headers ) {
+ $headers[] = $header;
+ return strlen( $header );
+ }
+ );
+ curl_exec( $ch );
+ curl_close( $ch );
+
+ return $headers;
+}
+
+/**
+ * Print the default apache 404 page
+ *
+ * @return void
+ */
+function print_404_page() {
+ header( $_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found' );
+ $request_uri = htmlspecialchars( $_SERVER['REQUEST_URI'] );
+ echo '';
+ echo "
404 Not FoundNot Found
The requested URL {$request_uri} was not found on this server.
";
+}
+
+/**
+ * Send to browser remote file headers
+ *
+ * @param string $url remote file url.
+ * @return void
+ */
+function print_remote_file_headers( $url ) {
+ $headers = get_remote_file_headers( $url );
+ print_file_headers( $headers );
+}
+
+/**
+ * Send to browser local file headers
+ *
+ * @param string $file local file.
+ * @return void
+ */
+function print_local_file_headers( $file ) {
+ global $ttl;
+ $exp_gmt = gmdate( 'D, d M Y H:i:s', time() + $ttl * 60 ) . ' GMT';
+ $mod_gmt = gmdate( 'D, d M Y H:i:s', filemtime( $file ) ) . ' GMT';
+ $max_age = $ttl * 60;
+ $headers = array(
+ 'Expires:' => $exp_gmt,
+ 'Last-Modified:' => $mod_gmt,
+ 'Cache-Control:' => 'public, max-age=' . $max_age, // for MSIE 5.
+ 'Content-Type:' => 'image/png',
+ );
+ print_file_headers( $headers );
+}
+
+/**
+ * Send to browser file headers filtering it with the previously array in config file
+ *
+ * @param array $file_headers eg: array( 'Content-Type:' => 'image/png' ) or array( 'Access-Control-Allow-Origin: *' ).
+ *
+ * @return void
+ */
+function print_file_headers( &$file_headers ) {
+ global $headers;
+ // send headers only if not previously defined in config file.
+ foreach ( $file_headers as $header => $value ) {
+ if ( is_string( $header ) ) {
+ header( $header . ' ' . $value );
+ } else {
+ header( $value );
+ }
+ }
+ // send all remaing default headers defined in config file.
+ foreach ( $headers as $header => $value ) {
+ header_remove( rtrim( $header, ':' ) );
+ header( $header . ' ' . $value );
+ }
+}