view tools/makegmaps.php @ 324:b0c2f11e60fa gmap2 tip

Canonize boolean values to true/false instead of mixing TRUE/FALSE in.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 12 Mar 2023 14:58:57 +0200
parents a157aa18ec75
children
line wrap: on
line source

#!/usr/bin/php
<?php
// Prevent non-cli execution
if (php_sapi_name() != "cli" || !empty($_SERVER["REMOTE_ADDR"]))
  die("You can only run this script as a commandline application.\n");

if (!extension_loaded("gd"))
  die("ERROR: The required GD extension to PHP was not found or enabled.\n");


$gmapsConfig = "config.php";


// Paths and files
$cfgDefaults = [
  "binConvert"      => [1, "convert", "Path of 'convert' binary from ImageMagick or GraphicsMagick."],
  "binMercurial"    => [1, "hg", "Path of 'hg' binary of Mercurial DVCS."],
  "binMake"         => [1, "make", "Path of 'make', preferably GNU make."],

  "pathMapUtils"    => [2, "maputils/", "Path for maputils directory tree."],
  "pathImageCache"  => [2, "cache/", "Image cache directory."],
  "pathGMap"        => [2, "../", "Path to map main directory and marker data files."],
  "pathTileData"    => [2, "../tiles/", "Path to map tiles directory structure."],

  "pathRawMaps"     => [3, false, "Path to the raw ASCII maps."],
  "pathLocFiles"    => [3, false, "Path to the location data LOC files."],
  "worldConfig"     => [3, false, "Path of the world configuration file 'world.inc.php' from MapUtils."],
];


$fontFile = "./lucon.ttf";


// Internal data and settings
$tileDim = 256;

$minZoom = 1;
$maxZoom = 10;

// Font sizes
$fontSize = [];
$fontSize[ 8] = 7;
$fontSize[16] = 13;
$fontSize[32] = 26;

$modes = [
  "xml"     => ["batclient.xml"     , 0, 0],
  "json"    => ["markers.json"      , 0, 0],
];

$mapPalette = [];
$mapPalette["!"]  = [ 204, 255, 255];
$mapPalette["%"]  = [   0, 170, 170];
$mapPalette["-"]  = [  51,  51,  51];
$mapPalette["|"]  = [  51,  51,  51];
$mapPalette["/"]  = [  51,  51,  51];
$mapPalette["+"]  = [  51,  51,  51];
$mapPalette["\\"] = [  51,  51,  51];
$mapPalette["="]  = [  72,  67,  57];
$mapPalette["@"]  = [ 255, 107,   0];
$mapPalette["F"]  = [   0, 136,   0];
$mapPalette["L"]  = [ 255,  80,   0];
$mapPalette["S"]  = [  68, 204, 204];
$mapPalette["^"]  = [ 113, 130, 146];
$mapPalette["c"]  = [  95,  86,  85];
$mapPalette["f"]  = [   0, 182,   0];
$mapPalette["i"]  = [ 255, 255, 255];
$mapPalette["l"]  = [ 100, 100, 255];
$mapPalette["s"]  = [ 157, 168,  10];
$mapPalette["v"]  = [  34, 221,  34];
$mapPalette["x"]  = [ 138, 131,  96];
$mapPalette["z"]  = [ 177, 164, 133];
$mapPalette["#"]  = [  79,  54,  69];
$mapPalette["."]  = [  85, 146,   0];
$mapPalette[","]  = [ 140,  87,  56];
$mapPalette["?"]  = [ 255, 255,   0];
$mapPalette["C"]  = [ 153, 153,   0];
$mapPalette["H"]  = [ 102,  63,   0];
$mapPalette["R"]  = [  51, 102, 255];
$mapPalette["V"]  = [ 255,  51,   0];
$mapPalette["b"]  = [ 207, 196, 165];
$mapPalette["d"]  = [ 238, 187,  34];
$mapPalette["h"]  = [ 153, 102,   0];
$mapPalette["j"]  = [  19, 150,  54];
$mapPalette["r"]  = [ 102, 153, 255];
$mapPalette["t"]  = [  97, 195, 162];
$mapPalette["w"]  = [ 119, 170, 255];
$mapPalette["y"]  = [ 167, 204,  20];
$mapPalette["~"]  = [  51,  51, 170];
$mapPalette["1"]  = [ 255, 102,  16];
$mapPalette[3]    = [   0,   0,   0];


