Source for file SMap.php
Documentation is available at SMap.php
* The layered map view is not for the faint of heart. If you are new to this,
* expect to spend quite a bit of time going back and fourth trying to
* understand what is going on. Of course I've done my best to make things
* simple, but this is simply complicated by nature.
* I'll attempt an overview:
* I've tried to implement common functionality, but you'll <i>need</i> to
* extend classes. At a minimum, you need to extend {@link SMap_Data},
* {@link SMap_Form}, {@link SMap_Lang}, and {@link SMap_Geo} to define the
* characteristics of your map. You will most likely extend varients of
* {@link SMap_Object} and {@link SMap_Layer} to draw out your map.
* The calling structure is to create instances of your custom classes, and pass
* them to the {@link SMap::__construct() constructor} of {@link SMap}. Once you
* have an instance of SMap, you call {@link SMap::addView()} to add your view
* to the map. Each view can be thought of as a drawing window. By default, they
* are linked to move and zoom together. So, you can display the same area with
* different layers showing.
* The interesting work takes place within {@link SMap_Layer the layer}. Layers
* are added to each view via {@link SMap_View::addLayer()}. When the time
* comes, {@link SMap_Layer::getImgObjs()} or {@link SMap_Layer::getMapObjs()}
* will be called. The layer will construct, organize, and return a set of
* objects appropriate for the view and bounds. The objects will draw your info.
* {@link SMap_Object} handles the brunt work of translating from whatever your
* layer wants to the {@link SMap_Canvas canvas}. Because these are primitives,
* you may or may not need to extend them.
* Returning to our instance of {@link SMap}, we have now set up our map
* structure. SMap takes over the hard part from here. Layers have been turned
* on or off and other changes may have been returned via {@link SMap_Form}.
* Those are collected and applied. Call {@link SMap::getViewXHTML()} to get the
* XHTML to insert into the page. Or call {@link SMap::dispTileImg()} to
* generate the raw image to return to the browser. And you're done!
* - {@link http://wms.jpl.nasa.gov/}
* - {@link http://bluemarble.nasa.gov/}
* - {@link http://www2.jpl.nasa.gov/srtm/}
* SMap requires at least PHP version 5, but there are important bug fixes in
* more recent versions of PHP. Try to stay current.
* Copyright (c) 2006-2007, Seth Price <{@link mailto:seth@pricepages.org}
* seth@pricepages.org}> All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* - The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* @copyright Copyright (c) 2006-2007, Seth Price
* @author Seth Price <seth@pricepages.org>
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @see SMap::__construct()
* SMap is split into parts for file size reasons
require 'SMap/Object.php';
require 'SMap/Layer.php';
* Inetrface for the main mapping functions
* The SMap is the object which binds all the mapping objects together. Kind of
* like {@link http://en.wikipedia.org/wiki/One_Ring The One Ring}.
* @link http://en.wikipedia.org/wiki/One_Ring
* These are used to define a rectangle on the map. The rectangle is
* commonly some sort of viewport, or some sort of clickable label.
* The form that will be used to display the map view(s) and process input
* The data that this will be pulled from
* Geography centric calculations
* Anything that isn't in the native X,Y system should be passed through
* The views that this controller controls
* The main view is at index 0.
* The layers that are currently visible
* What zoom level are we at?
* Generate it now so we don't have to later.
* How many tiles are we viewing?
* What is the scale in each direction?
* This is SMap Units per one pixel.
* @see SMap_Geo::getWrap()
* Options that can be set
* The size of the tile is the granularity of the movement of the view.
* Smaller tiles mean more requests, but the view is more precise and it is
* more likely that a tile will stay cached on the broweser.
* Directory to dump the cached data into. The web process must have write
* Maximum cache time of a tile. Applies mostly to images.
* It is strongly suggested that data caching be enabled. Labels may not
* work correctly if it isn't.
* It is strongly suggested that caching be enabled. The only reason that it
* isn't enabled by default is so people don't get confused when
* experimenting with the package and things don't show up right away.
* Dubugging prints out a mass of debugging info, instead of doing anything
* This library relies on comparing floating point numbers often. This is
* normally a fairly stupid thing to do, but there is no good way around it
* here. To get around the problem of 1.0 turning into 0.99999... we are
* going to be rounding floating point numbers often. How many significant
* figures should we round to when using {@link SMap_Util::reduceSigFigs()}?
* The hack that I use to enable PNG transparency in IE requires an image
* with binary transparency set to clear.
* When producing JPEG compressed images, we can control the quality of the
* compression using 'jpegQuality'. When caching images, we put some extra
* effort into making them small. Specifically, we use the jpegtran command
* line utility to compress the files. If we want to create a progressive
* jpeg in this manner, we can use 'jpegProgressive'.
private static $options = array(
'backgroundColor' => array(255, 255, 255, 127),
'cacheDir' => '/tmp/map_cache',
'cacheEnabledData' => true,
'cacheEnabledImg' => false,
'maxFileSize' => 4000000, // ~4MB
'transPNG' => '/imgs/spacer.png',
'jpegProgressive' => true,
'jpegtranBin' => '/usr/local/bin/jpegtran',
'optipngBin' => '/usr/local/bin/optipng',
private static $regViews = array(
'raster' => array(false, 'SMap_View_Raster'),
'inset' => array(false, 'SMap_View_Inset'),
'links' => array(false, 'SMap_View_Links'));
* Constructs with the form and data objects that will be used
* @param object SMap_Form subclass
* @param object SMap_Data subclass
* @param object SMap_Geo subclass
* @param object SMap_Lang subclass
* @uses SMap_Form::getMapVars()
* @uses SMap_Util::genMapKey()
* @uses SMap_Geo::getWrap()
* @uses SMap_Geo::zoomFctr()
* @uses SMap_Form::getBoundVars()
* @uses SMap_Form::getTileVars()
function __construct(SMap_Form $form, SMap_Data $data, SMap_Geo $geo, SMap_Lang $lang){
//Log any options that should be changed
//While we are here, make sure that the cache dir works
$cacheDir = self::getOption('cacheDir');
//Save the current map information
$map = $form->getMapVars();
$this->zoom = $map['zoom'];
//Save the geographical data
$this->wrap = $geo->getWrap();
//Get the current viewport size & save
$this->scale = $geo->zoomFctr($map['zoom']);
$this->bounds = $form->getBoundVars();
$tg = $form->getTileVars();
throw new SMap_Ex('Was not able to create cacheDir "'. $dir. '"', SMap_Ex::FILE_WRITE);
throw new SMap_Ex('cacheDir "'. $dir. '" not writable', SMap_Ex::FILE_WRITE);
* Access useful vars read-only
* @param string Name of member
* @return mixed Value of member
public function __get($nm){
throw new SMap_Ex('Attempted to read non-existant member '. $nm);
* Gets XHTML for a map view window
* The main map view is always stored in index 0
* @param integer Index of view
* @uses SMap_View::getXHTML()
* @uses SMap_View::getTiles()
* @uses SMap_Form::getMapVars()
if(empty($this->views[$viewId])){
throw new SMap_Ex( 'SMap_View ID '. $viewId. ' not found',
$map = $this->form->getMapVars();
$tiles = $this->views[$viewId]->getTiles($this->form);
return $this->views[$viewId]->getXHTML($tiles);
* The main view is always stored at index 0. Throws an exception if there
* is already a view at that index.
* @param mixed SMap_View subclass or view to create
* @param mixed Passed to the view constructor, normally a View ID
* @param mixed Additional arguments passed to view constructor
* @return object The added view
public function addView( $view, $arg1 = null, $arg2 = null,
$arg3 = null, $arg4 = null, $arg5 = null,
$arg6 = null, $arg7 = null, $arg8 = null){
$view = $this->createView($view, $this, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8);
throw new SMap_Ex('Passed $view not an instance of SMap_View.');
//Throw error if already exists
if(!empty($this->views[$vid])){
throw new SMap_Ex( 'SMap_View of ID '. $vid. ' already exists.',
return $this->views[$vid] = $view;;
* Serves as a view factory. The first argument is the name of a view that
* is registered with, and the rest of the arguments are passed to the view.
* @param string View name
* @param mixed Additional arguments...
* @return object The created view
* @uses SMap_View::__construct()
public static function createView( $view, $arg1, $arg2 = null, $arg3 = null,
$arg4 = null, $arg5 = null, $arg6 = null,
$arg7 = null, $arg8 = null, $arg9 = null){
if(empty(self::$regViews[$view])){
throw new Exception('The given view type "'. $view. '" is not built in. ' .
'If you have created a custom view, please simply ' .
'pass the object to addView().');
$meta = self::$regViews[$view];
if($meta[0] && !class_exists($meta[1])){
$v = new $meta[1]($arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $arg7, $arg8, $arg9);
throw new SMap_Ex('View not a subclass of SMap_View.');
* Display a map tile image with appropriate headers.
* This displays the image itself, and should be called from a script
* expecting a dump of the image. Caching is handled internally. The ini
* value of 'display_errors' is unset so an image is always displayed. For
* errors, please look in your log file.
* @uses SMap_View_Raster::dispTileImg()
$tileVars = $this->form->getTileVars();
if(empty($this->views[$tileVars['viewId']])){
throw new SMap_Ex('View ID of '. $tileVars['viewId']. ' requested but doesn\'t exist.');
$view = $this->views[$tileVars['viewId']];
//Make sure we are dealing with what we think we are dealing with
$view->dispTileImg(new SMap_Tile($this,$view,$this->bounds,$tileVars['tileGrid']));
* Handle a possible imagemap click
* Handle the possibility that the user clicked on a server-side image map.
* @return mixed Clicked URL or false
* @uses SMap_View::getTiles()
* @uses SMap_Form::getClickCoords()
* @uses SMap_Tile::getImageMap()
$tg = $this->form->getTileVars();
//See if we have positioning info and an accurate view ID
if( !isset ($tg['tileGrid'][SMap_Tile::POSX]) ||
if(!isset ($this->views[$viewId])){
throw new SMap_Ex('Attempted to handle map on non-existant view id: '. $viewId);
$tiles = $this->views[$viewId]->getTiles($this->form);
if(!isset ($tiles[$ty][$tx])){
* This may ocour when the parameters passed as the tile grid
* position are actually outside of the current grid. This can
* happen when changing the dimensions of the grid after using the
$tile = $tiles[$ty][$tx];
//Were click coordinates parsed out?
if(!($coords = $this->form->getClickCoords($tile->bounds))){
//Foreach layer in the tile
if($maps = $tile->getImageMap()){
//Foreach imagemap in the layer
* Find if the click matches the area map
* @param array Coordinate/URL set
* @param array Click coordinates
* @return string URL or false
list ($x, $y) = $coords['px'];
if(!preg_match('/^(\d+\.?\d*),(\d+\.?\d*) (\d+\.?\d*),(\d+\.?\d*)$/', $map['coords'], $m)){
if($m[1] <= $x && $m[2] <= $y && $m[3] >= $x && $m[4] >= $y){
$npol = preg_match_all('/(\d+),(\d+)/', $map['coords'], $m, PREG_PATTERN_ORDER);
* http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
* Don't blame me if your head explodes.
for ($i = 0, $j = $npol- 1; $i < $npol; $j = $i++ ) {
if ((($yp[$i] <= $y && $y < $yp[$j]) ||
($yp[$j] <= $y && $y < $yp[$i])) &&
($x < ($xp[$j] - $xp[$i]) * ($y - $yp[$i]) / ($yp[$j] - $yp[$i]) + $xp[$i])){
throw new Exception('Unrecognized shape: '. $map['shape']);
* All available options are pre-set in the {@link $options} array, so we
* will be avoiding some frusteration if we throw an error on a bad option
* selection. Otherwise, we could think that we are retrieving a null value,
* but we have just misspelled the index.
* @param string Option name
* @param string Option value
final public static function setOption($name, $value){
if(isset (self::$options[$name])){
self::$options[$name] = $value;
throw new Exception('The requested option "'. $name. '" doesn\'t exist.');
* All available options are pre-set in the {@link $options} array, so we
* will be avoiding some frusteration if we throw an error on a bad option
* selection. Otherwise, we could think that we are retrieving a null value,
* but we have just misspelled the index.
* @param string Option name
* @return string Option value
final public static function getOption($name){
if(isset (self::$options[$name])){
return self::$options[$name];
throw new Exception('The requested option "'. $name. '" doesn\'t exist.');
* Not passed a SMap_View object
* Not passed a SMap_Form object
* Not passed a SMap_Data object
* Not passed a SMap_Geo object
* Not passed a SMap_Layer object
* Not passed a SMap_View_Raster object
* Not passed a SMap_View_Links object
* Not passed a SMap_Object object
* Not passed a SMap_Tile object
* Requested view not found
* Poorly formatted bounds array
* Poorly formatted argument
* These should all be callable in static context.
* Rounds to reduce the number of sig figs in a float
* Useful when comparing two floating point numbers and printing numbers on
* a page. It should print numbers such that similar floating point numbers
* are always printed with the same string.
* Use the 'sigFigs' option to alter the precision of the produced strings.
* @param float Just another float.
* @return string Float expressed as a string.
* Generates a map key that can be used to identify a layout that may be
* stored in the database or filesystem.
* The layers list must be a numrical list in the order that the layers were
* @param array Active layers.
* @param integer Current view level.
* @param string ISO 639-2 three letter language string
* @return string SMap key, 40 chars or less
public static function genMapKey($layers, $zoom, $lang){
//Make sure we get what we expect
} elseif(isset ($layers[0]) && !is_int($layers[0])){
throw new SMap_Ex('Improperly formatted layer elm', SMap_Ex::BAD_ARG);
$lang) & 2147483647; // 2^31-1, prevent negative
* Generate a key for a tile
* The layers list must be a numrical list in the order that the layers were
* applied. Uses {@link reduceSigFigs()} to round the floats, so you can use
* 'sigFigs' to alter the size of thte numbers produced.
* @param array Bounds array
* @param array Position relative to other tiles
* @return string Key for this tile
public static function genTileKey($bounds, $tileGrid){
//Make sure we get what we expect
throw new SMap_Ex('Improperly formatted tile grid', SMap_Ex::BAD_ARG);
} elseif(isset ($layers[0]) && !is_int($layers[0])){
throw new SMap_Ex('Improperly formatted layer elm', SMap_Ex::BAD_ARG);
$str = self::reduceSigFigs($bounds[SMap::MAXY]). '_'.
self::reduceSigFigs($bounds[SMap::MAXX]). '_'.
self::reduceSigFigs($bounds[SMap::MINY]). '_'.
self::reduceSigFigs($bounds[SMap::MINX]);
return crc32($str) & 2147483647; // 2^31-1, prevent negative
* Shifts a view around a wrapped map
* @param array The bounds to wrap
* @param array The wrapping information
public static function wrapBounds(&$bounds, $wrap){
while(isset ($wrap[SMap::MAXY]) && $wrap[SMap::MAXY] < $bounds[SMap::MINY]){
$diff = $wrap[SMap::MAXY] - $wrap[SMap::MINY];
$bounds[SMap::MAXY] -= $diff;
$bounds[SMap::MINY] -= $diff;
while(isset ($wrap[SMap::MAXX]) && $wrap[SMap::MAXX] < $bounds[SMap::MINX]){
$diff = $wrap[SMap::MAXX] - $wrap[SMap::MINX];
$bounds[SMap::MAXX] -= $diff;
$bounds[SMap::MINX] -= $diff;
while(isset ($wrap[SMap::MINY]) && $wrap[SMap::MINY] > $bounds[SMap::MAXY]){
$diff = $wrap[SMap::MINY] - $wrap[SMap::MAXY];
$bounds[SMap::MINY] -= $diff;
$bounds[SMap::MAXY] -= $diff;
while(isset ($wrap[SMap::MINX]) && $wrap[SMap::MINX] > $bounds[SMap::MAXX]){
$diff = $wrap[SMap::MINX] - $wrap[SMap::MAXX];
$bounds[SMap::MINX] -= $diff;
$bounds[SMap::MAXX] -= $diff;
* Shifts the given bounds to be "normal"
* Shifts the bounds to contain an integer multiple of tiles, and for there
* to be an integer multiple of tiles between the current bounds and (0,0).
* When tiles are normalized like this there can be less precision in the
* URL. Any rounding errors are normalized out here.
* @param array Bounds to normalize
* @param float Map units per tile X
* @param float Map units per tile Y
* @param integer Number of tiles that should be in the X dir
* @param integer Number of tiles that should be in the Y dir
public static function normalizeBounds(&$B, $muPerTileX, $muPerTileY, $tilesX, $tilesY){
//Round to nearest multiple of (Dimension * Scale).
$B[SMap::MINX] = round($B[SMap::MINX]/ $muPerTileX) * $muPerTileX;
$B[SMap::MAXY] = round($B[SMap::MAXY]/ $muPerTileY) * $muPerTileY;
//Adjust the lower right corner to match
$B[SMap::MAXX] = $B[SMap::MINX] + $muPerTileX * $tilesX;
$B[SMap::MINY] = $B[SMap::MAXY] - $muPerTileY * $tilesY;
* Create bounds centered on this point
* @param float Center point X
* @param float Center point Y
* @param float Map units per tile X
* @param float Map units per tile Y
* @param integer Number of tiles that should be in the X dir
* @param integer Number of tiles that should be in the Y dir
* @param boolean Normalizes bounds also (default= true)
* @return array The new bounds
* @uses normalizeBounds()
$muPerTileX, $muPerTileY,
//Move the upper-left corner while remaining centered on the requested position
$B[SMap::MINX] = $Bx - ($muPerTileX * $tilesX)/ 2;
$B[SMap::MAXY] = $By + ($muPerTileY * $tilesY)/ 2;
self::normalizeBounds($B, $muPerTileX, $muPerTileY, $tilesX, $tilesY);
//Adjust the lower right corner to match
$B[SMap::MAXX] = $B[SMap::MINX] + $muPerTileX * $tilesX;
$B[SMap::MINY] = $B[SMap::MAXY] - $muPerTileY * $tilesY;
* Encode an integer as a char
* This function is used so we can pack the URL smaller. The chars used are
public static function enc($int){
* Decode a char as an integer
public static function dec($chr){
* Make an XHTML image tag compatable with IE
* MSIE sucks at the whole "internet" thing. Maybe they'll get it right next
* time. In the mean time, this function specifically emits an image tag
* that hacks transparency into a given PNG.
* @param string Image URL
* @param string Image Alt tag
* @param integer Image width
* @param integer Image height
* @param string Extra tags (caller escapes)
* @param string Extra style (caller escapes)
* @return string Image tag
public static function transPng($url, $alt, $w, $h, $tags, $style){
//Escape text, because it's a good idea
$url = htmlspecialchars($url);
$alt = htmlspecialchars($alt);
//Is the web browser evil?
$msie = '/msie\s(5\.[5-9]|[6]\.[0-9]*).*(win)/i';
if( !isset ($_SERVER['HTTP_USER_AGENT']) ||
!preg_match($msie,$_SERVER['HTTP_USER_AGENT']) ||
preg_match('/opera/i',$_SERVER['HTTP_USER_AGENT'])){
if($isEvil && substr_compare($url, '.png', strlen($url)- 4, 4, true)){
//If not evil, return a normal image
$style = ' style="'. $style. '"';
return '<img src="'. $url. '" alt="'. $alt. '" ' .
'width="'. $w. '" height="'. $h. '" '. $tags. $style. ' />';
return '<img src="'. SMap::getOption('transPNG'). '" alt="'. $alt. '" ' .
'width="'. $w. '" height="'. $h. '" ' .
'style="width:'. $w. ';height:'. $h. ';'. $style .
'filter:progid:DXImageTransform.Microsoft.' .
'AlphaImageLoader(src=\''. $url. '\',sizingMethod=scale);" '.
* We need to connect to the DB somehow, all relevant methods are defined here.
* I've defined some basic SQL queries as constants, feel free to use them or
* abuse them as needed. The selects are all prepared statements and should be
* It is my intent to make these statements simple enough to be usable with
* both SQLite and MySQL while still maintaining functionality. I am aware that
* people may want more powerful syntax, and they are encouraged to extend this
* CREATE TABLE cached_rec_bounds SQL
* Rectangle bounds data is stored here across sessions. We need a map_id
* and bound_id to identify the bound that we are interested in, and most of
* our lookups will be selecting for either map_key and rec_key, or all
* bounds of a given map_key that fall in a given bounds. The former is
* preferred for performance reasons, though.
* For a given set of bounds, there should be a unique map_key, rec_key
* pair, and for the pair, there should be unique bounds.
'CREATE TABLE cached_rec_bounds (
map_key INTEGER NOT NULL,
rec_key INTEGER NOT NULL,
PRIMARY KEY (map_key, rec_key))';
* CREATE INDEX ON the rect bounds
* I think that only one of top-bottom / left-right indices will be useful
* and all four indices is overkill (and will delay inserts). So I'm going
* to choose to keep the left-right index under the assumption that most
* maps will be wider than they are tall (same with monitors).
'CREATE INDEX bounds ON cached_rec_bounds (map_key, maxx, minx)';
* INSERT a new bound in the cache
* Parameters: map_key, rec_key, top, bottom, left, right, time
INTO cached_rec_bounds (map_key, rec_key, maxx, maxy, minx, miny, data, time)
VALUES (:map_key, :rec_key, :maxx, :maxy, :minx, :miny, :data, :time)';
* SELECT a cached rect by key
* Parameters: map_key, rec_key
* Results: maxx, maxy, minx, miny, time
'SELECT maxx, maxy, minx, miny, data, time
WHERE map_key=:map_key AND rec_key=:rec_key';
* SELECT a session rect by the a boundry that intersects it
* This SQL assumes that up is positive Y and right is positive X (like the
* latitude/longitude coordinate system).
* Parameters: map_key, maxx, maxy, minx, miny
* Results: rec_key, maxx, maxy, minx, miny, data, time
'SELECT rec_key, maxx, maxy, minx, miny, data, time
maxx >= :minx AND minx <= :maxx AND
maxy >= :miny AND miny <= :maxy';
* DELETE session data before a given timestamp
* The session results are calculated from the position of objects on the
* map, and should be recalculated depending on how often the objects
* Some enumerations for types of rects
* The currently defined functions expect this to be an instance of PHP's
* PDO, but you can override them and use whatever database/schema that you
* Create the database connection
* Currently, the other functions expect this to create a new instance of
* PDO and save it at $this->conn. We'll attempt to worry about a lack of
* table as they are needed (queries fail, exceptions ocour).
* The connection is automatically generated when you call
* {@link prepare()}, but if the need arises in your code, feel free to call
abstract protected function connect();
* CREATEs the cached_rec_bounds table
$this->conn->exec(self::CREATE_CACHED_RECT_BOUNDS);
$this->conn->exec(self::CREATE_CACHED_RECT_INDEX);
} catch (PDOException $e){
throw new SMap_Data_Ex('Trouble creating tables: '. $rs->getMessage());
* INSERT new rectangle(s) into the cache
* The parameter is an array of new rectangle(s) to insert into the
* cache. The new array should have keys for: ['map_key'], ['rec_key'],
* ['maxx'], ['maxy'], ['minx'], ['miny'].
* @param array Input data
$stmt = $this->prepare(self::INSERT_CACHED_RECT_DATA);
foreach($newRects as $rect){
$stmt->execute(array( ':map_key' => $rect['map_key'],
':rec_key' => $rect['rec_key'],
':maxx' => $rect[SMap::MAXX],
':maxy' => $rect[SMap::MAXY],
':minx' => $rect[SMap::MINX],
':miny' => $rect[SMap::MINY],
':data' => $rect['data'],
* SELECT cached rect by its key
* @param string A key that identifies the map the rect is on.
* @param string A key that identifies the specific rectangle.
* @return mixed Array on result or null on no result
$stmt = $this->prepare(self::SELECT_CACHED_RECT_BY_KEY);
$this->execute(array( ':map_key' => $mapKey,
return $stmt->fetchAll();
* SELECT a cached rect by the its intersection with another rect
* @param string A string to identify the map we are looking in.
* @param array Selection bounds
* @return array Any rectangles in the selection.
$stmt = $this->prepare(self::SELECT_CACHED_RECT_IN_BOUND);
$stmt->execute(array( ':map_key' => $mapKey,
':maxx' => $B[SMap::MAXX],
':maxy' => $B[SMap::MAXY],
':minx' => $B[SMap::MINX],
':miny' => $B[SMap::MINY] ));
return $stmt->fetchAll();
* DELETE all old cached data
* @param integer Old timestamp
$stmt = $this->prepare(self::CREATE_CACHED_RECT_BOUNDS);
$stmt->execute(array(':time' => $oldTime));
* All statements should be prepared before execution. Besides being
* prepared, we check here to ensure that a DB connection exists, and that
* the tables exist in the DB.
* @param string SQL statement to prepare
* @return resource Prepared statement
* @uses createCachedRectBounds()
if($this->conn === null){
return $this->conn->prepare($sql);
} catch (PDOException $e){
if($e->getCode() == '42S02'){ // This must be the 'no table' code for MySQL?
$this->prepare($sql); // Careful! This may overflow!!! (FIXME)
throw new SMap_Data_Ex('Statement failed: '. $e->getCode(). ' '. $e->getMessage());
* SMap DB exception class
* Controls geographic charicteristics of the view
* The user of the library will implement these to return appropriate numbers
* for whatever coordinate system this is being used in. Also sets geographical
* Get the maximum zoom level
* Give the distance between two points in meters
* @param object First point
* @param object Second point
abstract public function distBetween(SMap_Object_Point $p1, SMap_Object_Point $p2);
* Return a point that has followed a path for a distance
* @param object SMap_Object_Path
* @param float Distance in meters
abstract public function follow(SMap_Object_Path $path, $dist);
* Return a line that has followed a bearing from a point
* @param object SMap_Object_Point
* @param float Distance in meters
* @param float Angle in radians
* @return object SMap_Object_Line
abstract public function bearing(SMap_Object_Point $start, $dist, $bearing);
* At the chosen zoom number, one pixel equals how many SMap Units?
* This number should be zero or a positive integer. Zero is zoomed in all
* @param integer Zoom number
* @return array Scaling factor X,Y
* @see SMap_Form::setDefaults()
abstract public function zoomFctr($zoomNum);
* Get globe wrapping information
* Does this map wrap? On what bounds does it wrap? This should return an
* array of bounds which are wrapped.
* @return array Wrapping information
abstract public function getWrap();
* Return a nicely printed coordinate
* @return string Coordinate text
abstract public function pretty($val, $side);
* Controls the language used in the view
* The language differs on the units used and the humans used. All language
* codes used in the map package are three letter ISO 639-2 codes.
* The language that we will be using
* Sets the language that we will be using
* You can only set this once though
* @param string The language code
throw new SMap_Ex('Refusing to re-set the language.');
* Gets the current language
* @return string The current language code
* @param integer The ID of the requested layer
* @return string The name of the layer
* A canvas for drawing the image
* Keep track of references to the image resource
* The tile which this canvas is drawing
* Create a canvas which fits the current tile
* Access useful vars read-only
* Creates the image resource on demand
* @param string Name of member
* @return mixed Value of member
throw new SMap_Ex('Attempted to read non-existant member '. $nm);
* "If I go down, I'm taking this image with me!"
* "Don't do it man! Noooooooo!!!!"
* Allocates a true color image and initialazes it to be clear.
* @return resource A GD image resource
throw new SMap_Ex('Image already alloced!');
$w = $this->tile->widthPx;
$h = $this->tile->heightPx;
throw new SMap_Ex('Unable to allocate image!');
//Start with a transparent canvas
* Allocate a color for an image
* @param array Color in RGB or RGBA
* @return integer A color handle
if(!isset ($color[3]) || $color[3] === null){
* Composite another canvas atop this one
* @param object Top canvas
public function composite(SMap_Canvas $atop){
* Composite an image resource
protected function compositeRes($res, $dstX,$dstY, $dstW,$dstH){
imagecopy($this->img, $res, $dstX,$dstY, 0,0, $dstW, $dstH);
* Load an image from file
* @param integer File type
* @param string File name
* @param integer Place at X
* @param integer Place at Y
* @param integer Load from X
* @param integer Load from Y
* @param integer Load width
* @param integer Load height
* @return boolean Image loaded
public function load($ftype, $fname, $dstX,$dstY, $srcX,$srcY, $width,$height){
$rawImg = $this->loadRaw($ftype, $fname, $srcX,$srcY, $width,$height);
$this->tile->widthPx != $width || $this->tile->heightPx != $height ||
return (bool) $this->img;
* Load an image from file
* @param integer File type
* @param string File name
* @param integer Load from X
* @param integer Load from Y
* @param integer Load width
* @param integer Load height
* @return resource GD Image
protected function loadRaw($ftype, $fname, $srcX,$srcY, $width,$height){
if($width < 0 || $height < 0){
throw new SMap_Ex( 'Unable to recognize image type: '. $ftype,
if($width >= 0 && $height >= 0){
imagecopy($img, $rawImg, 0,0, $srcX,$srcY, $width, $height);
* @param integer Requested format
* @param integer Color handle
* @param integer Beginning X
* @param integer Beginning Y
* @param integer Ending X
* @param integer Ending Y
* @param integer Color handle
public function line($begX,$begY,$endX,$endY, $col){
* @param integer Upper left corner X
* @param integer Upper left corner Y
* @param integer Lower right corner X
* @param integer Lower right corner Y
* @param integer Color handle
public function rectangle($x1, $y1, $x2, $y2, $col){
* Draw a filled rectangle
* @param integer Upper left corner X
* @param integer Upper left corner Y
* @param integer Lower right corner X
* @param integer Lower right corner Y
* @param integer Color handle
* @param array Pairs of points
* @param integer Number of points
* @param integer Color to draw
public function polygon(&$pts, $numPts, $col){
* @param array Pairs of points
* @param integer Number of points
* @param integer Color to draw
* @param float Angle to draw the text at
* @param resource Font to use
* @param integer X Position to draw text
* @param integer Y Position to draw text
* @param integer Color to draw
public function str($ang, $font, $x,$y, $text, $col){
throw new SMap_Ex('Text angle not supported: '. $ang);
* Find a font width in pixels
* @param integer Font resource
* @param string Text to measure
* @return integer Font width in pixels
public static function fontWidth($font, $str){
* Find a font height in pixels
* @param integer Font resource
* @param string Text to measure
* @return integer Font height in pixels
* A class for caching tiles
* This class is used to cache tile images to disk. If you are caching the
* layered tile, pass the image type to {@link load()} and an optimized version
* of the tile will be saved when {@link display()} is called. For optimization,
* ensure that you have the command line utilites jpegtran and optipng
* If you have image caching disabled, I expect that you will enable it
* eventually, so I enable a "debug mode" in {@link cache()} which reads the
* images back after saving them to help ensure that there is no image
* If you are caching an individual layer, you should use IMAGETYPE_GD2 for the
* format. Then embed this cache in {@link SMap_Object_CachedImage}.
* The retrieved image data
* The size of the retrieved image
* @param array Layers contained in this cache
* @param integer Image format
function __construct(SMap_Tile $tile, $layers, $fType){
* Creates the filename for a cached tile
* We will need the identity of the map, which is passed in as a string. We
* will also need the identity of this tile, which we will be generating.
//Get the key for the tile
$layerKey = '_'. $this->layers[0];
$fname = SMap::getOption('cacheDir'). '/imgs/'. $this->tile->map->key. '_'. $tileKey. $layerKey;
* Was a cached image loaded?
return !empty($this->img) || !empty($this->imgRaw);
* Loads a cached tile image
* If a cached version of this tile exists, load it, but respect the cache
* time. If the file exists, but the age of the file is greater than the
* modification time, no image is loaded.
* The expected filetype is one of the IMAGETYPE_* constants.
* The last modified time is set as the modified time of the file using
* {@link setLastModified()}. The size of file read is limited to 4 MB. I
* can't think of a good reason for your tile to be larger than that
* anyway, so this may signal an error.
* @param integer Minimum file modification time.
* @return mixed False if not found, GD resource, or file contents
* @uses setLastModified()
//Save us from being stupid
throw new SMap_Ex('Attempted to load from cache when we already had ' .
'an image loaded.', SMap_Ex::RES_COLL);
if(!($fp = fopen($fname. '.lock', 'r')) || !flock($fp, LOCK_SH)){
//Close if we have a file pointer
//Read it if valid, delete it if not
$this->tile->setLastModified($mod);
public function cache($fmt){
if(!($fp = fopen($fName. '.lock', 'w')) || !flock($fp, LOCK_EX + LOCK_NB)){
//Close if we have a file pointer
* If the image is worth caching, it's worth compressing. So compress it
* here so we can save bandwidth in the future.
* Otherwise, we're enabling a "debug" mode which checks the validity of
* the files. If the files may get corrupted, we best know about it
* before we enable caching.
imagegd2($this->img, $fName, max($widthPx, $heightPx), IMG_GD2_COMPRESSED);
//Compress the png a bit more
|