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.
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.com is 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
here.
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}");
You should get this:
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.<br>
$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.
A formatted copy of the source code can be found
here. This does work, I've tried it! Please don't email me asking for map data or
conversion tools. All the things you need are out there for free, just look
for them. Thats how I found them.