//
// Helper functions
//
function stMakeDir($path)
{
  if (file_exists($path))
    return true;
  else
    return mkdir($path, 0755, true);
}


function stOutputToFile($sfilename, $sdata)
{
  if (file_put_contents($sfilename, $sdata, LOCK_EX) === false)
    die("Error writing to '".$sfilename."'.\n");
}


function stOutputToJSONFile($qfilename, $qdata)
{
  stOutputToFile($qfilename, json_encode($qdata));
}


// Calculate worldmap coordinates from continent coordinates
function stGetWorldCoords($cname, $xp, $yp, &$xc, &$yc)
{
  global $worldMap, $continentList;

  if (!isset($continentList[$cname]))
    return false;

  $xc = $worldMap["ox"] + $continentList[$cname][CTI_XOFFS] + $xp - 1;
  $yc = $worldMap["oy"] + $continentList[$cname][CTI_YOFFS] + $yp - 1;
  return true;
}


// Get worldmap coordinates for a given tradelane waypoint
function stGetWaypointCoords($waypoint, &$xc, &$yc)
{
  global $tradelanePoints;

  if (!isset($tradelanePoints[$waypoint]))
    return false;

  return stGetWorldCoords($tradelanePoints[$waypoint][0],
    $tradelanePoints[$waypoint][1], $tradelanePoints[$waypoint][2], $xc, $yc);
}


function stYesNoPrompt($msg, $default = false)
{
  echo $msg." [".($default ? "Y/n" : "y/N")."]? ";
  $sprompt = strtolower(trim(fgets(STDIN)));

  if ($default)
    return ($sprompt == "n");
  else
    return ($sprompt == "y");
}


function stInputPrompt($msg, $default = false, $validate = null)
{
  $valid = false;
  while (!$valid)
  {
    echo $msg."\n".($default !== false ? "[".$default."]" : "")."> ";
    $sprompt = trim(fgets(STDIN));

    if ($sprompt == "")
      $sprompt = ($default !== false ? $default : "");

    $valid =  !is_callable($validate) || call_user_func($validate, $sprompt);
  }
  return $sprompt;
}


function stValidateNotEmpty($val)
{
  if ($val == "")
  {
    echo "The value can't be empty.\n";
    return false;
  }
  else
    return true;
}


function stValidateURLPrefix($sprefix)
{
  if (substr($sprefix, 0, 7) != "http://" &&
      substr($sprefix, 0, 8) != "https://")
  {
    echo "URL must start with http:// or https://\n";
    return false;
  }
  else
  if (substr($sprefix, -1) != "/")
  {
    echo "URL must end with /\n";
    return false;
  }
  else
  if (preg_match("/^https?:\/\/[a-zA-Z0-9]+[a-zA-Z0-9\/\.\:\-]*?\//", $sprefix) === false)
  {
    echo "Malformed URL (or atleast this silly regexp does not accept it.\n";
    return false;
  }
  else
    return true;
}


function stMakeHtAccessFile($urlprefix, $sprefix, $spath)
{
  global $firstRun;
  $sfile = $sprefix.$spath.".htaccess";
  if (($firstRun || !file_exists($sfile)) && file_exists($sprefix.$spath."sea.png"))
  {
    echo "Creating ".$sfile."\n";
    stOutputToFile($sfile, "ErrorDocument 404 ".$urlprefix.$spath."sea.png\n");
  }
}


