Distance Compare Class Part A
This post assumes advanced understanding of PHP scripting and some object-orientated knowledge.
One area of PHP that I haven't had much experience with yet is classes. I have a rough idea of what they do, but after working with a few inefficient classes, figured that they are more trouble then they're worth. It wasn't until one of my contract jobs specifically requested an application built up from a class that I actually built one from scratch and started understanding the true benefit of object-orientated programming.
In this scenario, the client owned about eight stores with unique addresses. They wanted a page on their website that allowed a user to input a zip code in a form to find the closest stores. There are several php classes online that can do this, but they depend on custom databases with zip code information built in and only calculate straight-line distances. As zip codes change over time, these classes became outdated quickly, requiring regular maintenance.
For this project, I decided to connect to Google Maps to find out the driving distance between the user's zip code and the available stores. The first step, though, is creating the class with a construct method. I wanted this class to work regardless of the method of address retrieval - database, flat-file, or passed in from the user - so my construct method takes an array of addresses and saves them to a public variable.
public function __construct($address_array)
{
foreach($address_array as $key => $row)
{
if($this->_checkAddress($row))
$this->address_array[$key] = array('address'=>$row,'distance'=>'');
else
$this->error[] = array('error','there is a problem with the address array');
}
}
This function is checking the validity of each address in the array before saving it to $this->address_array(), which will be my holder for the store addresses throughout the rest of my class. I made my checkAddress function very simple, though - right now it only returns true. If I needed a more secure regular expression checker, I could add it here - this application only uses zip codes, and I check for those before they're sent to this class.
Once the address array is loaded, the next step would be to find out how close the user zip code is to the available stores. While this particular application only wanted the stores within a certain radius returned, I wanted to have the option of sorting the available addresses in order of proximity. So, I made a sortAddress function.
public function _sortAddresses($address)
{
if($this->_checkAddress($address))
{
foreach($this->address_array as $key => $row)
{
$to = $row['address'];
$from = $address;
$distance = $this->_requestDistance($to,$from);
$this->address_array[$key]['distance'] = $distance;
}
foreach($this->address_array as $key => $row)
$distance_array[$key] = $row['distance'];
asort($distance_array);
foreach($distance_array as $key => $row)
$array[] = $this->address_array[$key];
}
else
$this->error[] = array('error','there is a problem with this address: '.$address);
if($this->error)
return $this->error;
else
return $array;
}
This function takes a single test address, in this case a zip code entered by the user, and first checks to see if its valid. It then loops through the available addresses and gets the distance between each available address and the test address, returning the distance and saving it to the address array. I then had to separate out the distances to sort the array by distance before returning the whole array. The distance request function is a request from Google...
private function _requestDistance($to,$from)
{
$url = 'http://maps.google.com/maps?saddr='.urlencode($from).'&daddr='.urlencode($to).'&oe=utf8&doflg='.$this->format.'&dirflg='.$this->route.'&output=kml';
$xml = simplexml_load_file($url,'SimpleXMLElement',LIBXML_NOCDATA);
for($i=0;$i
Document->Placemark);$i++) {
if($xml->Document->Placemark[$i]->name=='Route')
$description = $xml->Document->Placemark[$i]->description;
}
if($description)
return floatval(str_replace(',','',substr($description,strpos($description,'Distance: ')+10,strpos($description,' ')-(strpos($description,'Distance: ')+10))));
else
$this->error[] = array('error','google can not understand the address');
}
I learned, quite a while ago, that if you change the output variable on any Google Maps link, you can return a variety of different formats, including KML. This is incredibly handy for this application, though the distance information is embedded pretty deep. After I grab the KML, I need to parse out the correct placemark node and then take out the correct substring from its description. The interesting thing I found here was the inability for floatval, a native PHP function, to understand commas in large numbers, which is why I had to remove it before sending the distance back to my sorting array.
So far, I've taken an array of addresses, compared it to a single address, found the distance between, and sorted the original array by this distance. I still needed to make two more functions - one to filter the array by a radius and another to return the 'best match'. These two functions use the sortAddresses function to keep things simple, and only play with the array returned before passing the request out.
public function _filterAddresses($address,$radius)
{
$array = $this->_sortAddresses($address);
foreach($array as $key => $row)
{
if($row['distance']>$radius)
{
$array = array_slice($array,0,$key);
break;
}
}
if(!$array)
$this->error[] = array('error','there was no addresses that are within the radius');
if($this->error)
return $this->error;
else
return $array;
}
public function _bestAddress($address)
{
$array = $this->_sortAddresses($address);
return $array[0];
}
Now, to view the entire class with defined variables...
class gDistanceCompare
{
public $format = '';//ptm = miles, ptk = km
public $route = '';//h = avoid highways, t = avoid tolls
public $address_array = array();
public $error = array();
public function __construct($address_array)
{
/*
loops through the address array and checks format, setup array
('key'=>('address'=>'address','distance',''))
*/
foreach($address_array as $key => $row)
{
if($this->_checkAddress($row))
$this->address_array[$key] = array('address'=>$row,'distance'=>'');
else
$this->error[] = array('error','there is a problem with the address array');
}
}
public function _filterAddresses($address,$radius)
{
/*
takes an address and radius, finds distance from address array and gets sorted array
loops through and keeps addresses within radius
if none are found, returns error
*/
$array = $this->_sortAddresses($address);
foreach($array as $key => $row)
{
if($row['distance']>$radius)
{
$array = array_slice($array,0,$key);
break;
}
}
if(!$array)
$this->error[] = array('error','there was no addresses that are within the radius');
if($this->error)
return $this->error;
else
return $array;
}
public function _bestAddress($address)
{
/*
takes an address and finds closest match in address array
*/
unset($this->error);
$array = $this->_sortAddresses($address);
return $array[0];
}
public function _sortAddresses($address)
{
/*
takes an address, checks format
then loops through address array
addresses from the array are sorted and returned with distances
('key'=>('address'=>'address','distance'=>'distance'))
*/
if($this->_checkAddress($address))
{
foreach($this->address_array as $key => $row)
{
$to = $row['address'];
$from = $address;
$distance = $this->_requestDistance($to,$from);
$this->address_array[$key]['distance'] = $distance;
}
foreach($this->address_array as $key => $row)
$distance_array[$key] = $row['distance'];
asort($distance_array);
foreach($distance_array as $key => $row)
$array[] = $this->address_array[$key];
}
else
$this->error[] = array('error','there is a problem with this address: '.$address);
if($this->error)
return $this->error;
else
return $array;
}
private function _checkAddress($address)
{
/*
checks if an address is valid, optional for careful people
*/
return true;
}
private function _requestDistance($to,$from)
{
/*
takes two lat/lng and returns distance from the two points driving
*/
$url = 'http://maps.google.com/maps?saddr='.urlencode($from).'&daddr='.urlencode($to).'&oe=utf8&doflg='.$this->format.'&dirflg='.$this->route.'&output=kml';
$xml = simplexml_load_file($url,'SimpleXMLElement',LIBXML_NOCDATA);
for($i=0;$i
Document->Placemark);$i++) {
if($xml->Document->Placemark[$i]->name=='Route')
$description = $xml->Document->Placemark[$i]->description;
}
if($description)
return floatval(str_replace(',','',substr($description,strpos($description,'Distance: ')+10,strpos($description,' ')-(strpos($description,'Distance: ')+10))));
else
$this->error[] = array('error','google can not understand the address');
}
}
If you understand classes, this should be fairly straightforward. I even added some toys, including setting miles/km and route type. In my next post, I'll show another class that extends this one to fulfill the client's needs.
Comments (3)