A string representing the street location * 'additional' => A string for any additional portion of the street location * 'city' => A string for the city name * 'province' => The standard postal abbreviation for the province * 'country' => The two-letter ISO code for the country of the location (REQUIRED) * 'postal_code' => The international postal code for the location * * @param string $link_text * Text for a link. * * @return string * A link to a map provided by a third-party. The idea is to encode the appropriate * parameters as HTTP GET variables to the URL. * * @ingroup Location */ function location_map_link($location = array(), $link_text = 'See map: ') { if (!isset($location['country']) || $location['country'] == 'xx') { return ''; } location_load_country($location['country']); $default_func = 'location_map_link_' . $location['country'] . '_default_providers'; $providers_func = 'location_map_link_' . $location['country'] . '_providers'; // Merge the global providers with country specific ones so that countries // can add to or override the defaults. $providers = function_exists($providers_func) ? array_merge( location_map_link_providers(), $providers_func() ) : location_map_link_providers(); // Default providers will be taken from the country specific default providers // function if it exists, otherwise it will use the global function. $selected_providers = variable_get( 'location_map_link_' . $location['country'], function_exists($default_func) ? $default_func() : location_map_link_default_providers() ); $links = array(); foreach ($selected_providers as $mapper) { $link_func = 'location_map_link_' . $location['country'] . '_' . $mapper; // If there is no country function try for a global one. $link_func = function_exists($link_func) ? $link_func : 'location_map_link_' . $mapper; if (function_exists($link_func)) { if ($link = $link_func($location)) { $links[] = '' . $providers[$mapper]['name'] . ''; } } } if (count($links)) { return ''; } else { return NULL; } } /** * Provide default map link providers. * * By implementing location_map_link_COUNTRYCODE_providers, individual * countries can add to this (they will be merged so this default can also be * overridden using those functions). * * @return array * An array where * - the key is the a descriptive key for the provider. * The key is also used in the function name of the function that * generates the map links for that provider. For example, a key of * 'google' means the name of the function that builds a link to a map * Google Maps would be 'location_map_link_google', or * 'location_map_link_COUNTRYCODE_google' for country overrides. * - the value is itself an array with 3 key/value pairs: * - 'name' => points to the name of the mapping service. For 'google', * this would be 'Google Maps' * - 'url' => the url of the main page of the mapping service. For * 'google', this would be 'http://maps.google.com' * - 'tos' => the url of the page that explains the map providers Terms of * Service, or Terms of Use. For 'google', this would be * 'http://www.google.com/help/terms_local.html' */ function location_map_link_providers() { return array( 'google' => array( 'name' => t('Google Maps'), 'url' => 'http://maps.google.com', 'tos' => 'http://www.google.com/help/terms_local.html', ), ); } /** * Provide the default map link providers. * * This can be overridden by implementing * location_map_link_COUNTRYCODE_default_providers for a given country. * * @return array * An array of values that work as keys to the array returned by * location_map_link_providers (and country versions of that function). * The idea is that if the administrator of the site has not yet had a chance * to visit the "Map Links" subtab on the location module's settings page, * that we can provide deep-linking to a relatively safe default. * By 'relatively safe', we mean that the Terms Of Service of the provider * of the maps are flexible enough for most parties. * * For example, in the case of the U.S., 'google' has relatively flexible * Terms Of Service, whereas Yahoo! Maps and MapQuest have more restrictive * Terms Of Service. */ function location_map_link_default_providers() { return array('google'); } /** * Google link. */ function location_map_link_google($location = array()) { $query_params = array(); foreach (array('street', 'city', 'province', 'postal_code', 'country') as $field) { if (isset($location[$field])) { $query_params[] = $location[$field]; } } if (count($query_params)) { return ('http://maps.google.com?q=' . urlencode(implode(", ", $query_params))); } else { return NULL; } } /** * Try to extract the the Latitude and Longitude data from the postal code. * * @param array $location * Array. the location data * -> the values are: * 'street' => the string representing the street location (REQUIRED) * 'additional' => the string representing the additional street location portion in the location form * 'city' => the city name (REQUIRED) * 'province' => the province code defined in the country-specific include file * 'country' => the lower-case of the two-letter ISO code (REQUIRED) * 'postal_code' => the postal-code (REQUIRED) * * @return array|null * Array or NULL. NULL if the delegated-to function that does the * actual look-up does not exist. If the appropriate function exists, * then this function returns an associative array where * 'lon' => A floating point number for the longitude coordinate of the parameter location * 'lat' => A floating point number for the latitude coordinate of the parameter location * * @ingroup Location */ function location_get_postalcode_data($location = array()) { $location['country'] = isset($location['country']) ? trim($location['country']) : NULL; $location['postal_code'] = isset($location['postal_code']) ? trim($location['postal_code']) : NULL; if (is_null($location['postal_code']) || is_null( $location['country'] ) || empty($location['country']) || empty($location['postal_code']) || $location['postal_code'] == 'xx' ) { return NULL; } location_load_country($location['country']); $country_specific_function = 'location_get_postalcode_data_' . $location['country']; if (function_exists($country_specific_function)) { return $country_specific_function($location); } else { return NULL; } } /** * Given two points in lat/lon form, returns the distance between them. * * @param array $latlon_a * An associative array where * 'lon' => is a floating point of the longitude coordinate for the point given by latlonA * 'lat' => is a floating point of the latitude coordinate for the point given by latlonB * * @param array $latlon_b * Another point formatted like $latlon_b * * @param string $distance_unit * A string that is either 'km' or 'mile'. * If neither 'km' or 'mile' is passed, the parameter is forced to 'km' * * @return array|null * NULL if sense can't be made of the parameters. * An associative array where * 'scalar' => Is the distance between the two lat/lon parameter points * 'distance_unit' => Is the unit of distance being represented by 'scalar'. * This will be 'km' unless 'mile' is passed for the $distance_unit param * * @ingroup Location */ function location_distance_between($latlon_a = array(), $latlon_b = array(), $distance_unit = 'km') { if (!isset($latlon_a['lon']) || !isset($latlon_a['lat']) || !isset($latlon_b['lon']) || !isset($latlon_b['lat'])) { return NULL; } if ($distance_unit != 'km' && $distance_unit != 'mile') { return NULL; } // $conversion_factor = number to divide by to convert meters to $distance_unit // At this point, $distance_unit == 'km' or 'mile' and nothing else // $conversion_factor = ($distance_unit == 'km') ? 1000.0 : 1609.347; $meters = earth_distance($latlon_a['lon'], $latlon_a['lat'], $latlon_b['lon'], $latlon_b['lat']); return array( 'scalar' => round($meters / (($distance_unit == 'km') ? 1000.0 : 1609.347), 1), 'distance_unit' => $distance_unit, ); } /** * Takes two locations and tries to return a deep-link to driving directions. * * @param array $location_a * An associative array that represents an location where * 'street' => the street portions of the location * 'additional' => additional street portion of the location * 'city' => the city name * 'province' => the province, state, or territory * 'country' => lower-cased two-letter ISO code (REQUIRED) * 'postal_code' => the postal code * * @param array $location_b * An associative array that represents an location in the same way that * parameter $location_a does. * * @param string $link_text * The text of the HTML link that is to be generated. * * @return string * A deep-link to driving directions on Yahoo! or some other mapping service, if enough fields are filled in the parameters. * A deep-link to a form for driving directions with information pre-filled if not enough, but some fields are filled in the parameters. * The empty string if no information is provided (or if so little information is provided that there is no function to which to delegate * the call. * * We dispatch the call to a country-specific function. The country-specific function, in this case, * will be the one reflected by the country parameter of the first function. We require that * both locationes supplied have a country field at the minimum. * * The country-specific functions will ultimately decide, with the parameters given, whether to * to link to a form for driving directions is provided, where this form will be * pre-populated with whatever values were available or whether to link directly to the driving * directions themselves if enough fields are filled for each location. * * @ingroup Location */ function location_driving_directions_link($location_a = array(), $location_b = array(), $link_text = 'Get directions') { if (!isset($location_a['country']) or !isset($location_b['country'])) { return ''; } // For now, return empty string if starting-point and destinations are in different countries // if ($location_a['country'] != $location_b['country']) { // return ''; // } // Lines above commented out because I want to let the country-specific function of the departure point decide // what it will do with driving destination locationes from other countries. As an example, Yahoo! Maps supports driving // direction queries for locations between the U.S. and Canada. $driving_direction_function = 'location_driving_directions_link_' . $location_a['country']; if (function_exists($driving_direction_function)) { $http_link = $driving_direction_function($location_a, $location_b); if (strlen($http_link)) { return '' . $link_text . ''; } else { return ''; } } return ''; } /** * Convert distances to meter. * * @param int $distance * A number in either miles or km. * * @param string $distance_unit * String (optional). Either 'mile' or 'km' (default) * * @return float * A floating point number where the number in meters after the initially passed scalar has been ceil()'d * This is done after the $distance_unit parmeter is forced to be 'km' or 'mile' */ function _location_convert_distance_to_meters($distance, $distance_unit = 'km') { if (!is_numeric($distance)) { return NULL; } if ($distance == 0) { return NULL; } if ($distance_unit != 'km' && $distance_unit != 'mile') { $distance_unit = 'km'; } // Convert distance to meters. $retval = round(floatval($distance) * (($distance_unit == 'km') ? 1000.0 : 1609.347), 2); return $retval; } /** * Takes an location and returns a "rough" latitude/longitude pair based on the postal code data available for the given country. * * @param array $location * An associative array $location where * 'street' => the street portion of the location * 'additional' => additional street portion of the location * 'province' => the province, state, or territory * 'country' => lower-cased two-letter ISO code (REQUIRED) * 'postal_code' => international postal code (REQUIRED) * * @return array|null * NULL if data cannot be found. * Otherwise, an associative array where * 'lat' => is a floating point of the latitude coordinate of this location * 'lon' => is a floating point of the longitude coordinate of this location * * @ingroup Location */ function location_latlon_rough($location = array()) { if (!isset($location['country']) || !isset($location['postal_code'])) { return NULL; } location_load_country($location['country']); $latlon_function = 'location_latlon_rough_' . $location['country']; if (function_exists($latlon_function) && $result = $latlon_function($location)) { return $result; } else { return location_latlon_rough_default($location); } } /** * Returns a lat/lon pair of the approximate center of the given postal code in the given country. * * This function is a default implementation, in case the country support doesn't implement a proper function for this. * * @param array $location * An associative array $location where * 'street' => the street portion of the location * 'supplemental' => additional street portion of the location * 'province' => the province, state, or territory * 'country' => lower-cased two-letter ISO code (REQUIRED) * 'postal_code' => the international postal code for this location (REQUIRED) * * @return array * An associative array where * 'lat' => approximate latitude of the center of the postal code's area * 'lon' => approximate longitude of the center of the postal code's area */ function location_latlon_rough_default($location = array()) { if (!isset($location['country']) || !isset($location['postal_code'])) { return NULL; } $query = db_select('zipcodes', 'z'); $query->addField('z', 'latitude', 'lat'); $query->addField('z', 'longitude', 'lon'); $coords = $query->condition('country', $location['country']) ->condition('zip', $location['postal_code']) ->execute() ->fetchAssoc(); if ($coords) { return $coords; } // If nothing found internally (new zipcode, or incomplete zipcodes table). elseif (($newlatlong = location_latlon_exact($location)) != NULL) { // Try a one-time external geocoding. if ($newlatlong['lat']) { // And store results in zipcodes table for next lookups being internally handled // (yeah this is missing city/state info a.t.m., but is way better than nothing!) db_insert('zipcodes') ->fields( array( 'latitude' => $newlatlong['lat'], 'longitude' => $newlatlong['lon'], 'country' => $location['country'], 'zip' => $location['postal_code'], ) ) ->execute(); } return $newlatlong; } else { return NULL; } } /** * Get an appropriate bounding box for showing an entire country on a map. * * Target is a bounding box large enough to show the country in both spherical * mercator and mercator projections. * * @param array $location * Either a location array or a country code. * * @return array * An array with 'minlng', 'minlat', 'maxlng', and 'maxlat' elements. */ function location_country_bounds($location = array()) { if (!is_array($location)) { $location = array('country' => $location); } if (!empty($location['country'])) { location_load_country($location['country']); } $country_bounds_function = 'location_bounds_' . $location['country']; if (function_exists($country_bounds_function)) { return $country_bounds_function(); } else { return array( 'minlng' => -180.0, 'minlat' => -90.0, 'maxlng' => 180.0, 'maxlat' => 90.0, ); } } /** * Currently, this is not a priority until there is an implementable use for exact longitude latitude coordinates for an location. * * The idea is that this call will eventually retrieve * information through a web-service. Whereas location_latlon_rough() returns an approximate * lat/lon pair based strictly on the postal code where this lat/lon pair is pulled from a * database table, this function is intended to send the entire location to a web-service and * to retrieve exact lat/lon coordinates. * * @param array $location * An array where * -> the key values are 'street', 'additional', 'province', 'country', 'postal_code' * -> the values are: * 'street' => the string representing the street location (REQUIRED) * 'additional' => the string representing the additional street location portion in the location form * 'city' => the city name (REQUIRED) * 'province' => the province code defined in the country-specific include file * 'country' => the lower-case of the two-letter ISO code (REQUIRED) * 'postal_code' => the postal-code (REQUIRED) * * @return array|null * NULL if the delegated-to function that does the actual look-up does not exist. * If the appropriate function exists, then this function returns an associative array where * 'lon' => A floating point number for the longitude coordinate of the parameter location * 'lat' => A floating point number for the latitude coordinate of the parameter location * * @ingroup Location */ function location_latlon_exact($location = array()) { $country = $location['country']; location_standardize_country_code($country); $service = variable_get('location_geocode_' . $country, 'none'); if (!empty($country) && $service != 'none') { // Figure out what the exact function should be. if (strpos($service, '|')) { location_load_country($country); // The code change below fixes the problem of the country specific // function for geocoding not being correctly called (it removes any // text from the pipe (|) onwards). $exact_latlon_function = 'location_geocode_' . $country . '_' . substr($service, 0, strpos($service, '|')); } else { location_load_geocoder($service); $exact_latlon_function = $service . '_geocode_location'; } if (function_exists($exact_latlon_function)) { return $exact_latlon_function($location); } else { return NULL; } } return NULL; } /** * Returns an associative array of countries currently supported by the location system where. * * -> the keys represent the two-letter ISO code and * -> the values represent the English name of the country. * The array is sorted the index (i.e., by the short English name of the country). * * Please note the different between "supported" countries and "configured" * countries: A country being "supported" means that there is an include file * to support the country while "configure" implies that the site admin has * configured the site to actually use that country. * * @ingroup Location */ function _location_supported_countries() { $supported_countries = & drupal_static(__FUNCTION__, array()); // If this function has already been called this request, we can avoid a DB hit. if (!empty($supported_countries)) { return $supported_countries; } // Try first to load from cache, it's much faster than the scan below. if ($cache = cache_get('location:supported-countries', 'cache_location')) { $supported_countries = $cache->data; } else { // '' => '' $iso_list = location_get_iso3166_list(); $path = drupal_get_path('module', 'location') . '/supported/location.'; foreach ($iso_list as $cc => $name) { if (file_exists($path . $cc . '.inc')) { $supported_countries[$cc] = $name; } } cache_set('location:supported-countries', $supported_countries, 'cache_location'); } return $supported_countries; } // @@@ New in 3.x, document. /** * Fetch the provinces for a country. */ function location_get_provinces($country = 'us') { $provinces = & drupal_static(__FUNCTION__, array()); // Current language. $lang_code = $GLOBALS['language']->language; location_standardize_country_code($country); if (!isset($provinces[$country])) { if ($cache = cache_get("provinces:$country:$lang_code", 'cache_location')) { $provinces[$country] = $cache->data; } else { location_load_country($country); $func = 'location_province_list_' . $country; if (function_exists($func)) { $provinces[$country] = $func(); cache_set("provinces:$country:$lang_code", $provinces[$country], 'cache_location'); } } } // Invoke hook_location_provinces_alter and // hook_location_provinces_COUNTRY_CODE_alter. drupal_alter( array('location_provinces', 'location_provinces_' . $country), $provinces, $country ); return isset($provinces[$country]) ? $provinces[$country] : array(); } // @@@ New in 3.x, document. /** * Get the translated name of a country code. */ function location_country_name($country = 'us') { location_standardize_country_code($country); $countries = location_get_iso3166_list(); if (isset($countries[$country])) { return $countries[$country]; } else { return ''; } } // @@@ New in 3.x, document. /** * Get the full name of a province code. */ function location_province_name($country = 'us', $province = 'xx') { $provinces = location_get_provinces($country); $province = strtoupper($province); if (isset($provinces[$province])) { return $provinces[$province]; } else { // If the province value is not indexed, allow pass-through as-is. // This helps with legacy data migration that may not be stored // in the correct provincial code format. return check_plain($province); } } // @@@ New in 3.x, document. /** * Get a province code from a code or full name and a country. */ function location_province_code($country = 'us', $province = 'xx') { // An array of countries is useful if someone specified multiple countries // in an autoselect for example. // It *is* possibly ambiguous, especially if the province was already a code. // We make an array here for single (the usual case) for code simplicity reasons. if (!is_array($country)) { $country = array($country); } $p = strtoupper($province); foreach ($country as $c) { if ($c == 'xx') { return $province; } $provinces = location_get_provinces($c); foreach ($provinces as $k => $v) { if ($p == strtoupper($k) || $p == strtoupper($v)) { return $k; } } } return ''; } // @@@ New in 3.x, document. /** * Canonicalize a country code. */ function location_standardize_country_code(&$country) { $country = trim($country); // @@@ Double check the validity of this validity check. ;) if (!ctype_alpha($country) || strlen($country) != 2) { $country = 'xx'; return FALSE; } else { $country = strtolower($country); return TRUE; } } /** * Load support for a country. * * This function will load support for a country identified by its two-letter ISO code. * * @param string $country * Two-letter ISO code for country. * * @return bool * TRUE if the file was found and loaded, FALSE otherwise. */ function location_load_country($country) { location_standardize_country_code($country); $file = DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/supported/location.' . $country . '.inc'; if (file_exists($file)) { include_once $file; return TRUE; } return FALSE; } // @@@ New in 3.x, document. /** * Load a general geocoding service. */ function location_load_geocoder($geocoder) { include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/geocoding/' . $geocoder . '.inc'; } /** * Create a single line address. * * @param array $location * The address parts * * @return string * The single line address */ function location_address2singleline($location = array()) { // Check if its a valid address. if (empty($location)) { return ''; } $address = ''; if (!empty($location['street'])) { $address .= $location['street']; } if (!empty($location['city'])) { if (!empty($location['street'])) { $address .= ', '; } $address .= $location['city']; } if (!empty($location['province'])) { if (!empty($location['street']) || !empty($location['city'])) { $address .= ', '; } // @@@ Fix this! if (substr($location['province'], 0, 3) == $location['country'] . '-') { $address .= substr($location['province'], 3); watchdog('Location', 'BUG: Country found in province attribute.'); } else { $address .= $location['province']; } } if (!empty($location['postal_code'])) { if (!empty($address)) { $address .= ' '; } $address .= $location['postal_code']; } if (!empty($location['country'])) { $address .= ', ' . $location['country']; } return $address; } /** * Geocoder list. */ function location_get_general_geocoder_list() { $list = & drupal_static(__FUNCTION__, array()); if (!count($list)) { $files = file_scan_directory( drupal_get_path('module', 'location') . '/geocoding', '/\.inc$/', array('nomask' => '/(\.\.?|CVS|\.svn)$/') ); foreach ($files as $full_path_name => $fileinfo) { $list[] = $fileinfo->name; } } return $list; } /** * The following is an array of all. * * countrycode => country-name pairs as layed out in * ISO 3166-1 alpha-2 */ function location_get_iso3166_list($upper = FALSE) { include_once DRUPAL_ROOT . '/includes/locale.inc'; // Statically cache a version of the core Drupal list of countries // with lower case country codes for use by this module. $countries = & drupal_static(__FUNCTION__); if ($upper) { // Drupal core stores ISO 3166-1 alpha2 codes in upper case, as // per the ISO standard. return country_get_list(); } elseif (!isset($countries)) { // Location module uses lower-case ISO 3166-1 alpha2 codes, so we need // to convert. $countries = array_change_key_case(country_get_list(), CASE_LOWER); } return $countries; }