function stQueryConfigItems($level)
{
  global $cfgDefaults, $cfg;
  foreach ($cfgDefaults as $citem => $cdata)
  {
    if ($cdata[0] == $level)
    {
      $def = ($cdata[1] !== false) ? $cdata[1] : $cfg[$citem];

      $sdone = false;
      while (!$sdone)
      {
        $tmp = $cfg[$citem] = stInputPrompt($cdata[2], $def, "stValidateNotEmpty");

        switch ($cdata[0])
        {
          case 1:
            exec("which ".escapeshellarg($tmp), $tmpOut, $res);
            if ($res != 0)
            {
              echo "ERROR: Could not find '".$tmp."'. Perhaps it is not in path, or path is wrong.\n";
            }
            else
              $sdone = true;
            break;

          default:
            $sdone = true;
        }
      }
    }
  }
}


//
// Check for first run
//
echo
  "===========================================================\n".
  "GMaps TNG bootstrap and update script by Ggr & Jeskko\n".
  "===========================================================\n";

if (file_exists($gmapsConfig))
{
  $firstRun = false;
  include $gmapsConfig;
}
else
{
  $firstRun = true;
  echo
    "It seems you are running this for the first time ..\n".
    "You will be asked some information this time, which will be\n".
    "and saved for later use in file '".$gmapsConfig."'\n\n";

  $sdone = false;
  while (!$sdone)
  {
    $cfg["pageBaseURL"] = stInputPrompt(
      "Enter base URL for the map page. For example: http://foobar.com/map/\n",
      false, "stValidateURLPrefix");
    $sdone = stYesNoPrompt("The page base URL to be used is \"".$cfg["pageBaseURL"]."\", e.g.\n".
      "index.php would be: \"".$cfg["pageBaseURL"]."index.php\"\n".
      "Is this correct?");
    echo "\n";
  }

  $sdone = false;
  while (!$sdone)
  {
    $cfg["urlTilePrefix"] = stInputPrompt(
      "Enter URL prefix for tiles. For example: http://foobar.com/map/tiles/\n",
      $cfg["pageBaseURL"]."tiles/", "stValidateURLPrefix");
    $sdone = stYesNoPrompt("The URL prefix to be used is \"".$cfg["urlTilePrefix"]."\", e.g.\n".
      "htaccess to be created would be: \"".$cfg["urlTilePrefix"]."sea.png\"\n".
      "Is this correct?");
    echo "\n";
  }

  $cfg["gmapsKey"] = stInputPrompt(
    "Enter your Google Maps API key (or leave empty, and edit later)\n",
    false, false);

  echo
    "\nNext up are some files and paths. All of them can be safely\n".
    "left to their default values (e.g. just press <enter>) unless\n".
    "you know you want something set differently.\n\n";

  stQueryConfigItems(1);
  stQueryConfigItems(2);

  $cfg["pathRawMaps"] = $cfg["pathMapUtils"]."world/";
  $cfg["pathLocFiles"] = $cfg["pathMapUtils"]."world/";
  $cfg["worldConfig"] = $cfg["pathMapUtils"]."www/world.inc.php";

  stQueryConfigItems(3);


  stOutputToFile($gmapsConfig, "<?php\n\$cfg = ".var_export($cfg, true)."\n?>");

  stOutputToFile($cfg["pathGMap"]."config.inc.php",
  "<?php\n".
  "\$pageBaseURL = \"".$cfg["pageBaseURL"]."\";\n".
  "\$gmapsKey = \"".$cfg["gmapsKey"]."\";\n".
  "\$gmapsVersion = \"3\";\n".
  "?>\n");
}


//
// Set rest of the paths etc
//
$pathTileData = $cfg["pathGMap"]."tiles/";
$worldJS = $cfg["pathGMap"]."world.js";
$tradelaneOut = $cfg["pathGMap"]."tradelane.json";
$tradelaneOverlay = $cfg["pathGMap"]."trlines.json";
$tgtMkLoc = "bin/mkloc";
$binMkLoc = $cfg["pathMapUtils"].$tgtMkLoc;
$rawSuffix = ".new";
$rawAltSuffix = ".map";

