90, 'maxLat' => -90, 'minLng' => 180, 'maxLng' => -180 ); // If any markers are specified, choose a default lat/lng as the center of all the markers $markers = array(); if($markersTemp=k($params,'marker')) { if(!is_array($markersTemp)) $markersTemp = array($markersTemp); // If no latitude is set, use the center of all the markers foreach($markersTemp as $i=>$m) { if(preg_match_all('/(?P[a-z]+):(?P[^;]+)/', $m, $matches)) { $properties = array(); foreach($matches['k'] as $j=>$key) { $properties[$key] = $matches['v'][$j]; } // Skip invalid marker definitions, show error in a header if(array_key_exists('icon', $properties) && ( (array_key_exists('lat', $properties) && array_key_exists('lng', $properties)) || array_key_exists('location', $properties) ) ) { // Geocode the provided location and return lat/lng if(array_key_exists('location', $properties)) { $result = Geocoder::geocode($properties['location']); if(!$result) { #header('X-Marker-' . ($i+1) . ': error geocoding location "' . $properties['location'] . '"'); continue; } $properties['lat'] = $result->latitude; $properties['lng'] = $result->longitude; } if(preg_match('/https?:\/\/(.+)/', $properties['icon'], $match)) { $properties['iconImg'] = false; // Only allow external referenced icons from authenticated requests if($is_authenticated) { // Looks like an external image, attempt to download it $ch = curl_init($properties['icon']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $img = curl_exec($ch); $properties['iconImg'] = @imagecreatefromstring($img); } } else { $properties['iconImg'] = imagecreatefrompng($assetPath . '/' . $properties['icon'] . '.png'); } if($properties['iconImg']) { $markers[] = $properties; } if($properties['lat'] < $bounds['minLat']) $bounds['minLat'] = $properties['lat']; if($properties['lat'] > $bounds['maxLat']) $bounds['maxLat'] = $properties['lat']; if($properties['lng'] < $bounds['minLng']) $bounds['minLng'] = $properties['lng']; if($properties['lng'] > $bounds['maxLng']) $bounds['maxLng'] = $properties['lng']; } else { #header('X-Marker-' . ($i+1) . ': missing icon, or lat/lng/location parameters'); } } } } $paths = array(); if($pathsTemp=k($params,'path')) { if(!is_array($pathsTemp)) $pathsTemp = array($pathsTemp); foreach($pathsTemp as $i=>$path) { $properties = array(); if(preg_match_all('/(?P[a-z]+):(?P[^;]+)/', $path, $matches)) { foreach($matches['k'] as $j=>$key) { $properties[$key] = $matches['v'][$j]; } } // Set default color and weight if none specified if(!array_key_exists('color', $properties)) $properties['color'] = '333333'; if(!array_key_exists('weight', $properties)) $properties['weight'] = 3; // Now parse the points into an array if(preg_match_all('/(?P\[[0-9\.-]+,[0-9\.-]+\])/', $path, $matches)) { $properties['path'] = json_decode('[' . implode(',', $matches['point']) . ']'); // Adjust the bounds to fit the path foreach($properties['path'] as $point) { if($point[1] < $bounds['minLat']) $bounds['minLat'] = $point[1]; if($point[1] > $bounds['maxLat']) $bounds['maxLat'] = $point[1]; if($point[0] < $bounds['minLng']) $bounds['minLng'] = $point[0]; if($point[0] > $bounds['maxLng']) $bounds['maxLng'] = $point[0]; } } if(array_key_exists('path', $properties)) $paths[] = $properties; } } $defaultLatitude = $bounds['minLat'] + (($bounds['maxLat'] - $bounds['minLat']) / 2); $defaultLongitude = $bounds['minLng'] + (($bounds['maxLng'] - $bounds['minLng']) / 2); if(k($params,'latitude') !== false) { $latitude = k($params,'latitude'); $longitude = k($params,'longitude'); } elseif(k($params,'location') !== false) { $result = ArcGISGeocoder::geocode(k($params,'location')); if(!$result->success) { $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; $longitude = $defaultLongitude; } $width = k($params, 'width', 300); $height = k($params, 'height', 300); // If no zoom is specified, choose a zoom level that will fit all the markers and the path if(k($params,'zoom')) { $zoom = k($params,'zoom'); } else { // start at max zoom level (20) $fitZoom = 21; $doesNotFit = true; while($fitZoom > 1 && $doesNotFit) { $fitZoom--; $center = webmercator\latLngToPixels($latitude, $longitude, $fitZoom); $leftEdge = $center['x'] - $width/2; $topEdge = $center['y'] - $height/2; // check if the bounding rectangle fits within width/height $sw = webmercator\latLngToPixels($bounds['minLat'], $bounds['minLng'], $fitZoom); $ne = webmercator\latLngToPixels($bounds['maxLat'], $bounds['maxLng'], $fitZoom); // leave some padding around the objects $fitHeight = abs($ne['y'] - $sw['y']) + (0.1 * $height); $fitWidth = abs($ne['x'] - $sw['x']) + (0.1 * $width); if($fitHeight <= $height && $fitWidth <= $width) { $doesNotFit = false; } } $zoom = $fitZoom; } if(k($params,'maxzoom') && k($params,'maxzoom') < $zoom) { $zoom = k($params,'maxzoom'); } $minZoom = 2; if($zoom < $minZoom) $zoom = $minZoom; $tileServices = array( 'streets' => array( 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{Z}/{Y}/{X}' ), 'satellite' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}' ), 'hybrid' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}', 'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{Z}/{Y}/{X}' ), 'topo' => array( 'http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{Z}/{Y}/{X}' ), 'gray' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{Z}/{Y}/{X}', 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{Z}/{Y}/{X}' ), 'gray-background' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{Z}/{Y}/{X}', ), 'oceans' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{Z}/{Y}/{X}' ), 'national-geographic' => array( 'https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{Z}/{Y}/{X}' ), 'osm' => array( 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png' ), 'stamen-toner' => array( 'http://tile.stamen.com/toner/{Z}/{X}/{Y}.png' ), 'stamen-toner-background' => array( 'http://tile.stamen.com/toner-background/{Z}/{X}/{Y}.png' ), 'stamen-toner-lite' => array( 'http://tile.stamen.com/toner-lite/{Z}/{X}/{Y}.png' ), 'stamen-terrain' => array( 'http://tile.stamen.com/terrain/{Z}/{X}/{Y}.png' ), 'stamen-terrain-background' => array( 'http://tile.stamen.com/terrain-background/{Z}/{X}/{Y}.png' ), 'stamen-watercolor' => array( 'http://tile.stamen.com/watercolor/{Z}/{X}/{Y}.png' ), ); if(k($params,'basemap') && k($tileServices, k($params,'basemap'))) { $tileURL = $tileServices[k($params,'basemap')][0]; if(array_key_exists(1, $tileServices[k($params,'basemap')])) $overlayURL = $tileServices[k($params,'basemap')][1]; else $overlayURL = 0; } elseif(k($params, 'basemap') == 'custom' && $is_authenticated) { $tileURL = $params['tileurl']; $overlayURL = false; } else { $tileURL = $tileServices['gray'][0]; $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); } $im = imagecreatetruecolor($width, $height); // Find the pixel coordinate of the center of the map $center = webmercator\latLngToPixels($latitude, $longitude, $zoom); $leftEdge = $center['x'] - $width/2; $topEdge = $center['y'] - $height/2; $tilePos = webmercator\pixelsToTile($center['x'], $center['y']); // print_r($tilePos); // echo '
'; $pos = webmercator\positionInTile($center['x'], $center['y']); // print_r($pos); // echo '
'; // For the given number of pixels, determine how many tiles are needed in each direction $neTile = webmercator\pixelsToTile($center['x'] + $width/2, $center['y'] + $height/2); // print_r($neTile); // echo '
'; $swTile = webmercator\pixelsToTile($center['x'] - $width/2, $center['y'] - $height/2); // print_r($swTile); // echo '
'; // Now download all the tiles $tiles = array(); $overlays = array(); $chs = array(); $mh = curl_multi_init(); $numTiles = 0; $urls = array(); for($x = $swTile['x']; $x <= $neTile['x']; $x++) { $x = (int)$x; if(!array_key_exists($x, $tiles)) { $tiles[$x] = array(); $overlays[$x] = array(); $chs[$x] = array(); $ochs[$x] = array(); } for($y = $swTile['y']; $y <= $neTile['y']; $y++) { $y = (int)$y; $url = urlForTile($x, $y, $zoom, $tileURL); $urls[] = $url; $tiles[$x][$y] = false; $chs[$x][$y] = curl_init($url); curl_setopt($chs[$x][$y], CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($chs[$x][$y], CURLOPT_FOLLOWLOCATION, true); curl_multi_add_handle($mh, $chs[$x][$y]); if($overlayURL) { $url = urlForTile($x, $y, $zoom, $overlayURL); $overlays[$x][$y] = false; $ochs[$x][$y] = curl_init($url); curl_setopt($ochs[$x][$y], CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ochs[$x][$y], CURLOPT_FOLLOWLOCATION, TRUE); curl_multi_add_handle($mh, $ochs[$x][$y]); } $numTiles++; } } $running = null; // Execute the handles. Blocks until all are finished. do { $mrc = curl_multi_exec($mh, $running); } while($running > 0); // In case any of the tiles fail, they will be grey instead of throwing an error $blank = imagecreatetruecolor(256, 256); $grey = imagecolorallocate($im, 224, 224, 224); imagefill($blank, 0,0, $grey); foreach($chs as $x=>$yTiles) { foreach($yTiles as $y=>$ch) { $content = curl_multi_getcontent($ch); $tiles[$x][$y] = @imagecreatefromstring($content); if(!$tiles[$x][$y]) $tiles[$x][$y] = $blank; } } if($overlayURL) { foreach($ochs as $x=>$yTiles) { foreach($yTiles as $y=>$ch) { $content = curl_multi_getcontent($ch); $overlays[$x][$y] = @imagecreatefromstring($content); if(!$overlays[$x][$y]) $overlays[$x][$y] = $blank; } } } // Assemble all the tiles into a new image positioned as appropriate foreach($tiles as $x=>$yTiles) { foreach($yTiles as $y=>$tile) { $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); imagecopy($im, $tile, $ox,$oy, 0,0, imagesx($tile),imagesy($tile)); } } if($overlayURL) { foreach($overlays as $x=>$yTiles) { foreach($yTiles as $y=>$tile) { $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); imagecopy($im, $tile, $ox,$oy, 0,0, imagesx($tile),imagesy($tile)); } } } if(count($paths)) { // Draw the path with ImageMagick because GD sucks as anti-aliased lines $mg = new Imagick(); $mg->newImage($width, $height, new ImagickPixel('none')); $draw = new ImagickDraw(); $colors = array(); foreach($paths as $path) { $draw->setStrokeColor(new ImagickPixel('#'.$path['color'])); $draw->setStrokeWidth($path['weight']); $draw->setFillOpacity(0); $draw->setStrokeLineCap(Imagick::LINECAP_ROUND); $draw->setStrokeLineJoin(Imagick::LINEJOIN_ROUND); $previous = false; foreach($path['path'] as $point) { if($previous) { $from = webmercator\latLngToPixels($previous[1], $previous[0], $zoom); $to = webmercator\latLngToPixels($point[1], $point[0], $zoom); if(k($params, 'bezier')) { $x_dist = abs($from['x'] - $to['x']); $y_dist = abs($from['y'] - $to['y']); // If the X distance is longer than Y distance, draw from left to right if($x_dist > $y_dist) { // Draw from left to right if($from['x'] > $to['x']) { $tmpFrom = $from; $tmpTo = $to; $from = $tmpTo; $to = $tmpFrom; unset($tmp); } } else { // Draw from top to bottom if($from['y'] > $to['y']) { $tmpFrom = $from; $tmpTo = $to; $from = $tmpTo; $to = $tmpFrom; unset($tmp); } } $angle = 1 * k($params, 'bezier'); // Midpoint between the two ends $M = [ 'x' => ($from['x'] + $to['x']) / 2, 'y' => ($from['y'] + $to['y']) / 2 ]; // Derived from http://math.stackexchange.com/a/383648 and http://www.wolframalpha.com/input/?i=triangle+%5B1,1%5D+%5B5,2%5D+%5B1-1%2Fsqrt(3),1%2B4%2Fsqrt(3)%5D // See for details $A = $from; $B = $to; $P = [ 'x' => ($M['x']) - (($A['y']-$M['y']) * tan(deg2rad($angle))), 'y' => ($M['y']) + (($A['x']-$M['x']) * tan(deg2rad($angle))) ]; $draw->pathStart(); $draw->pathMoveToAbsolute($A['x']-$leftEdge,$A['y']-$topEdge); $draw->pathCurveToQuadraticBezierAbsolute( $P['x']-$leftEdge, $P['y']-$topEdge, $B['x']-$leftEdge, $B['y']-$topEdge ); $draw->pathFinish(); } else { $draw->line($from['x']-$leftEdge,$from['y']-$topEdge, $to['x']-$leftEdge,$to['y']-$topEdge); } } $previous = $point; } } $mg->drawImage($draw); $mg->setImageFormat("png"); $pathImg = imagecreatefromstring($mg); imagecopy($im, $pathImg, 0,0, 0,0, $width,$height); } // Add markers foreach($markers as $marker) { // Icons that have 'dot' in the name do not have a shadow and center vertically and horizontally $shadow = !preg_match('/dot/', $marker['icon']); if($width < 120 || $height < 120) { $shrinkFactor = 1.5; } else { $shrinkFactor = 1; } // Icons with a shadow are centered at the bottom middle pixel. // Icons with no shadow are centered in the center pixel. $px = webmercator\latLngToPixels($marker['lat'], $marker['lng'], $zoom); $pos = array( 'x' => $px['x'] - $leftEdge, 'y' => $px['y'] - $topEdge ); if($shrinkFactor > 1) { $markerImg = imagecreatetruecolor(round(imagesx($marker['iconImg'])/$shrinkFactor), round(imagesy($marker['iconImg'])/$shrinkFactor)); imagealphablending($markerImg, true); $color = imagecolorallocatealpha($markerImg, 0, 0, 0, 127); imagefill($markerImg, 0,0, $color); imagecopyresampled($markerImg, $marker['iconImg'], 0,0, 0,0, imagesx($markerImg),imagesy($markerImg), imagesx($marker['iconImg']),imagesy($marker['iconImg'])); } else { $markerImg = $marker['iconImg']; } if($shadow) { $iconPos = array( 'x' => $pos['x'] - round(imagesx($markerImg)/2), 'y' => $pos['y'] - imagesy($markerImg) ); } else { $iconPos = array( 'x' => $pos['x'] - round(imagesx($markerImg)/2), 'y' => $pos['y'] - round(imagesy($markerImg)/2) ); } imagecopy($im, $markerImg, $iconPos['x'],$iconPos['y'], 0,0, imagesx($markerImg),imagesy($markerImg)); } if(k($params,'attribution') == 'mapbox') { $logo = imagecreatefrompng($assetPath . '/mapbox-attribution.png'); imagecopy($im, $logo, $width-imagesx($logo), $height-imagesy($logo), 0,0, imagesx($logo),imagesy($logo)); } elseif(k($params,'attribution') == 'mapbox-small') { $logo = imagecreatefrompng($assetPath . '/mapbox-attribution.png'); $shrinkFactor = 2; imagecopyresampled($im, $logo, $width-round(imagesx($logo)/$shrinkFactor), $height-round(imagesy($logo)/$shrinkFactor), 0,0, round(imagesx($logo)/$shrinkFactor),round(imagesy($logo)/$shrinkFactor), imagesx($logo),imagesy($logo)); } elseif(k($params,'attribution') != 'none') { $logo = imagecreatefrompng($assetPath . '/powered-by-esri.png'); // Shrink the logo if the image is small if($width > 120) { if($width < 220 || k($params, 'attribution') == 'small') { $shrinkFactor = 2; imagecopyresampled($im, $logo, $width-round(imagesx($logo)/$shrinkFactor)-4, $height-round(imagesy($logo)/$shrinkFactor)-4, 0,0, round(imagesx($logo)/$shrinkFactor),round(imagesy($logo)/$shrinkFactor), imagesx($logo),imagesy($logo)); } else { imagecopy($im, $logo, $width-imagesx($logo)-4, $height-imagesy($logo)-4, 0,0, imagesx($logo),imagesy($logo)); } } } #header('Cache-Control: max-age=' . (60*60*24*30) . ', public'); #header('X-Tiles-Downloaded: ' . $numTiles); // TODO: add caching $fmt = k($params,'format', 'png'); switch($fmt) { case "jpg": case "jpeg": header('Content-type: image/jpg'); $quality = k($params, 'quality', 75); imagejpeg($im, $filename, $quality); break; case "png": default: header('Content-type: image/png'); imagepng($im, $filename); break; } imagedestroy($im); /** * http://msdn.microsoft.com/en-us/library/bb259689.aspx * http://derickrethans.nl/php-mapping.html */ } function k($a, $k, $default=false) { 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; }