├── models ├── behaviors │ └── geocodable.php └── geo_address.php ├── tests ├── cases │ └── behaviors │ │ └── geocodable.test.php └── fixtures │ ├── address_fixture.php │ ├── city_fixture.php │ ├── country_fixture.php │ ├── geo_address_fixture.php │ └── state_fixture.php └── views └── helpers └── geomap.php /models/behaviors/geocodable.php: -------------------------------------------------------------------------------- 1 | 'google', 19 | 'key' => null, 20 | 'fields' => array( 21 | 'hash' => false, 22 | 'address', 23 | 'latitude', 24 | 'longitude', 25 | 'address1', 26 | 'address2', 27 | 'city', 28 | 'state', 29 | 'zip', 30 | 'country' 31 | ), 32 | 'addressFields' => array( 33 | 'address1' => array('addr', 'address_1'), 34 | 'address2' => array('addr2', 'address_2'), 35 | 'city', 36 | 'state', 37 | 'zip' => array('zipcode', 'zip_code', 'postal_code'), 38 | 'country' 39 | ), 40 | 'models' => array() 41 | ); 42 | 43 | /** 44 | * Service information 45 | * 46 | * @var array 47 | */ 48 | protected $services = array( 49 | 'google' => array( 50 | 'url' => 'http://maps.google.com/maps/geo?q=${address}&output=csv&key=${key}', 51 | 'format' => '${address1} ${address2}, ${city}, ${zip} ${state}, ${country}', 52 | 'pattern' => '/200,[^,]+,([^,]+),([^,\s]+)/', 53 | 'matches' => array( 54 | 'latitude' => 1, 55 | 'longitude' => 2 56 | ) 57 | ), 58 | 'yahoo' => array( 59 | 'url' => 'http://api.local.yahoo.com/MapsService/V1/geocode?appid=${key}&location=${address}', 60 | 'format' => '${address1} ${address2}, ${city}, ${zip} ${state}, ${country}', 61 | 'pattern' => '/(.*?)<\/Latitude>(.*?)<\/Longitude>/U', 62 | 'matches' => array( 63 | 'latitude' => 1, 64 | 'longitude' => 2 65 | ) 66 | ) 67 | ); 68 | 69 | /** 70 | * HttpSocket instance 71 | * 72 | * @var object 73 | */ 74 | protected $socket; 75 | 76 | /** 77 | * Units relative to 1 kilometer. 78 | * k: kilometers, m: miles, f: feet, i: inches, n: nautical miles 79 | * 80 | * @var array 81 | */ 82 | protected $units = array('k' => 1, 'm' => 0.621371192, 'f' => 3280.8399, 'i' => 39370.0787, 'n' => 0.539956803); 83 | 84 | /** 85 | * Setup behavior 86 | * 87 | * @param object $model Model 88 | * @param array $settings Settings 89 | */ 90 | public function setup($model, $settings = array()) { 91 | if (!isset($this->settings[$model->alias])) { 92 | $configured = Configure::read('Geocode'); 93 | if (!empty($configured)) { 94 | foreach($this->default as $key => $value) { 95 | if (isset($configured[$key])) { 96 | $this->default[$key] = $configured[$key]; 97 | } 98 | } 99 | } 100 | $this->settings[$model->alias] = $this->default; 101 | } 102 | 103 | if (!empty($settings['models'])) { 104 | foreach($settings['models'] as $field => $data) { 105 | unset($settings['models'][$field]); 106 | if (is_numeric($field) && !is_array($data)) { 107 | $field = $data; 108 | $data = array('model' => Inflector::classify($field)); 109 | } else if (is_numeric($field) && !empty($data['field'])) { 110 | $field = $data['field']; 111 | } 112 | 113 | if (!is_array($data)) { 114 | $data = array('model' => $data); 115 | } 116 | 117 | if (empty($data['model'])) { 118 | continue; 119 | } 120 | 121 | if (empty($data['referenceField'])) { 122 | $modelName = $data['model']; 123 | if (strpos($data['model'], '.') !== false) { 124 | list($modelName, $childModelName) = explode('.', $data['model']); 125 | } 126 | 127 | $data['referenceField'] = Inflector::underscore($modelName) . '_id'; 128 | } 129 | 130 | $settings['models'][$field] = array_merge(array('field' => 'name'), $data); 131 | } 132 | } 133 | 134 | $settings = Set::merge($this->settings[$model->alias], $settings); 135 | 136 | if (empty($this->services[strtolower($settings['service'])])) { 137 | trigger_error(sprintf(__('Geocode service %s not implemented', true), $settings['service']), E_USER_WARNING); 138 | return false; 139 | } 140 | 141 | if (!isset($this->socket)) { 142 | $this->socket = new HttpSocket(); 143 | } 144 | 145 | foreach(array('fields', 'addressFields') as $parameter) { 146 | $fields = array(); 147 | foreach($settings[$parameter] as $i => $field) { 148 | $fields[is_numeric($i) ? $field : $i] = ($parameter != 'fields' || $model->hasField($field) ? $field : false); 149 | } 150 | $settings[$parameter] = $fields; 151 | } 152 | 153 | $this->settings[$model->alias] = $settings; 154 | } 155 | 156 | /** 157 | * Before save callback 158 | * 159 | * @param object $model Model using this behavior 160 | * @return bool true if the operation should continue, false if it should abort 161 | */ 162 | public function beforeSave($model) { 163 | $settings = $this->settings[$model->alias]; 164 | $latitudeField = $settings['fields']['latitude']; 165 | $longitudeField = $settings['fields']['longitude']; 166 | 167 | if ( 168 | !empty($latitudeField) && !empty($longitudeField) && 169 | !isset($model->data[$model->alias][$latitudeField]) && !isset($model->data[$model->alias][$longitudeField]) 170 | ) { 171 | $data = $model->data[$model->alias]; 172 | if (!empty($settings['models'])) { 173 | $data = $this->_data($settings['models'], $data); 174 | } 175 | $geocode = $this->geocode($model, $data, false); 176 | if (!empty($geocode)) { 177 | $address = array(); 178 | list($address[$latitudeField], $address[$longitudeField]) = $geocode; 179 | if (!empty($settings['fields']['address'])) { 180 | $address[$settings['fields']['address']] = $this->_address($settings, $data); 181 | } 182 | 183 | $model->data[$model->alias] = array_merge( 184 | $model->data[$model->alias], 185 | $address 186 | ); 187 | 188 | $this->_addToWhitelist($model, array_keys($address)); 189 | } 190 | } 191 | 192 | return parent::beforeSave($model); 193 | } 194 | 195 | /** 196 | * Calculate geocode for given address, getting it from DB if already calculated 197 | * 198 | * @param object $model 199 | * @param mixed $address Array with address info (address, city, etc.) or full address as string 200 | * @param bool $save Set to true to save result in model, false otherwise 201 | * @return mixed Array (latitude, longitude), or false if error 202 | */ 203 | public function geocode($model, $address, $save = true) { 204 | $settings = $this->settings[$model->alias]; 205 | $fullAddress = $this->_address($settings, $address); 206 | if (empty($fullAddress)) { 207 | return false; 208 | } 209 | 210 | $data = array($model->alias => array()); 211 | $conditions = array(); 212 | 213 | if (!empty($settings['fields']['hash'])) { 214 | $hash = Security::hash($fullAddress); 215 | $conditions[$model->alias . '.' . $settings['fields']['hash']] = $hash; 216 | $data[$model->alias][$settings['fields']['hash']] = $hash; 217 | } else if (!empty($settings['fields']['address'])) { 218 | $conditions[$model->alias . '.' . $settings['fields']['address']] = $fullAddress; 219 | } 220 | 221 | if (!empty($settings['fields']['address'])) { 222 | $data[$model->alias][$settings['fields']['address']] = $fullAddress; 223 | } 224 | 225 | if (is_array($address)) { 226 | foreach(array_intersect_key($address, $settings['fields']) as $field => $value) { 227 | if (empty($settings['fields']['hash']) && empty($settings['fields']['address'])) { 228 | $conditions[$model->alias . '.' . $field] = $value; 229 | } 230 | $data[$model->alias][$field] = $value; 231 | } 232 | } 233 | 234 | if (empty($settings['fields']['latitude']) || empty($settings['fields']['longitude'])) { 235 | $conditions = null; 236 | $data = null; 237 | } 238 | 239 | $coordinates = false; 240 | if (!empty($conditions)) { 241 | $coordinates = $model->find('first', array( 242 | 'conditions' => $conditions, 243 | 'recursive' => -1, 244 | 'fields' => array($settings['fields']['latitude'], $settings['fields']['longitude']) 245 | )); 246 | if (!empty($coordinates)) { 247 | $coordinates = array( 248 | $coordinates[$model->alias][$settings['fields']['latitude']], 249 | $coordinates[$model->alias][$settings['fields']['longitude']], 250 | ); 251 | } 252 | } 253 | 254 | if (empty($coordinates) && empty($settings['key'])) { 255 | trigger_error(__('Address not found in model and no API key was provided', true), E_USER_WARNING); 256 | return false; 257 | } 258 | 259 | if (empty($coordinates)) { 260 | $coordinates = $this->_fetchCoordinates($settings, $fullAddress); 261 | } 262 | 263 | if (!empty($coordinates)) { 264 | foreach($coordinates as $i => $coordinate) { 265 | $coordinates[$i] = floatval($coordinate); 266 | } 267 | } 268 | 269 | if ($save && !empty($coordinates) && !empty($data)) { 270 | $data[$model->alias][$settings['fields']['latitude']] = $coordinates[0]; 271 | $data[$model->alias][$settings['fields']['longitude']] = $coordinates[1]; 272 | 273 | if (!empty($data[$model->alias][$settings['fields']['state']])) { 274 | $model->create(); 275 | $model->save($data); 276 | } 277 | } 278 | 279 | return $coordinates; 280 | } 281 | 282 | /** 283 | * Find points near given point for already saved records 284 | * 285 | * @param object $model Model 286 | * @param string $type Find type (first / all / etc.) 287 | * @param mixed $origin A point (latitude, longitude), a full address string, or an array of address data 288 | * @param float $distance Set to a maximum distance if you wish to limit results 289 | * @param string $unit Unit (k: kilometers, m: miles, f: feet, i: inches, n: nautical miles) 290 | * @param array $query Query settings (as given to normal find operations) to override 291 | * @return mixed Results 292 | */ 293 | public function near($model, $type, $origin, $distance = null, $unit = 'k', $query = array()) { 294 | $settings = $this->settings[$model->alias]; 295 | list($latitudeField, $longitudeField) = array( 296 | $settings['fields']['latitude'], 297 | $settings['fields']['longitude'], 298 | ); 299 | 300 | if (!empty($query['fields'])) { 301 | $query['fields'] = array_merge($query['fields'], array( 302 | $latitudeField, 303 | $longitudeField 304 | )); 305 | } 306 | 307 | $point = null; 308 | if (is_array($origin) && count($origin) == 2 && isset($origin[0]) && isset($origin[1]) && is_numeric($origin[0]) && is_numeric($origin[1])) { 309 | $point = $origin; 310 | } else { 311 | $point = $this->geocode($model, $origin); 312 | } 313 | 314 | if (empty($point)) { 315 | return false; 316 | } 317 | 318 | if (empty($query['order'])) { 319 | unset($query['order']); 320 | } 321 | 322 | $query = Set::merge( 323 | $this->distanceQuery($model, $point, $distance, $unit, !empty($query['direction']) ? $query['direction'] : 'ASC'), 324 | array_diff_key($query, array('direction'=>true)) 325 | ); 326 | 327 | if ($type == 'count' && !empty($query['order'])) { 328 | unset($query['order']); 329 | } 330 | $result = $model->find($type, $query); 331 | 332 | if (!empty($result) && $type != 'count') { 333 | $result = $this->_loadDistance($model, $result, $point, $unit, $model->alias, $latitudeField, $longitudeField); 334 | } 335 | 336 | return $result; 337 | } 338 | 339 | /** 340 | * Calculate distance (in given unit) between two given points, each of them 341 | * expressed as latitude, longitude. Uses the haversine formula. 342 | * 343 | * @param object $model Model 344 | * @param mixed $origin Starting point (latitude, longitude), expressed in numeric degrees, a full address string, or array with address data 345 | * @param mixed $destination Ending point (latitude, longitude), expressed in numeric degrees, a full address string, or array with address data 346 | * @param string $unit Unit (k: kilometers, m: miles, f: feet, i: inches, n: nautical miles) 347 | * @return float Distance expressed in given unit 348 | */ 349 | public function distance($model, $origin, $destination, $unit = 'k') { 350 | $unit = (!empty($unit) && array_key_exists(strtolower($unit), $this->units) ? $unit : 'k'); 351 | $point1 = null; 352 | $point2 = null; 353 | 354 | foreach(array('point1'=>'origin', 'point2'=>'destination') as $var => $parameter) { 355 | $data = $$parameter; 356 | if (is_array($data) && count($data) == 2 && isset($data[0]) && isset($data[1]) && is_numeric($data[0]) && is_numeric($data[1])) { 357 | $$var = $data; 358 | } else { 359 | $$var = $this->geocode($model, $data); 360 | } 361 | } 362 | 363 | if (empty($point1) || empty($point2)) { 364 | return false; 365 | } 366 | 367 | $line = array( 368 | deg2rad($point2[0] - $point1[0]), 369 | deg2rad($point2[1] - $point1[1]) 370 | ); 371 | $angle = sin($line[0]/2) * sin($line[0]/2) + sin($line[1]/2) * sin($line[1]/2) * cos(deg2rad($point1[0])) * cos(deg2rad($point2[0])); 372 | $earthRadiusKm = 6378; 373 | return ($earthRadiusKm * 2 * atan2(sqrt($angle), sqrt(1 - $angle))) * $this->units[strtolower($unit)]; 374 | } 375 | 376 | /** 377 | * Give back needed condition / ordering clause to find points near given point 378 | * 379 | * @param object $model Model 380 | * @param array $point Point (latitude, longitude), expressed in numeric degrees 381 | * @param float $distance If specified, add condition to only match points within given distance 382 | * @param string $unit Unit (k: kilometers, m: miles, f: feet, i: inches, n: nautical miles) 383 | * @param string $direction Sorting direction (ASC / DESC) 384 | * @return array Query parameters (conditions, order) 385 | */ 386 | public function distanceQuery($model, $point, $distance = null, $unit = 'k', $direction = 'ASC') { 387 | $unit = (!empty($unit) && array_key_exists(strtolower($unit), $this->units) ? $unit : 'k'); 388 | $settings = $this->settings[$model->alias]; 389 | foreach($point as $k => $v) { 390 | $point[$k] = floatval($v); 391 | } 392 | list($latitude, $longitude) = $point; 393 | list($latitudeField, $longitudeField) = array( 394 | $model->escapeField($settings['fields']['latitude']), 395 | $model->escapeField($settings['fields']['longitude']), 396 | ); 397 | 398 | $expression = 'SQRT( 399 | POW((COS(RADIANS(' . $latitude . ')) * COS(RADIANS(' . $longitude . ')) 400 | - COS(RADIANS(' . $latitudeField . ')) * COS(RADIANS(' . $longitudeField . '))), 2) + 401 | POW((COS(RADIANS(' . $latitude . ')) * SIN(RADIANS(' . $longitude . ')) 402 | - COS(RADIANS(' . $latitudeField . ')) * SIN(RADIANS(' . $longitudeField . '))), 2) + 403 | POW((SIN(RADIANS(' . $latitude . ')) 404 | - SIN(RADIANS(' . $latitudeField . '))), 2) 405 | )'; 406 | 407 | $expression = str_replace("\n", ' ', $expression); 408 | $query = array( 409 | 'order' => $expression . ' ' . $direction, 410 | 'conditions' => array( 411 | 'ROUND(' . $latitudeField . ', 4) !=' => round($latitude, 4), 412 | 'ROUND(' . $longitudeField . ', 4) !=' => round($longitude, 4) 413 | ) 414 | ); 415 | 416 | if (!empty($distance)) { 417 | $earthRadiusKm = 6378; 418 | $ratio = $earthRadiusKm * $this->units[strtolower($unit)]; 419 | $query['conditions'][] = '(' . $expression . ' * ' . $ratio . ') <= ' . $distance; 420 | } 421 | 422 | return $query; 423 | } 424 | 425 | /** 426 | * Navigate result rows and calculate distance 427 | * 428 | * @param object $model Model 429 | * @param array $result Result rows 430 | * @param array $point Point (latitude, longitude), expressed in numeric degrees 431 | * @param string $unit Unit (k: kilometers, m: miles, f: feet, i: inches, n: nautical miles) 432 | * $param string $alias Location model alias 433 | * @param string $latitudeField Name of latitude field 434 | * @param string $longitudeField Name of longitude field 435 | * @return array Modified results 436 | */ 437 | protected function _loadDistance($model, $result, $point, $unit, $alias, $latitudeField, $longitudeField) { 438 | if (!is_array($result)) { 439 | return $result; 440 | } else if (!empty($result[$alias])) { 441 | $result[$alias]['distance'] = $this->distance($model, 442 | array($result[$alias][$latitudeField], $result[$alias][$longitudeField]), 443 | $point, 444 | $unit 445 | ); 446 | } else { 447 | foreach($result as $i => $row) { 448 | $result[$i] = $this->_loadDistance($model, $row, $point, $unit, $alias, $latitudeField, $longitudeField); 449 | } 450 | } 451 | 452 | return $result; 453 | } 454 | 455 | /** 456 | * Query a service to get coordinates for given address 457 | * 458 | * @param array $settings Settings 459 | * @param string $address Full address 460 | * @return array Coordinates (latitude, longitude), expressed in numeric degrees 461 | */ 462 | protected function _fetchCoordinates($settings, $address) { 463 | $vars = array( 464 | '${key}' => $settings['key'], 465 | '${address}' => $address 466 | ); 467 | $service = $this->services[$settings['service']]; 468 | 469 | foreach($vars as $var => $value) { 470 | $vars[$var] = urlencode($value); 471 | } 472 | 473 | $url = str_replace(array_keys($vars), $vars, $service['url']); 474 | $result = $this->socket->get($url); 475 | 476 | if (empty($result) || !preg_match($service['pattern'], $result, $matches)) { 477 | return false; 478 | } 479 | 480 | $coordinates = array( 481 | $matches[$service['matches']['latitude']], 482 | $matches[$service['matches']['longitude']] 483 | ); 484 | 485 | return $coordinates; 486 | } 487 | 488 | /** 489 | * Build full address from given address 490 | * 491 | * @param array $settings Settings 492 | * @param mixed $address If array, will look for normal address parameters (address, city, etc.) 493 | * @return string Full address 494 | */ 495 | protected function _address($settings, $address) { 496 | if (is_array($address)) { 497 | $elements = array(); 498 | foreach($settings['addressFields'] as $type => $fields) { 499 | $fields = array_merge(array($type => $type), (array) $fields); 500 | $elements['${' . $type . '}'] = str_replace(',', ' ', trim(current(array_intersect_key($address, array_flip($fields))))); 501 | } 502 | $nonEmpty = array_filter($elements); 503 | if (empty($nonEmpty)) { 504 | return null; 505 | } 506 | 507 | $address = trim(str_replace(array_keys($elements), $elements, $this->services[$settings['service']]['format'])); 508 | $replacements = array( 509 | '/(\s)\s+/' => '\\1', 510 | '/\s+,(.+)/' => ',\\1', 511 | '/\s*,\s*,/' => ',', 512 | '/,\s*$/' => '' 513 | 514 | ); 515 | foreach($replacements as $pattern => $replacement) { 516 | $address = preg_replace($pattern, $replacement, $address); 517 | } 518 | $address = preg_replace('/,\s*$/', '', $address); 519 | } 520 | 521 | return $address; 522 | } 523 | 524 | /** 525 | * Adds missing address information based on specified settings. 526 | * E.g: 'city' => array('model' => 'City', 'referenceField' => 'city_id', 'field' => 'name') 527 | * 528 | * @param array $models How to get missing info 529 | * @param array $data Current model data 530 | * @return array Modified model data 531 | */ 532 | protected function _data($models, $data) { 533 | foreach($models as $field => $model) { 534 | if (!empty($data[$field])) { 535 | continue; 536 | } 537 | 538 | $modelName = $model['model']; 539 | $childModelName = null; 540 | if (strpos($model['model'], '.') !== false) { 541 | list($modelName, $childModelName) = explode('.', $model['model']); 542 | } 543 | $varName = 'Model' . $modelName; 544 | if (empty($$varName)) { 545 | $$varName = ClassRegistry::init($modelName); 546 | if (empty($$varName)) { 547 | continue; 548 | } 549 | } 550 | 551 | if (empty($data[$model['referenceField']])) { 552 | continue; 553 | } 554 | 555 | $record = $$varName->find('first', array( 556 | 'conditions' => array($$varName->alias . '.' . $$varName->primaryKey => $data[$model['referenceField']]), 557 | 'contain' => !empty($childModelName) ? preg_replace('/^[^\.]+\.(.+)$/', '\\1', $model['model']) : false 558 | )); 559 | if (empty($record)) { 560 | continue; 561 | } 562 | 563 | $data[$field] = $record[!empty($childModelName) ? $childModelName : $modelName][$model['field']]; 564 | } 565 | 566 | return $data; 567 | } 568 | } 569 | 570 | ?> 571 | -------------------------------------------------------------------------------- /models/geo_address.php: -------------------------------------------------------------------------------- 1 | _findMethods, array('near'=>true)); 21 | $findType = (is_string($conditions) && $conditions != 'count' && array_key_exists($conditions, $findMethods) ? $conditions : null); 22 | if (empty($findType) && is_string($conditions) && $conditions == 'count' && !empty($fields['type']) && array_key_exists($fields['type'], $findMethods)) { 23 | $findType = $fields['type']; 24 | unset($fields['type']); 25 | } 26 | 27 | if ($findType == 'near' && $this->Behaviors->enabled('Geocodable')) { 28 | $type = ($conditions == 'near' ? 'all' : $conditions); 29 | $query = $fields; 30 | if (!empty($query['address'])) { 31 | foreach(array('address', 'unit', 'distance') as $field) { 32 | $$field = isset($query[$field]) ? $query[$field] : null; 33 | unset($query[$field]); 34 | } 35 | return $this->near($type, $address, $distance, $unit, $query); 36 | } 37 | } 38 | return parent::find($conditions, $fields, $order, $recursive); 39 | } 40 | } 41 | ?> -------------------------------------------------------------------------------- /tests/cases/behaviors/geocodable.test.php: -------------------------------------------------------------------------------- 1 | array()); 16 | public $useTable = 'geo_addresses'; 17 | } 18 | 19 | class City extends AppModel { 20 | public $useDbConfig = 'test_suite'; 21 | public $belongsTo = array('State'); 22 | } 23 | 24 | class State extends AppModel { 25 | public $useDbConfig = 'test_suite'; 26 | public $belongsTo = array('Country'); 27 | public $hasMany = array('City'); 28 | } 29 | 30 | class Country extends AppModel { 31 | public $useDbConfig = 'test_suite'; 32 | } 33 | 34 | class TestExtendedAddress extends GeoAddress { 35 | public $useDbConfig = 'test_suite'; 36 | public $belongsTo = array('City', 'State'); 37 | public $actsAs = array('Geocode.Geocodable'=>array( 38 | 'models' => array( 39 | 'city' => 'City', 40 | 'state' => 'State', 41 | 'country' => 'State.Country' 42 | ) 43 | )); 44 | public $useTable = 'addresses'; 45 | } 46 | 47 | class GeocodableBehaviorTest extends CakeTestCase { 48 | public $fixtures = array( 49 | 'plugin.geocode.geo_address', 'plugin.geocode.address', 'plugin.geocode.city', 'plugin.geocode.state', 'plugin.geocode.country' 50 | 51 | ); 52 | 53 | public function startTest($method) { 54 | $this->Address = new TestAddress(); 55 | $this->Address->Behaviors->attach('Geocode.Geocodable', $this->Address->actsAs['Geocode.Geocodable']); 56 | 57 | $this->ExtendedAddress = new TestExtendedAddress(); 58 | $this->ExtendedAddress->Behaviors->attach('Geocode.Geocodable', $this->Address->actsAs['Geocode.Geocodable']); 59 | 60 | $this->Geocodable = $this->Address->Behaviors->Geocodable; 61 | $this->TestGeocodable = new TestGeocodableBehavior(); 62 | $this->configuredGeocode = Configure::read('Geocode'); 63 | } 64 | 65 | public function endTest($method) { 66 | unset($this->Geocodable); 67 | unset($this->Address); 68 | unset($this->TestGeocodable); 69 | Configure::delete('Geocode'); 70 | ClassRegistry::flush(); 71 | if (!empty($this->configuredGeocode)) { 72 | Configure::write('Geocode', $this->configuredGeocode); 73 | } 74 | } 75 | 76 | public function testSettings() { 77 | $default = $this->Geocodable->default; 78 | $this->assertTrue(!empty($default)); 79 | 80 | $this->Address->Behaviors->detach('Geocodable'); 81 | Configure::write('Geocode.service', 'yahoo'); 82 | $this->Address->Behaviors->attach('Geocodable', $this->Address->actsAs['Geocode.Geocodable']); 83 | $this->assertEqual($this->Geocodable->settings[$this->Address->alias]['service'], 'yahoo'); 84 | 85 | $this->Address->Behaviors->detach('Geocodable'); 86 | Configure::write('Geocode.service', 'nonexisting'); 87 | $this->expectError(); 88 | $this->Address->Behaviors->attach('Geocodable', array_diff_key($default, array('service'=>true))); 89 | } 90 | 91 | public function testAddress() { 92 | $result = $this->TestGeocodable->run('_address', $this->Geocodable->settings[$this->Address->alias], array( 93 | 'address1' => '1209 La Brad Lane', 94 | 'city' => 'Tampa', 95 | 'state' => 'FL' 96 | )); 97 | $expected = '1209 La Brad Lane, Tampa, FL'; 98 | $this->assertEqual($result, $expected); 99 | 100 | $result = $this->TestGeocodable->run('_address', $this->Geocodable->settings[$this->Address->alias], array( 101 | 'address1' => '1209 La Brad Lane', 102 | 'address_2' => 'Suite 4', 103 | 'city' => 'Tampa', 104 | )); 105 | $expected = '1209 La Brad Lane Suite 4, Tampa'; 106 | $this->assertEqual($result, $expected); 107 | 108 | $result = $this->TestGeocodable->run('_address', $this->Geocodable->settings[$this->Address->alias], array( 109 | 'address1' => '1209 La Brad Lane', 110 | 'city' => 'Tampa', 111 | 'state' => 'FL', 112 | 'country' => 'USA' 113 | )); 114 | $expected = '1209 La Brad Lane, Tampa, FL, USA'; 115 | $this->assertEqual($result, $expected); 116 | 117 | $result = $this->TestGeocodable->run('_address', $this->Geocodable->settings[$this->Address->alias], array( 118 | 'addr' => '1209 La Brad Lane', 119 | 'state' => 'FL', 120 | )); 121 | $expected = '1209 La Brad Lane, FL'; 122 | $this->assertEqual($result, $expected); 123 | 124 | $result = $this->TestGeocodable->run('_address', $this->Geocodable->settings[$this->Address->alias], array( 125 | 'address1' => '14348 N Rome Ave', 126 | 'city' => 'Tampa', 127 | 'state' => 'Florida', 128 | 'zip' => 33613 129 | )); 130 | $expected = '14348 N Rome Ave, Tampa, 33613 Florida'; 131 | $this->assertEqual($result, $expected); 132 | } 133 | 134 | public function testGeocodeNoApiKey() { 135 | $key = $this->Geocodable->settings[$this->Address->alias]['key']; 136 | $this->Geocodable->settings[$this->Address->alias]['key'] = null; 137 | 138 | $result = $this->Address->geocode(array( 139 | 'address1' => '1209 La Brad Lane', 140 | 'city' => 'Tampa', 141 | 'state' => 'FL' 142 | )); 143 | $expected = array(28.0792, -82.4735); 144 | $this->assertEqual($result, $expected); 145 | 146 | $this->expectError(); 147 | $result = $this->Address->geocode(array( 148 | 'address1' => '1600 Amphitheatre Parkway', 149 | 'city' => 'Mountan View', 150 | 'state' => 'CA', 151 | 'zip' => 94043, 152 | 'country' => 'United States of America' 153 | )); 154 | $this->assertFalse($result); 155 | 156 | $this->Geocodable->settings[$this->Address->alias]['key'] = $key; 157 | } 158 | 159 | public function testGeocode() { 160 | if ($this->skipIf(empty($this->Geocodable->settings[$this->Address->alias]['key']), 'No service API Key provided')) { 161 | return; 162 | } 163 | 164 | $address = array( 165 | 'address1' => '1600 Amphitheatre Parkway', 166 | 'city' => 'Mountan View', 167 | 'state' => 'CA', 168 | 'zip' => 94043, 169 | 'country' => 'United States of America' 170 | ); 171 | $result = $this->Address->find('first', array( 172 | 'conditions' => $address, 173 | 'recursive' => -1, 174 | 'fields' => array_keys($address) 175 | )); 176 | $this->assertFalse($result); 177 | 178 | $result = $this->Address->geocode($address); 179 | $this->assertTrue(!empty($result)); 180 | if (!empty($result)) { 181 | foreach($result as $i => $value) { 182 | $result[$i] = round($value, 1); 183 | } 184 | $expected = array(37.4, -122.1); 185 | $this->assertEqual($result, $expected); 186 | } 187 | 188 | $result = $this->Address->find('first', array( 189 | 'conditions' => $address, 190 | 'recursive' => -1, 191 | 'fields' => array_merge( 192 | array_keys($address), 193 | array('latitude', 'longitude') 194 | ) 195 | )); 196 | $this->assertTrue(!empty($result[$this->Address->alias])); 197 | if (!empty($result)) { 198 | foreach(array('latitude', 'longitude') as $field) { 199 | $result[$this->Address->alias][$field] = round($result[$this->Address->alias][$field], 1); 200 | } 201 | $expected = array($this->Address->alias => array_merge( 202 | $address, 203 | array('latitude' => 37.4, 'longitude' => -122.1) 204 | )); 205 | $this->assertEqual($result, $expected); 206 | } 207 | } 208 | 209 | public function testDistance() { 210 | $result = ceil($this->Address->distance( 211 | array(25.7953, -80.2789), 212 | array(9.9981, -84.2036) 213 | )); 214 | $expected = 1807; 215 | $this->assertEqual($result, $expected); 216 | 217 | $result = ceil($this->Address->distance( 218 | array(25.7953, -80.2789), 219 | array(9.9981, -84.2036), 220 | 'm' 221 | )); 222 | $expected = 1123; 223 | $this->assertEqual($result, $expected); 224 | 225 | $origin = array( 226 | 'address1' => '1209 La Brad Lane', 227 | 'city' => 'Tampa', 228 | 'state' => 'FL' 229 | ); 230 | 231 | $result = round($this->Address->distance($origin, array( 232 | 'address1' => '14348 N Rome Ave', 233 | 'city' => 'Tampa', 234 | 'state' => 'FL', 235 | 'zip' => 33613 236 | ), 'm'), 1); 237 | $expected = 0.2; 238 | $this->assertEqual($result, $expected); 239 | 240 | $result = round($this->Address->distance($origin, array( 241 | 'address1' => '1180 Magdalene Hill', 242 | 'state' => 'Florida', 243 | 'country' => 'US' 244 | ), 'm'), 1); 245 | $expected = 0.3; 246 | $this->assertEqual($result, $expected); 247 | 248 | $result = round($this->Address->distance($origin, '13216 Forest Hills Dr, Tampa, FL', 'm'), 1); 249 | $expected = 0.8; 250 | $this->assertEqual($result, $expected); 251 | 252 | $result = round($this->Address->distance($origin, array( 253 | 'address1' => '9106 El Portal Dr', 254 | 'city' => 'Tampa', 255 | 'state' => 'Florida', 256 | 'country' => 'US' 257 | ), 'm'), 1); 258 | $expected = 3.3; 259 | $this->assertEqual($result, $expected); 260 | 261 | if (!$this->skipIf(empty($this->Geocodable->settings[$this->Address->alias]['key']), 'No service API Key provided for test')) { 262 | $result = round($this->Address->distance($origin, '3700 Rocinante Blvd, Tampa, FL', 'm'), 1); 263 | $expected = 9; 264 | $this->assertEqual($result, $expected); 265 | } 266 | } 267 | 268 | public function testNear() { 269 | $result = $this->Address->near('first', array(25.7953, -80.2789)); 270 | $expected = 331; 271 | $this->assertTrue(!empty($result[$this->Address->alias])); 272 | $this->assertEqual(ceil($result[$this->Address->alias]['distance']), $expected); 273 | 274 | $result = $this->Address->near('all', array(25.7953, -80.2789)); 275 | $expected = 331; 276 | $this->assertTrue(!empty($result[0])); 277 | $this->assertEqual(ceil($result[0][$this->Address->alias]['distance']), $expected); 278 | 279 | $result = $this->Address->near('all', '1209 La Brad Lane, Tampa, FL'); 280 | $this->assertTrue(!empty($result)); 281 | if (!empty($result)) { 282 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 283 | foreach($result as $key => $distance) { 284 | $result[$key] = round($distance, 3); 285 | } 286 | $expected = array( 287 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.257, 288 | '1180 Magdalene Hill, Florida, US' => 0.499, 289 | '9106 El Portal Dr, Tampa, FL' => 5.331 290 | ); 291 | $this->assertEqual($result, $expected); 292 | } 293 | 294 | $result = $this->Address->near('all', '1209 La Brad Lane, Tampa, FL', 1); 295 | $this->assertTrue(!empty($result)); 296 | if (!empty($result)) { 297 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 298 | foreach($result as $key => $distance) { 299 | $result[$key] = round($distance, 3); 300 | } 301 | $expected = array( 302 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.257, 303 | '1180 Magdalene Hill, Florida, US' => 0.499 304 | ); 305 | $this->assertEqual($result, $expected); 306 | } 307 | 308 | $result = $this->Address->near('count', '1209 La Brad Lane, Tampa, FL', 1); 309 | $this->assertEqual($result, 2); 310 | 311 | $result = $this->Address->near('all', '1209 La Brad Lane, Tampa, FL', 0.5, 'm'); 312 | $this->assertTrue(!empty($result)); 313 | if (!empty($result)) { 314 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 315 | foreach($result as $key => $distance) { 316 | $result[$key] = round($distance, 3); 317 | } 318 | $expected = array( 319 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.160, 320 | '1180 Magdalene Hill, Florida, US' => 0.310 321 | ); 322 | $this->assertEqual($result, $expected); 323 | } 324 | } 325 | 326 | public function testSave() { 327 | $address = array( 328 | 'address1' => '1600 Amphitheatre Parkway', 329 | 'city' => 'Mountan View', 330 | 'state' => 'CA', 331 | 'zip' => 94043, 332 | 'country' => 'United States of America' 333 | ); 334 | $result = $this->Address->find('first', array( 335 | 'conditions' => $address, 336 | 'recursive' => -1, 337 | 'fields' => array_keys($address) 338 | )); 339 | $this->assertFalse($result); 340 | 341 | $this->Address->create(); 342 | $saved = $this->Address->save($address); 343 | $this->assertTrue($saved !== false); 344 | 345 | $result = $this->Address->find('first', array( 346 | 'conditions' => $address, 347 | 'recursive' => -1, 348 | 'fields' => array_merge(array_keys($address), array('latitude', 'longitude')) 349 | )); 350 | $this->assertTrue(!empty($result)); 351 | if (!empty($result)) { 352 | foreach(array('latitude', 'longitude') as $field) { 353 | $result[$this->Address->alias][$field] = round($result[$this->Address->alias][$field], 1); 354 | } 355 | 356 | $expected = array_merge($address, array( 357 | 'latitude' => 37.4, 358 | 'longitude' => -122.1 359 | )); 360 | 361 | $this->assertEqual($result[$this->Address->alias], $expected); 362 | } 363 | } 364 | 365 | public function testExtendedSave() { 366 | $address = array( 367 | 'address_1' => '1600 Amphitheatre Parkway', 368 | 'city_id' => '951470f2-e770-102c-aa5d-00138fbbb402', 369 | 'state_id' => '95147110-e770-102c-aa5d-00138fbbb402', 370 | 'zip' => 94043 371 | ); 372 | $result = $this->ExtendedAddress->find('first', array( 373 | 'conditions' => $address, 374 | 'recursive' => -1, 375 | 'fields' => array_keys($address) 376 | )); 377 | $this->assertFalse($result); 378 | 379 | $this->ExtendedAddress->create(); 380 | $saved = $this->ExtendedAddress->save($address); 381 | $this->assertTrue($saved !== false); 382 | 383 | $result = $this->ExtendedAddress->find('first', array( 384 | 'conditions' => $address, 385 | 'recursive' => -1, 386 | 'fields' => array_merge(array_keys($address), array('latitude', 'longitude')) 387 | )); 388 | $this->assertTrue(!empty($result)); 389 | if (!empty($result)) { 390 | foreach(array('latitude', 'longitude') as $field) { 391 | $result[$this->ExtendedAddress->alias][$field] = round($result[$this->ExtendedAddress->alias][$field], 1); 392 | } 393 | 394 | $expected = array_merge($address, array( 395 | 'latitude' => 37.4, 396 | 'longitude' => -122.1 397 | )); 398 | 399 | $this->assertEqual($result[$this->ExtendedAddress->alias], $expected); 400 | } 401 | 402 | $this->ExtendedAddress->create(); 403 | $saved = $this->ExtendedAddress->save(array( 404 | 'address_1' => '1600 Amphitheatre Parkway', 405 | 'city' => 'Mountan View', 406 | 'state_id' => '95147110-e770-102c-aa5d-00138fbbb402', 407 | 'zip' => 94043 408 | )); 409 | $this->assertTrue($saved !== false); 410 | 411 | $result = $this->ExtendedAddress->find('first', array( 412 | 'conditions' => $address, 413 | 'recursive' => -1, 414 | 'fields' => array_merge(array_keys($address), array('latitude', 'longitude')) 415 | )); 416 | $this->assertTrue(!empty($result)); 417 | if (!empty($result)) { 418 | foreach(array('latitude', 'longitude') as $field) { 419 | $result[$this->ExtendedAddress->alias][$field] = round($result[$this->ExtendedAddress->alias][$field], 1); 420 | } 421 | 422 | $expected = array_merge($address, array( 423 | 'latitude' => 37.4, 424 | 'longitude' => -122.1 425 | )); 426 | 427 | $this->assertEqual($result[$this->ExtendedAddress->alias], $expected); 428 | } 429 | } 430 | 431 | public function testFind() { 432 | $result = $this->Address->find('near', array('address'=>array(25.7953, -80.2789))); 433 | $expected = 331; 434 | $this->assertTrue(!empty($result[0])); 435 | $this->assertEqual(ceil($result[0][$this->Address->alias]['distance']), $expected); 436 | 437 | $result = $this->Address->find('near', array('address'=>'1209 La Brad Lane, Tampa, FL')); 438 | $this->assertTrue(!empty($result)); 439 | if (!empty($result)) { 440 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 441 | foreach($result as $key => $distance) { 442 | $result[$key] = round($distance, 3); 443 | } 444 | $expected = array( 445 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.257, 446 | '1180 Magdalene Hill, Florida, US' => 0.499, 447 | '9106 El Portal Dr, Tampa, FL' => 5.331 448 | ); 449 | $this->assertEqual($result, $expected); 450 | } 451 | 452 | $result = $this->Address->find('near', array('address'=>'1209 La Brad Lane, Tampa, FL', 'distance'=>1)); 453 | $this->assertTrue(!empty($result)); 454 | if (!empty($result)) { 455 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 456 | foreach($result as $key => $distance) { 457 | $result[$key] = round($distance, 3); 458 | } 459 | $expected = array( 460 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.257, 461 | '1180 Magdalene Hill, Florida, US' => 0.499 462 | ); 463 | $this->assertEqual($result, $expected); 464 | } 465 | 466 | $result = $this->Address->find('count', array('type'=>'near', 'address'=>'1209 La Brad Lane, Tampa, FL', 'distance'=>1)); 467 | $this->assertEqual($result, 2); 468 | 469 | $result = $this->Address->find('near', array('address'=>'1209 La Brad Lane, Tampa, FL', 'distance'=>0.5, 'unit'=>'m')); 470 | $this->assertTrue(!empty($result)); 471 | if (!empty($result)) { 472 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 473 | foreach($result as $key => $distance) { 474 | $result[$key] = round($distance, 3); 475 | } 476 | $expected = array( 477 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.160, 478 | '1180 Magdalene Hill, Florida, US' => 0.310 479 | ); 480 | $this->assertEqual($result, $expected); 481 | } 482 | } 483 | 484 | public function testPaginate() { 485 | $Controller = new Controller(); 486 | $Controller->uses = array('TestAddress'); 487 | $Controller->params['url'] = array(); 488 | $Controller->constructClasses(); 489 | 490 | $Controller->paginate = array('TestAddress' => array( 491 | 'near', 'fields' => array('address'), 'limit' => 2, 492 | 'address' => '1209 La Brad Lane, Tampa, FL' 493 | )); 494 | $result = $Controller->paginate('TestAddress'); 495 | $this->assertTrue(!empty($result)); 496 | if (!empty($result)) { 497 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 498 | foreach($result as $key => $distance) { 499 | $result[$key] = round($distance, 3); 500 | } 501 | $expected = array( 502 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.257, 503 | '1180 Magdalene Hill, Florida, US' => 0.499 504 | ); 505 | $this->assertEqual($result, $expected); 506 | } 507 | 508 | $Controller->paginate = array('TestAddress' => array( 509 | 'near', 'fields' => array('address'), 'limit' => 2, 510 | 'address' => '1209 La Brad Lane, Tampa, FL', 511 | 'page' => 2 512 | )); 513 | $result = $Controller->paginate('TestAddress'); 514 | $this->assertTrue(!empty($result)); 515 | if (!empty($result)) { 516 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 517 | foreach($result as $key => $distance) { 518 | $result[$key] = round($distance, 3); 519 | } 520 | $expected = array( 521 | '9106 El Portal Dr, Tampa, FL' => 5.331 522 | ); 523 | $this->assertEqual($result, $expected); 524 | } 525 | 526 | $Controller->paginate = array('TestAddress' => array( 527 | 'near', 'fields' => array('address'), 'limit' => 2, 528 | 'address' => '1209 La Brad Lane, Tampa, FL', 'unit' => 'm' 529 | )); 530 | $result = $Controller->paginate('TestAddress'); 531 | $this->assertTrue(!empty($result)); 532 | if (!empty($result)) { 533 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 534 | foreach($result as $key => $distance) { 535 | $result[$key] = round($distance, 3); 536 | } 537 | $expected = array( 538 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.160, 539 | '1180 Magdalene Hill, Florida, US' => 0.310 540 | ); 541 | $this->assertEqual($result, $expected); 542 | } 543 | 544 | $Controller->paginate = array('TestAddress' => array( 545 | 'near', 'fields' => array('address'), 'limit' => 2, 546 | 'address' => '1209 La Brad Lane, Tampa, FL', 'unit' => 'm', 'distance' => 0.25 547 | )); 548 | $result = $Controller->paginate('TestAddress'); 549 | $this->assertTrue(!empty($result)); 550 | if (!empty($result)) { 551 | $result = Set::combine($result, '/' . $this->Address->alias . '/address', '/' . $this->Address->alias . '/distance'); 552 | foreach($result as $key => $distance) { 553 | $result[$key] = round($distance, 3); 554 | } 555 | $expected = array( 556 | '14348 N Rome Ave, Tampa, 33613 FL' => 0.160 557 | ); 558 | $this->assertEqual($result, $expected); 559 | } 560 | } 561 | } 562 | ?> 563 | -------------------------------------------------------------------------------- /tests/fixtures/address_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'length' => 36, 'key' => 'primary'), 6 | 'address' => array('type' => 'text'), 7 | 'address_1' => array('type' => 'string', 'length' => 255), 8 | 'address_2' => array('type' => 'string', 'length' => 255, 'null' => true), 9 | 'city' => array('type' => 'string', 'length' => 255, 'null' => true), 10 | 'city_id' => array('type' => 'string', 'length' => 36, 'null' => true), 11 | 'state_id' => array('type' => 'string', 'length' => 36), 12 | 'zip' => array('type' => 'string', 'length' => 10), 13 | 'latitude' => array('type' => 'float'), 14 | 'longitude' => array('type' => 'float') 15 | ); 16 | } 17 | ?> 18 | -------------------------------------------------------------------------------- /tests/fixtures/city_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'length' => 36, 'key' => 'primary'), 6 | 'state_id' => array('type' => 'string', 'length' => 36, 'null' => true), 7 | 'name' => array('type' => 'string', 'length' => 255), 8 | ); 9 | public $records = array( 10 | array( 11 | 'id' => '951470f2-e770-102c-aa5d-00138fbbb402', 12 | 'state_id' => '95147110-e770-102c-aa5d-00138fbbb402', 13 | 'name' => 'Mountan View' 14 | ) 15 | ); 16 | } 17 | ?> 18 | -------------------------------------------------------------------------------- /tests/fixtures/country_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'length' => 36, 'key' => 'primary'), 6 | 'name' => array('type' => 'string', 'length' => 255), 7 | ); 8 | public $records = array( 9 | array( 10 | 'id' => '95147124-e770-102c-aa5d-00138fbbb402', 11 | 'name' => 'United States of America' 12 | ) 13 | ); 14 | } 15 | ?> 16 | -------------------------------------------------------------------------------- /tests/fixtures/geo_address_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'length' => 36, 'key' => 'primary'), 6 | 'address' => array('type' => 'string'), 7 | 'address1' => array('type' => 'string', 'length' => 255), 8 | 'address2' => array('type' => 'string', 'length' => 255, 'null' => true), 9 | 'city' => array('type' => 'string', 'length' => 255, 'null' => true), 10 | 'state' => array('type' => 'string', 'length' => 255, 'null' => true), 11 | 'zip' => array('type' => 'string', 'length' => 10), 12 | 'country' => array('type' => 'string', 'length' => 255, 'null' => true), 13 | 'latitude' => array('type' => 'float'), 14 | 'longitude' => array('type' => 'float') 15 | ); 16 | public $records = array( 17 | array( 18 | 'id' => '4a8f70ed-437c-45c5-ac04-0dc97f000101', 19 | 'address' => '1209 La Brad Lane, Tampa, FL', 20 | 'address1' => '1209 La Brad Lane', 21 | 'city' => 'Tampa', 22 | 'state' => 'FL', 23 | 'zip' => null, 24 | 'country' => null, 25 | 'latitude' => 28.0792040, 26 | 'longitude' => -82.4735510 27 | ), 28 | array( 29 | 'id' => '58325bdc-e08a-102c-b987-00138fbbb402', 30 | 'address' => '14348 N Rome Ave, Tampa, 33613 FL', 31 | 'address1' => '14348 N Rome Ave', 32 | 'city' => 'Tampa', 33 | 'state' => 'FL', 34 | 'zip' => 33613, 35 | 'country' => null, 36 | 'latitude' => 28.0780514, 37 | 'longitude' => -82.4758438 38 | ), 39 | array( 40 | 'id' => '2ea459ba-e08e-102c-b987-00138fbbb402', 41 | 'address' => '1180 Magdalene Hill, Florida, US', 42 | 'address1' => '1180 Magdalene Hill', 43 | 'city' => null, 44 | 'state' => 'Florida', 45 | 'zip' => null, 46 | 'country' => 'US', 47 | 'latitude' => 28.075205, 48 | 'longitude' => -82.475809 49 | ), 50 | array( 51 | 'id' => 'dfcf56d6-e08e-102c-b987-00138fbbb402', 52 | 'address' => '13216 Forest Hills Dr, Tampa, FL', 53 | 'address1' => '13216 Forest Hills Dr', 54 | 'city' => 'Tampa', 55 | 'state' => 'FL', 56 | 'zip' => null, 57 | 'country' => null, 58 | 'latitude' => 28.06817, 59 | 'longitude' => -82.473463 60 | ), 61 | array( 62 | 'id' => '7c92ca3e-e08f-102c-b987-00138fbbb402', 63 | 'address' => '9106 El Portal Dr, Tampa, FL', 64 | 'address1' => '9106 El Portal Dr', 65 | 'city' => 'Tampa', 66 | 'state' => 'FL', 67 | 'zip' => null, 68 | 'country' => null, 69 | 'latitude' => 28.0315434, 70 | 'longitude' => -82.4687346 71 | ) 72 | ); 73 | } 74 | ?> 75 | -------------------------------------------------------------------------------- /tests/fixtures/state_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'length' => 36, 'key' => 'primary'), 6 | 'country_id' => array('type' => 'string', 'length' => 36, 'null' => true), 7 | 'name' => array('type' => 'string', 'length' => 255), 8 | ); 9 | public $records = array( 10 | array( 11 | 'id' => '95147110-e770-102c-aa5d-00138fbbb402', 12 | 'country_id' => '95147124-e770-102c-aa5d-00138fbbb402', 13 | 'name' => 'California' 14 | ) 15 | ); 16 | } 17 | ?> 18 | -------------------------------------------------------------------------------- /views/helpers/geomap.php: -------------------------------------------------------------------------------- 1 | array( 17 | 'url' => 'http://www.google.com/jsapi?key=${key}' 18 | ), 19 | 'yahoo' => array( 20 | 'url' => 'http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=${key}' 21 | ) 22 | ); 23 | 24 | /** 25 | * Tells if JS resource was included 26 | * 27 | * @var bool 28 | */ 29 | private $included = false; 30 | 31 | /** 32 | * Get map HTML + JS code 33 | * 34 | * @param array $center If specified, center map in this location 35 | * @param array $markers Add these markers (each marker is array('point' => (x, y), 'title' => '', 'content' => '')) 36 | * @param array $parameters Parameters (service, key, id, width, height, zoom, div) 37 | * @return string HTML + JS code 38 | */ 39 | public function map($center = null, $markers = array(), $parameters = array()) { 40 | $parameters = array_merge(array( 41 | 'service' => Configure::read('Geocode.service'), 42 | 'key' => Configure::read('Geocode.key'), 43 | 'id' => null, 44 | 'width' => 500, 45 | 'height' => 300, 46 | 'zoom' => 10, 47 | 'div' => array('class'=>'map'), 48 | 'type' => 'street', 49 | 'layout' => Configure::read('Geocode.layout'), 50 | 'layouts' => array( 51 | 'default' => array( 52 | 'pan', 53 | 'scale', 54 | 'types', 55 | 'zoom' 56 | ), 57 | 'simple' => false 58 | ) 59 | ), $parameters); 60 | 61 | if (empty($parameters['layout'])) { 62 | $parameters['layout'] = 'default'; 63 | } 64 | 65 | if (empty($parameters['service'])) { 66 | $parameters['service'] = 'google'; 67 | } 68 | $service = strtolower($parameters['service']); 69 | if (!isset($this->services[$service])) { 70 | return false; 71 | } 72 | 73 | if (!$this->included) { 74 | $this->included = true; 75 | $this->Javascript->link(str_replace('${key}', $parameters['key'], $this->services[$service]['url']), false); 76 | } 77 | 78 | $out = ''; 79 | 80 | if (empty($parameters['id'])) { 81 | $parameters['id'] = 'map_' . Security::hash(uniqid(time(), true)); 82 | } 83 | 84 | if ($parameters['div'] !== false) { 85 | $out .= $this->Html->div( 86 | !empty($parameters['div']['class']) ? $parameters['div']['class'] : null, 87 | '', 88 | array_merge($parameters['div'], array('id'=>$parameters['id'])) 89 | ); 90 | } 91 | 92 | if (!empty($markers)) { 93 | foreach($markers as $i => $marker) { 94 | if (is_array($marker) && count($marker) == 2 && isset($marker[0]) && isset($marker[1]) && is_numeric($marker[0]) && is_numeric($marker[1])) { 95 | $marker = array('point' => $marker); 96 | } 97 | $marker = array_merge(array( 98 | 'point' => null, 99 | 'title' => null, 100 | 'content' => null, 101 | 'icon' => null, 102 | 'shadow' => null 103 | ), $marker); 104 | 105 | if (empty($marker['point'])) { 106 | unset($markers[$i]); 107 | continue; 108 | } 109 | 110 | foreach(array('title', 'content') as $parameter) { 111 | if (!empty($marker[$parameter])) { 112 | $marker[$parameter] = str_replace( 113 | array('"', "\n"), 114 | array('\\"', '\\n'), 115 | $marker[$parameter] 116 | ); 117 | } 118 | } 119 | 120 | $markers[$i] = $marker; 121 | } 122 | $markers = array_values($markers); 123 | } 124 | 125 | if (empty($center)) { 126 | $center = !empty($markers) ? $markers[0]['point'] : array(0, 0); 127 | } 128 | 129 | if (!empty($parameters['layout'])) { 130 | if (!is_array($parameters['layout'])) { 131 | if (!array_key_exists($parameters['layout'], $parameters['layouts'])) { 132 | $parameters['layout'] = 'default'; 133 | } 134 | $parameters['layout'] = $parameters['layouts'][$parameters['layout']]; 135 | } 136 | } 137 | 138 | $out .= $this->{'_'.$service}($parameters['id'], $center, $markers, $parameters); 139 | return $out; 140 | } 141 | 142 | /** 143 | * Google Map 144 | * 145 | * @param string $id Container ID 146 | * @param array $center If specified, center map in this location 147 | * @param array $markers Add these markers (each marker is array('point' => (x, y), 'title' => '', 'content' => '')) 148 | * @param array $parameters Parameters (service, version, key, id, width, height, zoom, div) 149 | * @return string HTML + JS code 150 | */ 151 | protected function _google($id, $center, $markers, $parameters) { 152 | $parameters = array_merge(array( 153 | 'version' => 2 154 | ), $parameters); 155 | 156 | if ($parameters['version'] >= 3) { 157 | $mapTypes = array( 158 | 'street' => 'google.maps.MapTypeId.ROADMAP', 159 | 'satellite' => 'google.maps.MapTypeId.SATELLITE', 160 | 'hybrid' => 'google.maps.MapTypeId.HYBRID', 161 | 'terrain' => 'google.maps.MapTypeId.TERRAIN' 162 | ); 163 | $layouts = array( 164 | 'elements' => array( 165 | 'scale' => 'scaleControl', 166 | 'types' => 'mapTypeControl', 167 | 'zoom' => 'navigationControl', 168 | 'pan' => 'navigationControl' 169 | ) 170 | ); 171 | } else { 172 | $mapTypes = array( 173 | 'street' => 'google.maps.maptypes.normal', 174 | 'satellite' => 'google.maps.maptypes.satellite', 175 | 'hybrid' => 'google.maps.maptypes.hybrid', 176 | 'terrain' => 'google.maps.maptypes.physical' 177 | ); 178 | $layouts = array( 179 | 'elements' => array( 180 | 'scale' => 'google.maps.ScaleControl', 181 | 'types' => 'google.maps.MapTypeControl', 182 | 'zoom' => 'google.maps.LargeMapControl3D', 183 | 'pan' => 'google.maps.LargeMapControl3D' 184 | ) 185 | ); 186 | } 187 | 188 | if (!empty($parameters['layout'])) { 189 | foreach($parameters['layout'] as $element => $enabled) { 190 | unset($parameters['layout'][$element]); 191 | if (is_numeric($element)) { 192 | $element = $enabled; 193 | $enabled = true; 194 | } 195 | $parameters['layout'][$element] = $enabled; 196 | } 197 | } 198 | 199 | $mapVarName = 'm' . $id; 200 | $script = 'var ' . $mapVarName . '_Callback = function() {'; 201 | 202 | if ($parameters['version'] >= 3) { 203 | $script .= ' 204 | var mapOptions = { 205 | mapTypeId: ' . $mapTypes[$parameters['type']] . ', 206 | disableDefaultUI: true 207 | }; 208 | '; 209 | 210 | if (!empty($parameters['width']) && !empty($parameters['height'])) { 211 | $script .= ' 212 | mapOptions.size = new google.maps.Size(' . $parameters['width'] . ', ' . $parameters['height'] . '); 213 | '; 214 | } 215 | 216 | if (!empty($center)) { 217 | list($latitude, $longitude) = $center; 218 | $script .= ' 219 | mapOptions.center = new google.maps.LatLng(' . $latitude . ', ' . $longitude . '); 220 | '; 221 | } 222 | 223 | if (!empty($parameters['zoom'])) { 224 | $script .= ' 225 | mapOptions.zoom = ' . $parameters['zoom'] . '; 226 | '; 227 | } 228 | 229 | if (!empty($parameters['layout'])) { 230 | foreach($parameters['layout'] as $element => $enabled) { 231 | if (empty($layouts['elements'][$element])) { 232 | continue; 233 | } else if ($element == 'zoom' && !empty($parameters['layout']['pan'])) { 234 | continue; 235 | } 236 | 237 | $key = $layouts['elements'][$element]; 238 | $value = !empty($enabled) ? 'true' : 'false'; 239 | if ($element == 'zoom' && empty($parameters['layout']['pan'])) { 240 | $value = 'google.maps.NavigationControlStyle.SMALL'; 241 | } elseif ($element == 'pan') { 242 | $value = 'google.maps.NavigationControlStyle.ZOOM_PAN'; 243 | } 244 | 245 | if (!in_array($value, array('true', 'false'))) { 246 | $script .= ' 247 | mapOptions.' . $key . ' = true; 248 | mapOptions.' . $key . 'Options = { style: ' . $value . ' }; 249 | '; 250 | } else { 251 | $script .= ' 252 | mapOptions.' . $key . ' = ' . $value . '; 253 | '; 254 | } 255 | } 256 | } 257 | 258 | $script .= $mapVarName . ' = new google.maps.Map(document.getElementById("' . $id . '"), mapOptions);'; 259 | } else { 260 | $script .= ' 261 | if (!google.maps.BrowserIsCompatible()) { 262 | return false; 263 | } 264 | 265 | var mapOptions = {}; 266 | '; 267 | 268 | if (!empty($parameters['width']) && !empty($parameters['height'])) { 269 | $script .= ' 270 | mapOptions.size = new google.maps.Size(' . $parameters['width'] . ', ' . $parameters['height'] . '); 271 | '; 272 | } 273 | 274 | $script .= $mapVarName . ' = new google.maps.Map2(document.getElementById("' . $id . '"), mapOptions);'; 275 | 276 | if (!empty($center)) { 277 | list($latitude, $longitude) = $center; 278 | $script .= $mapVarName . '.setCenter( 279 | new google.maps.LatLng(' . $latitude . ', ' . $longitude . ')' . 280 | (!empty($parameters['zoom']) ? ', ' . $parameters['zoom'] : '') . ' 281 | );'; 282 | } 283 | 284 | if (!empty($parameters['layout'])) { 285 | foreach($parameters['layout'] as $element => $enabled) { 286 | if (empty($layouts['elements'][$element])) { 287 | continue; 288 | } else if ($element == 'zoom' && !empty($parameters['layout']['pan'])) { 289 | continue; 290 | } else if (!$enabled) { 291 | continue; 292 | } 293 | 294 | $script .= $mapVarName . '.addControl(new ' . $layouts['elements'][$element] . '());'; 295 | } 296 | } 297 | } 298 | 299 | if (!empty($markers)) { 300 | foreach($markers as $i => $marker) { 301 | $markerOptions = array( 302 | 'title' => null, 303 | 'content' => null, 304 | 'icon' => null, 305 | 'shadow' => null 306 | ); 307 | $markerOptions = array_filter(array_intersect_key($marker, $markerOptions)); 308 | $content = (!empty($markerOptions['content']) ? $markerOptions['content'] : null); 309 | $markerVarName = 'marker'.$i; 310 | 311 | list($latitude, $longitude) = $marker['point']; 312 | 313 | if ($parameters['version'] >= 3) { 314 | $script .= ' 315 | var '.$markerVarName.' = new google.maps.Marker({ 316 | map: ' . $mapVarName . ', 317 | position: new google.maps.LatLng(' . $latitude . ', ' . $longitude . '), 318 | title: "' . (!empty($markerOptions['title']) ? $markerOptions['title'] : '') . '", 319 | clickable: ' . (!empty($content) ? 'true' : 'false') . ', 320 | icon: ' . (!empty($markerOptions['icon']) ? '"' . $markerOptions['icon'] . '"' : 'null') . ', 321 | shadow: ' . (!empty($markerOptions['shadow']) ? '"' . $markerOptions['shadow'] . '"' : 'null') . ' 322 | }); 323 | '; 324 | 325 | if (!empty($content)) { 326 | $script .= ' 327 | var '.$markerVarName.'InfoWindow = new google.maps.InfoWindow({ 328 | content: "' . $content . '" 329 | }); 330 | google.maps.event.addListener('.$markerVarName.', \'click\', function() { 331 | '.$markerVarName.'InfoWindow.open(' . $mapVarName . ', '.$markerVarName.'); 332 | }); 333 | '; 334 | 335 | } 336 | } else { 337 | $script .= 'var '.$markerVarName.'Options = {};'; 338 | 339 | if (!empty($markerOptions['icon'])) { 340 | $script .= ' 341 | var '.$markerVarName.'Icon = new google.maps.Icon(google.maps.DEFAULT_ICON); 342 | '.$markerVarName.'Icon.image = "' . $markerOptions['icon'] . '"; 343 | '.$markerVarName.'Options.icon = '.$markerVarName.'Icon; 344 | '; 345 | } 346 | 347 | $script .= 'var '.$markerVarName.' = new google.maps.Marker(new google.maps.LatLng(' . $latitude . ', ' . $longitude . '), '.$markerVarName.'Options);'; 348 | $script .= $mapVarName.'.addOverlay('.$markerVarName.');'; 349 | 350 | if (!empty($content)) { 351 | $script .= 'google.maps.Event.addListener('.$markerVarName.', \'click\', function() { 352 | '.$markerVarName.'.openInfoWindowHtml("' . $content . '"); 353 | }); 354 | '; 355 | } 356 | } 357 | } 358 | } 359 | 360 | $script .= ' 361 | ' . $mapVarName . '.__version__ = ' . $parameters['version'] . '; 362 | } 363 | 364 | var ' . $mapVarName . ' = null; 365 | google.load("maps", "' . $parameters['version'] . '", { 366 | other_params: "sensor=' . (!empty($parameters['sensor']) ? 'true' : 'false') . '", 367 | callback: ' . $mapVarName . '_Callback 368 | });'; 369 | 370 | return $this->Javascript->codeBlock($script); 371 | } 372 | 373 | /** 374 | * Yahoo Map 375 | * 376 | * @param string $id Container ID 377 | * @param array $center If specified, center map in this location 378 | * @param array $markers Add these markers (each marker is array('point' => (x, y), 'title' => '', 'content' => '')) 379 | * @param array $parameters Parameters (service, key, id, width, height, zoom, div) 380 | * @return string HTML + JS code 381 | */ 382 | protected function _yahoo($id, $center, $markers, $parameters) { 383 | $mapVarName = 'm' . $id; 384 | $mapTypes = array( 385 | 'street' => 'YAHOO_MAP_REG', 386 | 'satellite' => 'YAHOO_MAP_SAT', 387 | 'hybrid' => 'YAHOO_MAP_HYB' 388 | ); 389 | $layouts = array( 390 | 'elements' => array( 391 | 'pan' => '${var}.addPanControl()', 392 | 'scale' => '${var}.addZoomScale()', 393 | 'types' => '${var}.addTypeControl()', 394 | 'zoom' => '${var}.addZoomLong()' 395 | ) 396 | ); 397 | 398 | $script = ' 399 | var ' . $mapVarName . ' = new YMap(document.getElementById("' . $id . '")); 400 | '; 401 | 402 | $script .= $mapVarName . '.setMapType(' . $mapTypes[$parameters['type']] . ');' . "\n"; 403 | 404 | if (!empty($center)) { 405 | list($latitude, $longitude) = $center; 406 | $script .= $mapVarName . '.drawZoomAndCenter(new YGeoPoint(' . $latitude . ', ' . $longitude . '));' . "\n"; 407 | } 408 | 409 | if (!empty($parameters['width']) && !empty($parameters['height'])) { 410 | $script .= $mapVarName . '.resizeTo(new YSize(' . $parameters['width'] . ', ' . $parameters['height'] . '));' . "\n"; 411 | } 412 | 413 | if (!empty($parameters['zoom'])) { 414 | $script .= $mapVarName . '.setZoomLevel(' . $parameters['zoom'] . ');' . "\n"; 415 | } 416 | 417 | $script .= $mapVarName . '.removeZoomScale();' . "\n"; 418 | 419 | if (!empty($parameters['layout'])) { 420 | foreach($parameters['layout'] as $element => $enabled) { 421 | unset($parameters['layout'][$element]); 422 | if (is_numeric($element)) { 423 | $element = $enabled; 424 | $enabled = true; 425 | } 426 | $parameters['layout'][$element] = $enabled; 427 | } 428 | 429 | foreach($parameters['layout'] as $element => $enabled) { 430 | if ($enabled && !empty($layouts['elements'][$element])) { 431 | $script .= str_replace('${var}', $mapVarName, $layouts['elements'][$element]) . ';' . "\n"; 432 | } 433 | } 434 | } 435 | 436 | if (!empty($markers)) { 437 | foreach($markers as $i => $marker) { 438 | list($latitude, $longitude) = $marker['point']; 439 | $markerVvarName = 'marker'.$i; 440 | $script .= 'var '.$markerVarName.'Content = null' . "\n"; 441 | if (!empty($marker['content'])) { 442 | $script .= $markerVarName.'Content = "' . $marker['content'] . '";' . "\n"; 443 | } 444 | $script .= ' 445 | var '.$markerVarName.' = new YMarker(new YGeoPoint(' . $latitude . ', ' . $longitude . ')); 446 | YEvent.Capture('.$markerVVarName.', EventsList.MouseClick, function(o) { 447 | '.$markerVarName.'.openSmartWindow('.$markerVarName.'Content); 448 | }); 449 | ' . $mapVarName . '.addOverlay('.$markerVarName.'); 450 | '; 451 | } 452 | } 453 | 454 | return $this->Javascript->codeBlock($script); 455 | } 456 | } 457 | ?> 458 | --------------------------------------------------------------------------------