/*
foreach (["." => 0600, $cfg["pathMapUtils"] => 0600] as $spath => $sperm)
{
  if (chmod($spath, $sperm) === false)
    echo "Could not set permissions for '".$spath."'.\n");
}
*/


//
// Create htaccess files
//
stMakeHtAccessFile($cfg["urlTilePrefix"], $cfg["pathTileData"], "");
for ($i = $minZoom; $i <= $maxZoom; $i++)
  stMakeHtAccessFile($cfg["urlTilePrefix"], $cfg["pathTileData"], $i."/");


//
// Build maputils and fetch latest map data
//
if (!file_exists($cfg["pathMapUtils"]))
{
  // If maputils does not exist, clone the repository
  $tmp = $cfg["binMercurial"]." clone https://tnsp.org/hg/batmud/maputils/ ".escapeshellarg($cfg["pathMapUtils"]);
  echo "* $tmp\n";
  passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");

  // Clone th-libs
  $tmp = $cfg["binMercurial"]." clone https://tnsp.org/hg/th-libs/ ".escapeshellarg($cfg["pathMapUtils"]."th-libs/");
  echo "* $tmp\n";
  passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");
}
else
{
  $tmp = "cd ".escapeshellarg($cfg["pathMapUtils"])." && ".$cfg["binMercurial"]." pull && ".$cfg["binMercurial"]." update";
  echo "* $tmp\n";
  passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");

  $tmp = "cd ".escapeshellarg($cfg["pathMapUtils"]."th-libs/")." && ".$cfg["binMercurial"]." pull && ".$cfg["binMercurial"]." update";
  echo "* $tmp\n";
  passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");
}

$tmp = "cd ".escapeshellarg($cfg["pathMapUtils"])." && ".$cfg["binMake"]." ".escapeshellarg($tgtMkLoc);
echo "* $tmp\n";
passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");


if (!file_exists($binMkLoc))
  die($binMkLoc." not found. Maputils package not built, or some other error occured.\n");

$tmp = "cd ".escapeshellarg($cfg["pathMapUtils"])." && ".$cfg["binMake"];
passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");

$tmp = "cd ".escapeshellarg($cfg["pathRawMaps"])." && ".$cfg["binMake"]." fetch 2> /dev/null";
passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");


//
// Include continent and tradelane configuration
//
if (!file_exists($cfg["worldConfig"]))
  die("Required continent/tradelane configuration file '".$cfg["worldConfig"]."' not found.\n");

require $cfg["worldConfig"];


//
// Generate continents JavasScript data file
///
echo "* Generating $worldJS ...\n";
$str  = "var pmapWorld = {";
foreach ($worldMap as $wkey => $wval)
  $str .= "\"".$wkey."\": ".$wval.", ";
$str .= "};\n";

$str .= "var pmapContinents =\n[\n";
foreach ($continentList as $cname => $cdata)
if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
{
  $str .= sprintf("  [%-15s, %5d, %5d, %5d, %5d, %5d, %5d],\n",
  "\"".$cdata[CTI_NAME]."\"",
  $cdata[CTI_XOFFS],
  $cdata[CTI_YOFFS],
  $cdata[CTI_XOFFS] + $cdata[CTI_WIDTH] - 1,
  $cdata[CTI_YOFFS] + $cdata[CTI_HEIGHT] - 1,
  $cdata[CTI_WIDTH], $cdata[CTI_HEIGHT]);
}
$str .= "];\n";

stOutputToFile($worldJS, $str);



//
// Generate marker files from LOC data
///
echo "Converting location data from LOC files to GMaps markers...\n";

