$location['ip_address']), WATCHDOG_WARNING); } // See if this IP is already on the db $result = db_query('SELECT * FROM {ip_geoloc} WHERE ip_address = :ip', array(':ip' => $location['ip_address'])); $existing_location = $result->fetchAssoc(); if (!$existing_location) { // New entry, insert $location['city'] = utf8_encode($location['city']); $location['formatted_address'] = utf8_encode($location['formatted_address']); ip_geoloc_debug(t('IP Geolocaton: adding new record to db: !location', array('!location' => ip_geoloc_pretty_print($location)))); $full_location = &$location; } else { // When updating, drupal_write_record() does not erase fields not present in $location $empty_location['latitude'] = ''; $empty_location['longitude'] = ''; $empty_location['country'] = ''; $empty_location['country_code'] = ''; $empty_location['region'] = ''; $empty_location['region_code'] = ''; $empty_location['city'] = ''; $empty_location['locality'] = ''; $empty_location['route'] = ''; $empty_location['street_number'] = ''; $empty_location['postal_code'] = ''; $empty_location['administrative_area_level_1'] = ''; $empty_location['formatted_address'] = ''; $location['id'] = $existing_location['id']; $full_location = array_merge($empty_location, $location); ip_geoloc_debug(t('IP Geolocation: updating db with above location')); } try { $result = drupal_write_record('ip_geoloc', $full_location, $existing_location ? array('id') : array()); } catch (PDOException $e) { // May happen when a fields contains illegal characters drupal_set_message(check_plain($e->getMessage()), 'error'); $result = FALSE; } if ($result === FALSE) { drupal_set_message(t('IP Geolocation: could not save location to db: !location', array('!location' => ip_geoloc_pretty_print($full_location))), 'error'); } } return $result; } /** * Outputs an HTML div placeholder into which will be injected a map. * * The map is centered on the supplied lat,long coordinates. * Handy for use in Views. * * @param * latitude, string or double, e.g. "-37.87" or -37.87 * @param * longitude, string or double, e.g. "144.98" or 144.98 * @param * div_id, id of the div placeholder, can be anything as long as it's unique * @param * style, CSS style applied to the div, e.g "height:250px; width:300px" */ function ip_geoloc_output_map($latitude, $longitude, $div_id = 'ip-geoloc-map', $style = '', $balloon_text = '') { $javascript_files = drupal_add_js(); if (empty($javascript_files[IP_GEOLOC_GOOGLE_MAPS_GPS])) { // Only add non-GPS version when GPS version isn't already there drupal_add_js(IP_GEOLOC_GOOGLE_MAPS, array('group' => JS_LIBRARY)); } drupal_add_js(drupal_get_path('module', 'ip_geoloc') . '/js/ip_geoloc_gmap.js'); $script_code = "displayGMap($latitude, $longitude, '$div_id', '$balloon_text');"; drupal_add_js($script_code, array('type' => 'inline', 'scope' => 'footer')); $map_placeholder = "
'; return $map_placeholder; } /** * Outputs an HTML div placeholder into which is injected a map. * * The map is centered on the visitor's current location and features a position * marker, which when clicked reveals latitude and longitude, as well as the * street address and the accuracy of the position shown. * * Note this function will result in the visitor being prompted to share their * location. * * @param * $div_id, id of the div placeholder, can be anything as long as it's unique * @param * $map_options, as a JSON string e.g '{"mapTypeId":"roadmap", "zoom":15}' * @param * $map_style, CSS style applied to the div, e.g "height:200px; width:300px" * @param * $latitude, fallback value, eg. when error or user declines to share location * @param * $longitude, fallback value, eg. when error or user declines to share location */ function ip_geoloc_output_map_current_location($div_id = 'ip-geoloc-map-current-location', $map_options = NULL, $map_style = NULL, $latitude = NULL, $longitude = NULL) { $js_options = array('group' => JS_LIBRARY); drupal_add_js(IP_GEOLOC_GEO_JS, $js_options); drupal_add_js(IP_GEOLOC_GOOGLE_MAPS_GPS, $js_options); drupal_add_js(drupal_get_path('module', 'ip_geoloc') . '/js/ip_geoloc_gmap_current_loc.js'); if (!isset($map_options)) { $map_options = IP_GEOLOC_CURRENT_VISITOR_MAP_OPTIONS; } $settings = array( 'ip_geoloc_current_location_map_div' => $div_id, 'ip_geoloc_current_location_map_options' => drupal_json_decode($map_options), 'ip_geoloc_current_location_map_latlng' => array($latitude, $longitude) ); drupal_add_js($settings, 'setting'); if (!isset($map_style)) { $map_style = IP_GEOLOC_MAP_DIV_DEFAULT_STYLE; } $map_placeholder = "
'; return $map_placeholder; } /** * Outputs an HTML div placeholder into which will be injected a map. * * The locations to be mapped are supplied as an array of lat,long coordinates. * * @param * $locations, array of location objects each containing lat/long pair * and optionally address, visit count and last visit to appear when the * marker is clicked * @param * $div_id, id of the div placeholder, can be anything as long as it's unique * @param * $map_options, as a JSON string, .e.g '{"mapTypeId":"roadmap", "zoom":15}' * @param * $map_style, CSS style applied to the div, e.g "height:250px; width:300px" */ function ip_geoloc_output_map_multi_visitor($locations, $div_id = 'ip-geoloc-map-multi-locations', $map_options = NULL, $map_style = NULL) { $javascript_files = drupal_add_js(); if (empty($javascript_files[IP_GEOLOC_GOOGLE_MAPS_GPS])) { // Only add non-GPS version when GPS version isn't already there drupal_add_js(IP_GEOLOC_GOOGLE_MAPS, array('group' => JS_LIBRARY)); } drupal_add_js(drupal_get_path('module', 'ip_geoloc') . '/js/ip_geoloc_gmap_multi_visitor.js'); if (!isset($map_options)) { $map_options = IP_GEOLOC_RECENT_VISITORS_MAP_OPTIONS; } $settings = array( 'ip_geoloc_locations' => $locations, 'ip_geoloc_multi_location_map_div' => $div_id, 'ip_geoloc_multi_location_map_options' => drupal_json_decode($map_options) ); drupal_add_js($settings, 'setting'); if (!isset($map_style)) { $map_style = IP_GEOLOC_MAP_DIV_DEFAULT_STYLE; } $map_placeholder = "
'; return $map_placeholder; } /** * Outputs an HTML div placeholder into which will be injected a Google map. * * The locations to be mapped are supplied as an array of location objects, * each with lat,long coordinates and optional balloon text. * * Note this function will result in the visitor being prompted to share their * location. * * @param * $locations, array of location objects each containing lat/long pair * and optional balloon text. * @param * $div_id, id of the div placeholder, can be anything as long as it's unique * @param * $map_options, as a JSON string, .e.g '{"mapTypeId":"roadmap", "zoom":15}' * @param * $map_style, CSS style applied to the div, e.g "height:250px; width:300px" * @parem * $marker_color, default color used for all locations that haven't had their * color overridden, defaults to Google default (i.e. red marker with dot). * @param * $visitor_marker, one of FALSE (no marker), TRUE (standard red marker) or * a 'RRGGBB' color code * @param * $center_option, one of: * 0: fixed cenrer, must be provided thorugh $map_options) * 1: auto-center the map on the first location in the $locations array * 2: auto-center the map on the visitor's current location * @param * $center_latlng, array of length 2 with lat/long coords used as a backup * when $visitor_marker is set or $center_option == 2 and location could not * be determined or $visitor_location_gps == FALSE * @param * $visitor_location_gps, whether the HTML5-style location provider is to be * used, if FALSE $center_latlng is used */ function ip_geoloc_output_map_multi_location($locations, $div_id = 'ip-geoloc-map-multi-locations', $map_options = NULL, $map_style = NULL, $marker_color = NULL, $visitor_marker = TRUE, $center_option = 0, $center_latlng = array(0, 0), $visitor_location_gps = TRUE) { if ($visitor_location_gps) { drupal_add_js(IP_GEOLOC_GEO_JS, array('group' => JS_LIBRARY)); drupal_add_js(IP_GEOLOC_GOOGLE_MAPS_GPS, array('group' => JS_LIBRARY)); } else { $javascript_files = drupal_add_js(); if (empty($javascript_files[IP_GEOLOC_GOOGLE_MAPS_GPS])) { // Only add non-GPS version when GPS version isn't already there drupal_add_js(IP_GEOLOC_GOOGLE_MAPS, array('group' => JS_LIBRARY)); } } drupal_add_js(drupal_get_path('module', 'ip_geoloc') . '/js/ip_geoloc_gmap_multi_loc.js'); if (!isset($map_options)) { $map_options = IP_GEOLOC_RECENT_VISITORS_MAP_OPTIONS; } $marker_directory = variable_get('ip_geoloc_marker_directory', drupal_get_path('module', 'ip_geoloc') . '/markers'); $marker_directory = file_create_url($marker_directory); $marker_dimensions = explode('x', variable_get('ip_geoloc_marker_dimensions', '21 x 34')); $marker_width = (int)$marker_dimensions[0]; $marker_height = (int)$marker_dimensions[1]; ip_geoloc_debug(t('IP Geolocation: passing the following to Google Maps:')); ip_geoloc_debug(t('- map options %options:', array('%options' => $map_options))); ip_geoloc_debug(t('- center option: @option' , array('@option' => $center_option))); ip_geoloc_debug(t('- visitor marker: %marker', array('%marker' => $visitor_marker))); ip_geoloc_debug(t('- use GPS: @gps', array('@gps' => (bool)$visitor_location_gps))); ip_geoloc_debug(t('- visitor location fallback: (@lat, @lng)', array( '@lat' => empty($center_latlng[0]) ? '-' : $center_latlng[0], '@lng' => empty($center_latlng[1]) ? '-' : $center_latlng[1] ))); ip_geoloc_debug(t('- marker directory : %dir', array('%dir' => $marker_directory))); ip_geoloc_debug(t('- marker dimensions : w@w x h@h px', array('@w' => $marker_width, '@h' => $marker_height))); ip_geoloc_debug(t('- @count locations found', array('@count' => count($locations)))); foreach ($locations as $location) { $color = isset($location->marker_color) ? $location->marker_color : t('default') . " ($marker_color)"; $msg = '- ' . t('marker') . ' ' . $color . ' (' . $location->latitude . ', ' . $location->longitude . ')
' . $location->balloon_text . '
'; ip_geoloc_debug($msg); } $settings = array( 'ip_geoloc_locations' => $locations, 'ip_geoloc_multi_location_map_div' => $div_id, 'ip_geoloc_multi_location_map_options' => drupal_json_decode($map_options), 'ip_geoloc_multi_location_center_option' => $center_option, 'ip_geoloc_multi_location_center_latlng' => $center_latlng, 'ip_geoloc_multi_location_visitor_marker' => $visitor_marker, 'ip_geoloc_multi_location_visitor_location_gps' => $visitor_location_gps, 'ip_geoloc_multi_location_marker_directory' => $marker_directory, 'ip_geoloc_multi_location_marker_width' => $marker_width, 'ip_geoloc_multi_location_marker_height' => $marker_height, 'ip_geoloc_multi_location_marker_default_color' => $marker_color ); drupal_add_js($settings, 'setting'); if (!isset($map_style)) { $map_style = IP_GEOLOC_MAP_DIV_DEFAULT_STYLE; } $map_placeholder = "
'; return $map_placeholder; } /** * Uses AJAX to return in $_POST info about the visitor's current location. * * Note: this function will result in the browser prompting the visitor to share * their location, which they may or may not accept. * * This works via an asynchronous javascript call, so the result is not * immediately available on return from this function, hence the $menu_callback. * Upon page load the included javascript will, when ready, instigate an AJAX * call to the $menu_callback, which should invoke a function to retrieve the * lat/long and address values from the $_POST variable. * See ip_geoloc_current_location_ajax_recipient() for an example. * * Note: will result in a HTTP error 503 when the site is in maintenance mode, * as in maintenance mode menu items are not available. */ function ip_geoloc_get_current_location($menu_callback = 'ip-geoloc-current-location') { $js_options = array('group' => JS_LIBRARY); drupal_add_js(IP_GEOLOC_GEO_JS, $js_options); drupal_add_js(IP_GEOLOC_GOOGLE_MAPS_GPS, $js_options); drupal_add_js(drupal_get_path('module', 'ip_geoloc') . '/js/ip_geoloc_current_location.js'); $refresh_page = variable_get('ip_geoloc_page_refresh', FALSE) && !path_is_admin($_GET['q']); //if ($refresh_page) { //No longer using this jQuery API, see file js/ip_geoloc_current_location.js //drupal_add_js(IP_GEOLOC_JQUERY_BLOCK_UI, $js_options); //} $settings = array( 'ip_geoloc_menu_callback' => $menu_callback, 'ip_geoloc_refresh_page' => $refresh_page ); drupal_add_js($settings, 'setting'); } /** * Returns the location details associated with the supplied IP address. * * Performs a lookup in IP Geolocation's own database to see if the supplied IP * address has visited already and if so returns their location details (as an * array). If the IP address is not yet in the IP Geolocation database, then * retrieve lat/long using either Smart IP or GeoIP API (if enabled) and * reverse-geocode the lat/long (if the Google Maps service is enabled) into a * location. If the third argument is TRUE, then store the new location. * * @param * $ip_address * @param * $resample, if set to TRUE, ignore any existing location data for this * IP address and retrieve the latest * @param * $store, if TRUE, store the new or resampled location on the db * @param * $reverse_geocode, applies only when the supplied IP address is not * yet on the database or $resample=TRUE; use TRUE, FALSE or NULL; TRUE will * produce greater detail in the location returned; if NULL or omitted the * value is taken from the tick box on the IP Geolocation configuration page. * Reverse-geocoding is subject to a Google-imposed limit of 2500 calls per * day from the same server IP address. * @return * location as an array */ function ip_geoloc_get_location_by_ip($ip_address, $resample = FALSE, $store = FALSE, $reverse_geocode = NULL) { $location = $resample ? NULL : db_query('SELECT * FROM {ip_geoloc} WHERE ip_address = :ip_address', array(':ip_address' => $ip_address))->fetchAssoc(); if (empty($location)) { $location = array('ip_address' => $ip_address); if (ip_geoloc_use_smart_ip_if_enabled($location) || ip_geoloc_use_geoip_api_if_enabled($location)) { if (!isset($reverse_geocode)) { $reverse_geocode = variable_get('ip_geoloc_google_to_reverse_geocode', TRUE); } if ($reverse_geocode && isset($location['latitude']) && isset($location['longitude'])) { if ($google_address = ip_geoloc_reverse_geocode($location['latitude'], $location['longitude'])) { // Should we clear out whatever Smart IP or GeoIP put in the $location // to avoid fields contradicting eachother? Eg. Google normally // returns 'locality', whereas Smart IP and GeoIP return 'city'. // $location = array('ip_address' => $ip_address); ip_geoloc_flatten_google_address($google_address, $location); } } if ($store) { ip_geoloc_store_location($location); // calls drupal_alter() } else { drupal_alter('get_ip_geolocation', $location); } } } return $location; } /** * Returns the formatted address reverse-geocoded from the supplied lat,long. * * See the CALLER BEWARE note at ip_geoloc_reverse_geocode(). * * @param * latitude, string or double, e.g. "-37.87" or -37.87 * @param * longitude, string or double, e.g. "144.98" or 144.98 */ function ip_geoloc_get_address($latitude, $longitude) { return $google_address = ip_geoloc_reverse_geocode($latitidue, $longitude) ? $google_address['formatted_address'] : ''; } /** * Uses the Google webservice to retrieve address information based on lat/long. * * Effectively makes calls of this form: * http://maps.googleapis.com/maps/api/geocode/json?sensor=false&latlng=-37,144 * * CALLER BEWARE: * This is a server-side, as opposed to client-side call. If you want to call * this function repeatedly, remember that Google imposes a limit of 2500 calls * from the same IP address per 24 hours. It may return an OVER_QUERY_LIMIT * response. * * @param * latitude, string or double, e.g. "-37.87" or -37.87 * @param * longitude, string or double, e.g. "144.98" or 144.98 */ function ip_geoloc_reverse_geocode($latitude, $longitude) { if (empty($latitude) || empty($latitude)) { drupal_set_message(t('IP Geolocation: cannot reverse-geocode to address as no lat/long was specified.'), 'warning'); return FALSE; } $query_start = microtime(TRUE); $url = IP_GEOLOC_GOOGLE_MAPS_SERVER . "&latlng=$latitude,$longitude"; $response = drupal_http_request($url); if (!empty($response->error)) { $msg_args = array('%url' => $url, '@code' => $response->code, '%error' => $response->error); drupal_set_message(t('IP Geolocation: the HTTP request %url returned the following error (code @code): %error.', $msg_args), 'error'); watchdog('IP Geolocation', 'Error (code @code): %error. Request: %url', $msg_args, WATCHDOG_ERROR); return FALSE; } $data = drupal_json_decode($response->data); if ($data['status'] == 'OVER_QUERY_LIMIT') { $msg_args = array('%url' => $url); if (user_access('administer site configuration')) { drupal_set_message(t('IP Geolocation: Server is over its query limit. Request: %url', $msg_args), 'error'); } watchdog('IP Geolocation', 'Server is over its query limit. Request: %url', $msg_args, WATCHDOG_ERROR); return FALSE; } if ($data['status'] == 'ZERO_RESULTS' || !isset($data['results'][0])) { $msg_args = array('@protocol' => $response->protocol, '%url' => $url); drupal_set_message(t('IP Geolocation: the @protocol request %url succeeded, but returned no results.', $msg_args), 'warning'); watchdog('IP Geolocation', 'No results from @protocol request %url.', $msg_args, WATCHDOG_WARNING); return FALSE; } if ($data['status'] != 'OK') { $msg_args = array('%url' => $url, '%error' => $data['status']); drupal_set_message(t('IP Geolocation: unknown error %error. Request: %url..', $msg_args), 'error'); watchdog('IP Geolocation', 'Unknown error %error. Request: %url.', $msg_args, WATCHDOG_ERROR); return FALSE; } $google_address = $data['results'][0]; if (empty($google_address['formatted_address'])) { $msg_args = array('@lat' => $latitude, '@long' => $longitude); ip_geoloc_debug(t('IP Geolocation: (@lat, @long) could not be reverse-geocoded to a street address.', $msg_args), 'warning'); watchdog('IP Geolocation', '(@lat, @long) could not be reverse-geocoded to a street address..', $msg_args, WATCHDOG_WARNING); } else { $sec = number_format(microtime(TRUE) - $query_start, 1); $msg_args = array( '@lat' => $latitude, '@long' => $longitude, '%sec' => $sec, '%address' => $google_address['formatted_address'] ); ip_geoloc_debug(t('IP Geolocation: %address reverse-geocoded from (@lat, @long) in %sec s.', $msg_args)); watchdog('IP Geolocation', '%address reverse-geocoded from (@lat, @long) in %sec s.', $msg_args, WATCHDOG_INFO); } return $google_address; } /** * Return the visitor's location as currently stored in the session. * @return array */ function ip_geoloc_get_visitor_location() { return _ip_geoloc_get_session_value('location'); } /** * Calculate the center of the supplied locations using one of two algorithms. * * The first algorithm returns the center of the rectangle whose horizontal * sides pass through the top and bottom locations in the set, while its * vertical sides pass through the left-most and right-most locations. * * The second algorithm returns the center of gravity of all supplied locations. * The second algorithn is therefore sensitive to location clusters. This may * be what you want, or it may be what you want to avoid. * * @param $locations, array of location objects each with latitude and longitude * @param $center_of_gravity, if TRUE use the center of gravity algorithm * @return array containing latitude and longitude of the center */ function ip_geoloc_center_of_locations($locations, $center_of_gravity = FALSE) { if (empty($locations)) { return array(0.0, 0.0); } if ($center_of_gravity) { // Because longitude turns back on itself, cannot simply average coords. $count = 0; $x = $y = $z = 0.0; foreach ($locations as $location) { $lat = deg2rad($location->latitude); $lng = deg2rad($location->longitude); // Convert to Cartesian coords and total the 3 dimensions independently. $x += cos($lat) * cos($lng); $y += cos($lat) * sin($lng); $z += sin($lat); $count++; } $x /= $count; $y /= $count; $z /= $count; $center_lat = atan2($z, sqrt($x * $x + $y * $y)); $center_lng = atan2($y, $x); return array(rad2deg($center_lat), rad2deg($center_lng)); } // Alternative method based on top & bottom latitude and left & right longitude $location = reset($locations); $top = $bottom = $location->latitude; $left = $right = $location->longitude; while ($location = next($locations)) { $lat = $location->latitude; if ($lat > $top) { $top = $lat; } elseif ($lat < $bottom) { $bottom = $lat; } $lng = $location->longitude; if ($lng < $left) { $left = $lng; } elseif ($lng > $right) { $right = $lng; } } $center_lat = 0.5 * ($top + $bottom); $center_lng = 0.5 * ($left + $right); if ($right - $left > 180) { // If the angle between right and left is greater than 180, then averaging // is still ok, provided we flip over to the opposite end of the world. $center_lng = ($center_lng > 0.0) ? $center_lng - 180.0 : $center_lng + 180.0; } return array($center_lat, $center_lng); } /** * Returns the distance (in meters) between two points on the earth's surface. * * The points are defined by their lat/long coordinates. If the second point is * omitted, the current visitor's location is used, as taken from their session * data. * * @param * array $location, must contain 'latitude' and 'longitude' keys and values * @param * $ref_location, if an array, must contain 'latitude' and 'longitude' keys * and values, otherwise defaults to ip_geoloc_get_visitor_location() * @return * float, distance in meters. */ function ip_geoloc_distance($location, $ref_location = 'current visitor') { if (!is_array($ref_location)) { $ref_location = ip_geoloc_get_visitor_location(); } if (empty($ref_location)) { return '?'; } if (is_numeric($location['longitude']) && is_numeric($location['latitude']) && is_numeric($ref_location['longitude']) && is_numeric($ref_location['latitude'])) { return ip_geoloc_earth_distance($location['longitude'], $location['latitude'], $ref_location['longitude'], $ref_location['latitude']); } return '?'; } /** * Returns the distance between two points on the earth's surface. * * The points are defined by their lat/long coordinates. * * Gratefully copied from the http://drupal.org/project/location module, thus * ensuring compatibility of results. * * @param * longitude1 * @param * latitude1 * @param * longitude2 * @param * latitude2 * @return * distance in meters * * @see http://en.wikipedia.org/wiki/Great-circle_distance */ function ip_geoloc_earth_distance($longitude1, $latitude1, $longitude2, $latitude2) { $long1 = deg2rad($longitude1); $lat1 = deg2rad($latitude1); $long2 = deg2rad($longitude2); $lat2 = deg2rad($latitude2); // $long_factor = cos($long1)*cos($long2) + sin($long1)*sin($long2); // This is identical to this $long_factor = cos($long1 - $long2) $long_factor = cos($long1 - $long2); $cosangle = cos($lat1)*cos($lat2)*$long_factor + sin($lat1)*sin($lat2); $radius = ip_geoloc_earth_radius(0.5 * ($latitude1 + $latitude2)); $distance = acos($cosangle) * $radius; /* if ($distance < 1000) { // see http://en.wikipedia.org/wiki/Haversine_formula $sinlat = sin(0.5*($lat1 - $lat2)); $sinlong = sin(0.5*($long1 - $long2)); $sinangle = $sinlat*$sinlat + cos($long1)*cos($long2)*$sinlong*$sinlong; $distance = 2.0 * asin(sqrt($sinangle)) * $radius; } */ return $distance; } function ip_geoloc_earth_radius($latitude) { $lat = deg2rad($latitude); $x = cos($lat)/ip_geoloc_earth_radius_semimajor(); $y = sin($lat)/ip_geoloc_earth_radius_semiminor(); return 1.0 / (sqrt($x*$x + $y*$y)); } function ip_geoloc_earth_radius_semimajor() { return 6378137.0; } function ip_geoloc_earth_radius_semiminor() { return (ip_geoloc_earth_radius_semimajor() * (1.0 - ip_geoloc_earth_flattening())); } function ip_geoloc_earth_flattening() { return (1.0/298.257223563); }