Conversion tables A-K
  Conversion tables L-Z
  SPAM database query
  Reverse WHOIS utility
  North America area codes
  PHP Example forums

PHP example repository.

Web www.web-max.ca
Back to examples
GIS Mapping in PHP: Part 2
Published online at PHPBuilder.com

In my previous article I described a very simple method of plotting longitude and latitude coordinates onto a drawn map of the world. With a very small amount of work modifying the script in the article - adding a FOR loop and some database code ) you could quite easily come up with a system like www.geoURL.com in a matter of minutes.

The main limitation with this system is that you are restricted as to what background you can use. You have to have a predrawn raster image drawn in the correct scale and projection for the information you want to display. What if all you want to show is the location of a house in Prince Edward Island? Where do you get that raster image? What if the house is on the edge of the island and you want it centered on the screen? What if you want to zoom in and out without a significant loss of quality? What if goats overthrow the goverments of the world and forced us to wear itchy beards?. The answer to these questions is to draw your own background images dynamically. You can then scale, move, center, color to your hearts content. As for the goats .. well .. be nice to them and they will be nice to you.

What's the point of Vector?

So then first step is to get and process the data for the background image. The data we need is called vector data ( as opposed to the Raster image we were viously dealing with ). This is normally stored as a list of points divided into regions which make up complete polygon shape of the state / province / country. These points are normally listed as longitude and latitudes, which is great because we already have a function from the previous article to convert long/lats into screen coordinates - getlocactioncoords. Vector maps allow for easy scaling and movement. You can rotate, stretch and skew with a minimum of fuss and bother.

Now the big problem is finding the vector data in the first place. You can buy this information, or you can find basic vector information around the web. www.geogratis.comis a good place to start. Also check the US/Canadian Enviromental sites. Remember, normally the purchased products will have a greater level of detail. Most of the world is available for free in 1:1000000 scale, including outline maps for US and Canadian states/provinces ... just give google a try. For those lucky lucky people with access to Mapinfo check the sample disks that come with it. You should find coverage for North America plus a geocoded US Gazeteer. Another good google keyword to keep in mind is DCW - the digital chart of the world. This gives you polygons for every country in the world, plus more detailed information for North America ( states and provinces ). You can also find lakes, rivers, roads and outlines of major population areas.

Ok, so you've downloaded a chunk of data. We now run into our next problem. Most of the vector data on the web is in Arcview format ( .E00 ). We need to load this data into PHP in to variables which we can them read and manipulate. We need to be able to differentiate between the different types of vector information and also ( in later stages ) be able to assign colors and formating to the data.

The E00 format is text based ( in uncompressed form ) and there a few PHP routines out there that will load the various elements of an E00 file and draw it onto an image. However, in this article I will have -converted all my E00 into Mapinfo MIF format. "Why?" you ask. "You swine, we thought you were going to tell us how to load E00 directly" you scream. Well, for starters the MIF format is very easy to read and load in a script, and also allows for various attributes to be associated with the polygon region ( which although we ignore could be usefull to some people ). The other reason is that I use Mapinfo is that if you are serious about GIS, Mapinfo is a very handy tool to have. I'm not on a commission ( though a free mug would be nice ) , but from experience a proper GIS application becomes invaluable when dealing with large amounts of information and will allow you to export a wide variety of GEO vector data into the PHP scripts I've given you. It is great for splitting large vector regions into smaller, more manageable chunks ( think Quebec, a bit map at the best of times ) - the smaller the chunks of data you deal with the faster the processing time and the less work PHP will have to do. The other benefit is being able to align downloaded data and "triming" regions which are out of alignment. Remember free data from different sources will always have different levels of accuracy. Being able to align and trim data to a base map will make your maps look much more professional. Remember PHP will never be able to offer the complete functionality a true GIS application can, so if you are serious about mapping its worth splashing out on a licence on good GIS tool.