foreach ($modes as $mode => $mdata)
{
  $tmp = escapeshellcmd($binMkLoc)." -v -o ".escapeshellarg($cfg["pathGMap"].$mdata[0])." -G ".$mode." ";

  foreach ($continentList as $cname => $cdata)
  {
    if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
    {
      // has a map
      $tmp .=
      "-l ".escapeshellarg($cfg["pathLocFiles"].$cname.".loc")." ".
      "-c ".escapeshellarg($cdata[CTI_NAME])." ".
      "-x ".escapeshellarg($worldMap["ox"] + $cdata[CTI_XOFFS] + $mdata[1])." ".
      "-y ".escapeshellarg($worldMap["oy"] + $cdata[CTI_YOFFS] + $mdata[2])." ";
    }
  }

  passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");
}


//
// Export tradelane waypoint data
//
if (!isset($tradelanePoints))
  die("PHP array \$tradelanePoints not set, '".$cfg["worldConfig"]."' is old or incompatible.\n");

echo "\nCreating tradelane waypoint data '".$tradelaneOut."' ...\n";

$qdata = [];

foreach ($tradelanePoints as $tname => $tlane)
{
  $html = "<b>TRADELANE WPT</b><br>".htmlentities($tname);

  if (!stGetWorldCoords($tlane[0], $tlane[1], $tlane[2], $xc, $yc))
    die("Invalid tradelane waypoint '".$tname."', continent '".$tlane[0]."' not defined.\n");

  $qdata[] = [
    "x" => $xc,
    "y" => $yc,
    "name" => $tname,
    "html" => $html,
    "continent" => "",
    "type" => "tradelane",
    "flags" => 0,
  ];
}

stOutputToJSONFile($tradelaneOut, $qdata);


//
// Export tradelane polyline data
//
echo "\nCreating tradelane polyline data '".$tradelaneOverlay."' ...\n";
if (!isset($tradelaneDefs))
  die("PHP array \$tradelaneDefs not set, '".$cfg["worldConfig"]."' is old or incompatible.\n");

$qdata = [];
foreach ($tradelaneDefs as $index => $points)
{
  $qline = [];

  foreach ($points as $point)
  {
    if (!stGetWaypointCoords($point, $xc, $yc))
      die("Invalid tradelane definition #$index: waypoint '".$point."' not defined.\n");

    $qline[] = [
      "x" => $xc,
      "y" => $yc
    ];
  }

  $qdata[] = $qline;
}


stOutputToJSONFile($tradelaneOverlay, $qdata);


//
// Generate PNG maps
//
function makeMap($inFilename, $outFilename, $zlevel, $cdata)
{
  global $mapPalette, $fontFile, $fontSize;

  // Try to open input file
  if (($file = @fopen($inFilename, "r")) === false)
  {
    echo "Could not open input '".$inFilename."'\n";
    return false;
  }

  // Derp
  $zoom = pow(2, $zlevel - 1);
  $width = $cdata[CTI_WIDTH];
  $height = $cdata[CTI_HEIGHT];

  // Create image and assign colors
  if (($im = imagecreate($width*$zoom, $height*$zoom)) === false)
  {
    echo "Could not allocate image data for '".$inFilename."'\n";
    return false;
  }

  $black = imagecolorallocate($im, 0, 0, 0);
  foreach ($mapPalette as $id => $val)
    $colors[$id] = imagecolorallocate($im, $val[0], $val[1], $val[2]);

  imagefilledrectangle($im, 0, 0, $width*$zoom, $height*$zoom, $black);

  // Read input raw
  $y = 0;
  while ($y < $height && ($data = fgets($file, 4096)) !== false)
  {
    for ($x = 0; $x < $width; $x++)
    {
      if ($zoom == 1)
      {
        imagesetpixel($im, $x, $y, $colors[$data[$x]]);
      }
      else
      if ($zoom < 6)
      {
        imagefilledrectangle($im, $x*$zoom, $y*$zoom, ($x+1)*$zoom-1, ($y+1)*$zoom-1, $colors[$data[$x]]);
      }
      else
      {
        imagettftext($im,
          $fontSize[$zoom], 0,
          $x*$zoom + $fontSize[$zoom]/4,
          $y*$zoom + $fontSize[$zoom],
          $colors[$data[$x]],
          $fontFile,
          $data[$x]);
      }
    }
    $y++;
    echo ".";
  }

  if (imagepng($im, $outFilename) === false)
  {
    echo "Error creating '".$outFilename."'\n";
    imagedestroy($im);
    return false;
  }

  imagedestroy($im);
  return true;
}

