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.

  1. public function __construct($address_array)

  2. {

  3. foreach($address_array as $key => $row)

  4. {

  5. if($this->_checkAddress($row))

  6. $this->address_array[$key] = array('address'=>$row,'distance'=>'');

  7. else

  8. $this->error[] = array('error','there is a problem with the address array');

  9. }

  10. }

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.

  1. public function _sortAddresses($address)

  2. {

  3. if($this->_checkAddress($address))

  4. {

  5. foreach($this->address_array as $key => $row)

  6. {

  7. $to = $row['address'];

  8. $from = $address;

  9. $distance = $this->_requestDistance($to,$from);

  10. $this->address_array[$key]['distance'] = $distance;

  11. }

  12. foreach($this->address_array as $key => $row)

  13. $distance_array[$key] = $row['distance'];

  14. asort($distance_array);

  15. foreach($distance_array as $key => $row)

  16. $array[] = $this->address_array[$key];

  17. }

  18. else

  19. $this->error[] = array('error','there is a problem with this address: '.$address);

  20. if($this->error)

  21. return $this->error;

  22. else

  23. return $array;

  24. }

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...

  1. private function _requestDistance($to,$from)

  2. {

  3. $url = 'http://maps.google.com/maps?saddr='.urlencode($from).'&daddr='.urlencode($to).'&oe=utf8&doflg='.$this->format.'&dirflg='.$this->route.'&output=kml';

  4. $xml = simplexml_load_file($url,'SimpleXMLElement',LIBXML_NOCDATA);

  5. for($i=0;$iDocument->Placemark);$i++)

  6. {

  7. if($xml->Document->Placemark[$i]->name=='Route')

  8. $description = $xml->Document->Placemark[$i]->description;

  9. }

  10. if($description)

  11. return floatval(str_replace(',','',substr($description,strpos($description,'Distance: ')+10,strpos($description,' ')-(strpos($description,'Distance: ')+10))));

  12. else

  13. $this->error[] = array('error','google can not understand the address');

  14. }

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.

  1. public function _filterAddresses($address,$radius)

  2. {

  3. $array = $this->_sortAddresses($address);

  4. foreach($array as $key => $row)

  5. {

  6. if($row['distance']>$radius)

  7. {

  8. $array = array_slice($array,0,$key);

  9. break;

  10. }

  11. }

  12. if(!$array)

  13. $this->error[] = array('error','there was no addresses that are within the radius');

  14. if($this->error)

  15. return $this->error;

  16. else

  17. return $array;

  18. }

  19. public function _bestAddress($address)

  20. {

  21. $array = $this->_sortAddresses($address);

  22. return $array[0];

  23. }

Now, to view the entire class with defined variables...

  1. class gDistanceCompare

  2. {

  3. public $format = '';//ptm = miles, ptk = km

  4. public $route = '';//h = avoid highways, t = avoid tolls

  5. public $address_array = array();

  6. public $error = array();

  7. public function __construct($address_array)

  8. {

  9. /*

  10. loops through the address array and checks format, setup array

  11. ('key'=>('address'=>'address','distance',''))

  12. */

  13. foreach($address_array as $key => $row)

  14. {

  15. if($this->_checkAddress($row))

  16. $this->address_array[$key] = array('address'=>$row,'distance'=>'');

  17. else

  18. $this->error[] = array('error','there is a problem with the address array');

  19. }

  20. }

  21. public function _filterAddresses($address,$radius)

  22. {

  23. /*

  24. takes an address and radius, finds distance from address array and gets sorted array

  25. loops through and keeps addresses within radius

  26. if none are found, returns error

  27. */

  28. $array = $this->_sortAddresses($address);

  29. foreach($array as $key => $row)

  30. {

  31. if($row['distance']>$radius)

  32. {

  33. $array = array_slice($array,0,$key);

  34. break;

  35. }

  36. }

  37. if(!$array)

  38. $this->error[] = array('error','there was no addresses that are within the radius');

  39. if($this->error)

  40. return $this->error;

  41. else

  42. return $array;

  43. }

  44. public function _bestAddress($address)

  45. {

  46. /*

  47. takes an address and finds closest match in address array

  48. */

  49. unset($this->error);

  50. $array = $this->_sortAddresses($address);

  51. return $array[0];

  52. }

  53. public function _sortAddresses($address)

  54. {

  55. /*

  56. takes an address, checks format

  57. then loops through address array

  58. addresses from the array are sorted and returned with distances

  59. ('key'=>('address'=>'address','distance'=>'distance'))

  60. */

  61. if($this->_checkAddress($address))

  62. {

  63. foreach($this->address_array as $key => $row)

  64. {

  65. $to = $row['address'];

  66. $from = $address;

  67. $distance = $this->_requestDistance($to,$from);

  68. $this->address_array[$key]['distance'] = $distance;

  69. }

  70. foreach($this->address_array as $key => $row)

  71. $distance_array[$key] = $row['distance'];

  72. asort($distance_array);

  73. foreach($distance_array as $key => $row)

  74. $array[] = $this->address_array[$key];

  75. }

  76. else

  77. $this->error[] = array('error','there is a problem with this address: '.$address);

  78. if($this->error)

  79. return $this->error;

  80. else

  81. return $array;

  82. }

  83. private function _checkAddress($address)

  84. {

  85. /*

  86. checks if an address is valid, optional for careful people

  87. */

  88. return true;

  89. }

  90. private function _requestDistance($to,$from)

  91. {

  92. /*

  93. takes two lat/lng and returns distance from the two points driving

  94. */

  95. $url = 'http://maps.google.com/maps?saddr='.urlencode($from).'&daddr='.urlencode($to).'&oe=utf8&doflg='.$this->format.'&dirflg='.$this->route.'&output=kml';

  96. $xml = simplexml_load_file($url,'SimpleXMLElement',LIBXML_NOCDATA);

  97. for($i=0;$iDocument->Placemark);$i++)

  98. {

  99. if($xml->Document->Placemark[$i]->name=='Route')

  100. $description = $xml->Document->Placemark[$i]->description;

  101. }

  102. if($description)

  103. return floatval(str_replace(',','',substr($description,strpos($description,'Distance: ')+10,strpos($description,' ')-(strpos($description,'Distance: ')+10))));

  104. else

  105. $this->error[] = array('error','google can not understand the address');

  106. }

  107. }

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.