For those without access to GIS tools ( c'mon guys, there are some free utilities out there ), the import routine provided in this article could easily be modified to read from E00 files, and if I have time I will add the routine to do this in another article at a later date. If someone wants to do one and let me see it, even better. I will see if I can incorporate it.

Anyho, back to getting our data in.

Getting it all in.

We have downloaded an outline map of PEI from http://geogratis.gc.ca/ . I won't give to the exact link as its good for you to browse around and get an idea of what data is available ( sorry, thats the school teacher in me - just remember to look for data in GEOG format, not LAMB. I could tell you why, but I'd have to shoot you.)

Next we have convert our E00 data into Mapinfo MIF using Mapinfos Universal Translator ( or some free utility on the web ). Although it doesn't really matter, try and keep all your projections constant when converting the data. Though in the MIF file the coordinates will look the same and will work for us, if you use different projections it can get messy if you ever decide to edit the files in Mapinfo at a later date.

So lets get the data into PHP. For this article our mission is to plot the location of my house on an outline map of Prince Edward Island. Why Prince Edward Island? Well, I live there and it is also has a smaller polygon count compared to other provinces. I'm gonna be nice to you all .... download the PEI MIF file from the downloads section at the bottom of this page.

Sit down, have a piece of cake and get ready for the next part.

At it's most basic ( and minus a bunch of header information ) the MIF format looks like this:

DATA
{VECTOR TYPE} {NUMBER OF SUB-REGIONS}
{POINTS IN SUB-REGION 1}
{LONGITUDE} {LATITUDE}
{... POINTS IN SUB-REGION 2}
{LONGITUDE} {LATITUDE}

{VECTOR TYPE} {NUMBER OF SUB-REGIONS}
{POINTS IN REGION}
{LONGITUDE} {LATITUDE}

etc etc

ie ( a 'Region' type is basically a set of polygons )

Region 1
3
-63.778904 46.515854
-63.790916 46.518894
-63.796062 46.524384
Region 2
3
-63.778904 46.515854
-63.790916 46.518894
-63.796062 46.524384
4
-63.778904 46.515854
-63.790916 46.518894
-63.796062 46.524384
-63.796062 46.524384

E00 has a similar system of storing data but it involves a slightly more long winded process to get the data out.

For the outline polygons of states and provinces, we only really deal in the vector type of "REGION" (a polygon). MIF files can also contain PLINES ( a non-filled polygon ) and LINES which our importer can handle, plus a number of other shape types, which we don't need or care about. A point to note at this stage is that there are two types of Polygon - outside and inside. Basically one type of polygon is a filled area, the other is a hole - think of a donut. One polygon is the donut, the other is the hole. Due to processing limitations, we don't really deal with inside polygons. In later stages we come up with some cheats to get around this, especially when it comes to lakes and rivers.

To load the file, we just loop through each line read from the file. When we detect the start of a region ( by the presence of a string naming the type of vector object ), we create a new array and start to parse the coordinates into it. The polygons are stored in PHP in a array of associative arrays, with each associative array containing the type of vector object we have, the number of points in the object, the coordinates of the bounding rectangle and a string containing a space delimited list of coordinates.

At the moment we will load the vector data from a Mapinfo MIF file into the arrays each time we run the script. This is not the best way to do it for large amounts of data - however, it is the simplest method for the needs of the article. In later articles the same function will be used to prepare the data for import into POSTGRESQL.

The import script is made up of two functions, LoadMIF and GetPolyString.

<?php
function LoadMIF($file)
{

//Loads MIF file into a set of arrays.

// Open the file

$hfile = fopen("$file", "r");
$polygons = array();
$in_data = false;

// Read through the file until we hit the end

while (!feof($hfile))
{

$line = strtoupper(fgets($hfile, 1024));

// You could do this with reg. expressions. I hate them. So there.
// The DATA tag tells us we have got past the header info
// and are into the vector data proper.

if(substr($line,0,4)=="DATA")
{
$in_data=true;
}
else if($in_data)
{

// Are we a LINE? NB we don't plot these in this article.

if(substr($line,0,4)=="LINE")
{
$array = explode(" ",$line);
$poly_info = array();
$poly_info["min_long"] = $long = trim($array[1]);
$poly_info["min_lat"] = $lat = trim($array[2]);
$poly_info["max_long"] = $long_to = trim($array[3]);
$poly_info["max_lat"] = $lat_to = trim($array[4]);
$poly_info["vector_type"] = 1;
$poly_info["poly_count"] = 2;
$poly_info["poly_string"] = "$long $lat $long_to $lat_to";
}

// Are we a PLINE ( poly-line: A hollow polygon )

else if(substr($line,0,5)=="PLINE")
{
$array = explode(" ",$line);

// Get all the points in this polygon
// The first word on the line always stores
// the number of points in the polygon

$poly_info = GetPolyString($hfile,$array[1]);
$poly_info["vector_type"] = 2;
}

// Are we a region ( a filled polygon )

else if(substr($line,0,6)=="REGION")
{
$line = fgets($hfile, 1024);

// Again, get all the points in this polygon
// The first line always stores the number of points in the polygon

$poly_info = GetPolyString($hfile,$line);
$poly_info["vector_type"] = 3;
}
if(isset($poly_info))
{
$polygons[] = $poly_info;
unset($poly_info);
}
}
}
fclose($hfile);
return $polygons;
}

function GetPolyString($hfile,$poly_count)
{
$ret_vector = array();
$ret_vector["min_long"] = 9999999;
$ret_vector["min_lat"] = 9999999;
$ret_vector["max_long"] = -9999999;
$ret_vector["max_lat"] = -9999999;
$ret_vector["poly_string"] = "";
$ret_vector["poly_count"] = $poly_count;

// Loop though the coordinates
// Each line contains the long. and lats. coordinates
// delimited by a space.

for($i=0;$i<$ret_vector->poly_count;$i++)
{
$line = fgets($hfile, 1024);
$array = explode(" ",$line);
$long = $array[0];
$lat = $array[1];
$ret_vector["min_long"] = min($long,$ret_vector["min_long"]);
$ret_vector["min_lat"] = min($lat ,$ret_vector["min_lat"]);
$ret_vector["max_long"] = max($long,$ret_vector["max_long"]);
$ret_vector["max_lat"] = max($lat ,$ret_vector["max_lat"]);
if(!empty($ret_vector["poly_line"]))$ret_vector["poly_line"] .= " ";
$ret_vector["poly_line"] .= "$long $lat";
}
return $ret_vector;
?>

To use: $polygons = LoadMIF("{filename}");

There are some parts of these functions which do not seem to have any purpose whatsoever. Just relax and don't have a cow. In the greater scheme of things all will become clear.

Ok, so now we have the outlne of PEI loaded into memory. We should ( in the case of PEI ) have an array of 33 associative arrays containing all the information we need to draw the map.

Putting PEI on the map

The steps we need to take for drawing are:

Work out how big we want the final JPEG is going to be
Work out where the outline of PEI is to be centered
Work out the scale of the outline map.

We'll go straight the code which I'll disect later:

<?php
// Lets load the MIF file into our array

$polygons = LoadMIF("1.mif");
// This is the width of our final image

$image_sx = 400;
// This is the height of our final image

$image_sy = 400;

//This is the scale/zoom level if not parsed.

if(empty($scale))$scale = 35000;

// Next we set our base object we want to plot and center on

$my_long = -63.10774861954596;
$my_lat = 46.2899306519141;

// Set the correct scale for use in getlocationcoords

$sx = 2 * $scale;
$sy = $scale;

// Now we find out what screen coordinates the long/lat
//coordinates are at based on a complete map of the world

$center = getlocationcoords($my_lat, $my_long, $sx,$sy) ;

// Based on the size of the final image, we work out the
// amount we will need to add/subtract from the screen
// coordinate that will be calculated later to center our point
// on the final image.

$min_x = $center["x"] - ($image_sx / 2);
$min_y = $center["y"] - ($image_sy / 2);

// So lets create our image, and also allocate some colors to
// make everything look purdy.

$im = imagecreate($image_sx,$image_sy);
$land = imagecolorallocate ($im, 0xF7,0xEF,0xDE);
$sea = imagecolorallocate ($im, 0xB5,0xC7,0xD6);
$red = imagecolorallocate ($im,0xff,0x00,0x00);

// Lets now draw out inital background .. the mighty ocean.
// You could also use a drawn "sea scape" image if you
// wanted things to look a little different.
imagefilledrectangle($im,0,0,$image_sx,$image_sy,$sea);

// Now we loop through the array of arrays getting each polygon
// in turn

foreach($polygons as $poly)
{
$converted_points = array();

// Each vector objects is stored as a space delimited string
// {long} {lat} {long} {lat} etc etc.
// So we explode these points into a temporary array for
// easy conversion to screen coordinates.

$points = explode(" ",$poly["poly_string"]);
$number_points = count($points);
$i = 0;
while($i<$number_points)
{

// Get each long/lat in turn. Convert it to screen coordinates
// Then subtract the "world screen" coordindate of our base object
// ( our house ) so the polygon will be centered around it.

$lon = $points[$i];
$lat = $points[$i+1];
$pt = getlocationcoords($lat, $lon, $sx, $sy);
$converted_points[] = $pt["x"] - $min_x;
$converted_points[] = $pt["y"] - $min_y;
$i+=2;
}

// Then use GD to draw the polygon. We divide the number of points by
// 2 as this is the actually true number of points in the array

imagefilledpolygon($im,$converted_points,$number_points/2,$land);
}

// Next center our base object

$pt["x"] = $center["x"] - $min_x;
$pt["y"] = $center["y"] - $min_y;

// And plot it in the middle of the map

imagefilledrectangle($im,$pt["x"]-2,$pt["y"]-2,$pt["x"]+2,$pt["y"]+2,$red);

// Set the headers and return the image header("Content-type: image/png");

imagepng($im);
imagedestroy($im);

?>

And thats it ... you have drawn a map of PEI and plotted my house on it.

By altering the value of $scale you can zoom in and out. By adding/subtracting variables to $min_x and $min_y you can provide scrolling. You could also just save the image and use it as a base map for other maps or as a server side cache to speed things up. Tidy it up in Photoshop or add custom symbols for even better base maps with no copyright as you made it!!!!. Take it out to dinner and let it meet your parents.

Now althougth this map just shows PEI, there is no reason why you could not have a bunch of MIF files ( or one big one ) containing the whole of North America, or even the world. Then you have a scalable, scrollable map of the freakin' world! But before you go off celebrating, there are a few things to remember.

Don't stress out PHP

First and formost is speed. We love PHP and we know it does its best, but we are not far off doing some major number crunching here. PEI is the smallest province around ( awwww .. cutie!!! )... when you get to a complicated region like Quebec with 30000 polygons in it ( or even the world ) .. things really start to get slow ... very slow. These problems will be addressed in the next article as we let POSTGRESQL do some of the hard work, and we also talk about making a custom PHP module in C to do some of the donkeywork. However, as a work-around, consider the following options/modifications:

Make a bunch of scale dependant MIF/E00 file. You are looking at PEI at a scale of 100000 - you don't need all that coastline detail. So create another MIF file with less details, and load it dependant on a preset scale range ( ie pei_1_to_60000.mif, pei_60000_to_100000.mif and so on ).

We give you the max/min coordinates of a polygon when we load it in LoadMIF ( and you said that code wasn't being used .. shame on you ), feel free to use them. If a polygon is outside your display, don't bother with it. Don't convert it and don't display it! ( this is dealt with in the next article along with a "reverse" getlocationcoords for this purpose ). This is where a GIS tool comes in handy. Calculate the scale/size you use most often and split your map into chunks that just cover that area.

Then comes memory. Be mindfull of PHPs memory usage. The default 8meg allocated to a script on most PHP installations is not enough to handle a large MIF file with thousands of polygons ( like British Columbia ). POSTGRESQL will cure some of this, but try to split large maps into smaller chunks. You can then filter them in the LoadMIF program ( with some tweaking ).

Summary

So, there you have it. Next time we learn how to get this data into and out of POSTGRESQL. You want roads? Where we're going we don't need roads, but I show you how to plot them anyway. And maybe we'll do some work on caching ... you never know.

Simon Moss

 
Downloads

 article_2.zip

Related Links
Mapinfo
Geogratis