if (!stMakeDir($cfg["pathImageCache"]))
{
  die("Failed to create cache directory '".$cfg["pathImageCache"]."'!\n");
}

echo "Generating basic map data...\n";
foreach ($continentList as $cname => $cdata)
if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
{
  $inFilename = $cfg["pathRawMaps"].$cname.$rawSuffix;
  if (!file_exists($inFilename))
  {
    $inFilename = $cfg["pathRawMaps"].$cname.$rawAltSuffix;
    if (!file_exists($inFilename))
      die("Required file '".$cfg["pathRawMaps"].$cname."(".$rawSuffix."|".$rawAltSuffix.")' does not exist.\n");
  }
  $inMtime = filemtime($inFilename);

  for ($zoom = 1; $zoom <= 5; $zoom++)
  {
    $outFilename = $cfg["pathImageCache"].$cname."_".($zoom + 4).".png";
    $outMtime = file_exists($outFilename) ? filemtime($outFilename) : -1;
    echo "- ".$cname." (".$cdata[CTI_NAME]."): ";
    if ($inMtime > $outMtime)
    {
      $res = makeMap($inFilename, $outFilename, $zoom, $cdata);
      echo ($res ? "OK" : "FAIL")."\n";
    }
    else
      echo "SKIPPED\n";
  }
}


/*
 * Generate small versions
 */
echo "\nGenerating scaled small map data...\n";
$mapScales = ["50", "25", "12.5", "6.25", "3.125"];
foreach ($continentList as $cname => &$cdata)
if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
{
  $n = count($mapScales);
  $inFilename = $cfg["pathImageCache"].$cname."_".$n.".png";
  if (!file_exists($inFilename))
  {
    die("Required file '".$inFilename."' does not exist.\n");
  }
  $inMtime = filemtime($inFilename);

  foreach ($mapScales as $scale)
  {
    $n--;
    $outFilename = $cfg["pathImageCache"].$cname."_".$n.".png";
    $outMtime = file_exists($outFilename) ? filemtime($outFilename) : -1;

    echo "* ".$inFilename." -> ".$outFilename.": ";
    if ($inMtime > $outMtime)
    {
      $tmp = escapeshellcmd($cfg["binConvert"])." ".
        escapeshellarg($inFilename)." ".
        "-scale ".escapeshellarg($scale."%")." ".
        //"-type Palette ".
        escapeshellarg($outFilename);

      passthru($tmp) == 0 or die("Error executing: ".$tmp."\n");
      echo "OK\n";
    }
    else
      echo "SKIPPED\n";
  }
}


/*
 * Build tiles
 */
