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 |