function createTile($scale, $zoom, $x, $y, $mapData, $mapMtime)
{
  global $continentList, $worldMap, $cfg, $tileDim;

  $outFilename = $cfg["pathTileData"].$zoom."/".$y."/".$x.".png";
  if (file_exists($outFilename) && filemtime($outFilename) >= $mapMtime)
  {
    echo "!";
    return;
  }

  $drawn = false;
  $im = false;

  foreach ($continentList as $cname => &$cdata)
  {
    if (!$cdata[CTI_HAS_MAP] || !$cdata[CTI_REG_CONT])
      continue;

    $cx = $cdata[CTI_XOFFS] + $worldMap["ox"];
    $cy = $cdata[CTI_YOFFS] + $worldMap["oy"];
    $cw = $cdata[CTI_WIDTH];
    $ch = $cdata[CTI_HEIGHT];

    $tx = -($cx*$scale - $x*$tileDim);
    $ty = -($cy*$scale - $y*$tileDim);

    if (($cx + $cw)*$scale > $x*$tileDim &&
        ($cy + $ch)*$scale > $y*$tileDim &&
        ($cx * $scale)     < ($x+1)*$tileDim &&
        ($cy * $scale)     < ($y+1)*$tileDim)
    {
      if (!$drawn)
      {
        if ($zoom < 9)
        {
          $im = imagecreate($tileDim, $tileDim);
          if ($im === false)
            die("\nCould not create GD image resource open dim=".$tileDim.
            " for zoom=".$zoom.", continent=".$cname."\n");

          $sea = imagecolorallocate($im, 51, 51, 170);
          imagefilledrectangle($im, 0, 0, $tileDim, $tileDim, $sea);
        }
        else
        {
          $inFilename = $cfg["pathTileData"].$zoom."/sea.png";
          $im = imagecreatefrompng($inFilename);
          if ($im === false)
            die("\nCould not open '".$inFilename."'.\n");
        }
      }

      $dx = $tileDim;
      $dy = $tileDim;
      $xx = 0;
      $yy = 0;

      if ($tx < 0)
      {
        $xx -= $tx;
        $dx += $tx+1;
        $tx  = 0;
      }

      if ($ty < 0)
      {
        $yy -= $ty;
        $dy += $ty+1;
        $ty  = 0;
      }

      if ($dx > $cw*$scale-$tx)
        $dx = $cw*$scale - $tx;

      if ($dy > $ch*$scale-$ty)
        $dy = $ch*$scale - $ty;

      if ($im !== false)
      {
        imagecopy($im, $mapData[$cname], $xx, $yy, $tx, $ty, $dx, $dy);
        $drawn = true;
      }
    }
  }

  if ($drawn)
  {
    stMakeDir($cfg["pathTileData"].$zoom."/".$y, 0755);
    imagepng($im, $outFilename);
    imagedestroy($im);
  }
/*
   else {
    if (file_exists($outFilename))
      unlink($outFilename);

    symlink($cfg["pathTileData"]."sea.png", $outFilename);
    echo "+";
  }
*/
}


echo "\nBuilding tiles data for all zoom levels ...\n";

$mapData = [];

for ($zoom = $minZoom; $zoom <= $maxZoom; $zoom++)
{
  $zoom2 = $zoom - 1;
  $scale = pow(2, $zoom2 - 5);

  stMakeDir($cfg["pathTileData"].$zoom);

  echo "\nZoom level $zoom: ";

  $mx = ceil(($worldMap["w"] * $scale) / $tileDim);
  $my = ceil(($worldMap["h"] * $scale) / $tileDim);
  $mw = ceil( $worldMap["w"] * $scale);
  $mh = ceil( $worldMap["h"] * $scale);

  $mapMtime = -1;
  foreach ($continentList as $cname => &$cdata)
  if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
  {
    $mapFile = $cfg["pathImageCache"].$cname."_".$zoom2.".png";
    $mapData[$cname] = imagecreatefrompng($mapFile);
    if ($mapData[$cname] === false)
      die("Not an GD image resource ".$mapData[$cname]." in '".$mapFile."'\n");

    $tmp = filemtime($mapFile);
    if ($tmp > $mapMtime)
      $mapMtime = $tmp;
  }

  for ($y = 0; $y < $mx; $y++)
  {
    for ($x = 0; $x < $my; $x++)
      createTile($scale, $zoom, $x, $y, $mapData, $mapMtime);
    echo ".";
  }
  echo "\n";

  // Free image data for each continent
  foreach ($continentList as $cname => &$cdata)
  {
    if ($cdata[CTI_HAS_MAP] && $cdata[CTI_REG_CONT])
      imagedestroy($mapData[$cname]);
  }
}

?>