diff --git a/earth.inc b/earth.inc
new file mode 100644
index 0000000..d9f72e7
--- /dev/null
+++ b/earth.inc
@@ -0,0 +1,184 @@
+,
+ * the creator of these routines, Ka-Ping Yee, authorized these routines to be
+ * distributed under the GPL.
+ */
+
+/**
+ * @file
+ * Trigonometry for calculating geographical distances.
+ * All function arguments and return values measure distances in metres
+ * and angles in degrees. The ellipsoid model is from the WGS-84 datum.
+ * Ka-Ping Yee, 2003-08-11
+ */
+
+//$earth_radius_semimajor = 6378137.0;
+//$earth_flattening = 1/298.257223563;
+//$earth_radius_semiminor = $earth_radius_semimajor * (1 - $earth_flattening);
+//$earth_eccentricity_sq = 2*$earth_flattening - pow($earth_flattening, 2);
+
+// I don't know what's up: PHP is hating on my global variables (commented out above),
+// so I have to write functions that return them! (-Ankur)
+// Commenting out the global variables above and replacing them with functions that
+// return the same values is the only thing I changed since, for some reason, my
+// PHP wasn't acknowledging these global variables.
+// This library is an original implementation of UCB CS graduate student, Ka-Ping Yee (http://www.zesty.ca).
+
+function earth_radius_semimajor() {
+ return 6378137.0;
+}
+
+function earth_flattening() {
+ return (1/298.257223563);
+}
+
+function earth_radius_semiminor() {
+ return (earth_radius_semimajor() * (1 - earth_flattening()));
+}
+
+function earth_eccentricity_sq() {
+ return (2*earth_flattening() - pow(earth_flattening(), 2));
+}
+
+// Latitudes in all of U. S.: from -7.2 (American Samoa) to 70.5 (Alaska).
+// Latitudes in continental U. S.: from 24.6 (Florida) to 49.0 (Washington).
+// Average latitude of all U. S. zipcodes: 37.9.
+
+function earth_radius($latitude=37.9) {
+ //global $earth_radius_semimajor, $earth_radius_semiminor;
+ // Estimate the Earth's radius at a given latitude.
+ // Default to an approximate average radius for the United States.
+
+ $lat = deg2rad($latitude);
+
+ $x = cos($lat)/earth_radius_semimajor();
+ $y = sin($lat)/earth_radius_semiminor();
+ return 1 / (sqrt($x*$x + $y*$y));
+}
+
+function earth_xyz($longitude, $latitude, $height = 0) {
+ // Convert longitude and latitude to earth-centered earth-fixed coordinates.
+ // X axis is 0 long, 0 lat; Y axis is 90 deg E; Z axis is north pole.
+ //global $earth_radius_semimajor, $earth_eccentricity_sq;
+ $long = deg2rad($longitude);
+ $lat = deg2rad($latitude);
+
+ $coslong = cos($long);
+ $coslat = cos($lat);
+ $sinlong = sin($long);
+ $sinlat = sin($lat);
+ $radius = earth_radius_semimajor() /
+ sqrt(1 - earth_eccentricity_sq() * $sinlat * $sinlat);
+ $x = ($radius + $height) * $coslat * $coslong;
+ $y = ($radius + $height) * $coslat * $sinlong;
+ $z = ($radius * (1 - earth_eccentricity_sq()) + $height) * $sinlat;
+ return array($x, $y, $z);
+}
+
+function earth_arclength($angle, $latitude=37.9) {
+ // Convert a given angle to earth-surface distance.
+ return deg2rad($angle) * earth_radius($latitude);
+}
+
+function earth_distance($longitude1, $latitude1, $longitude2, $latitude2) {
+ // Estimate the earth-surface distance between two locations.
+ $long1 = deg2rad($longitude1);
+ $lat1 = deg2rad($latitude1);
+ $long2 = deg2rad($longitude2);
+ $lat2 = deg2rad($latitude2);
+ $radius = earth_radius(($latitude1 + $latitude2) / 2);
+
+ $cosangle = cos($lat1)*cos($lat2) *
+ (cos($long1)*cos($long2) + sin($long1)*sin($long2)) +
+ sin($lat1)*sin($lat2);
+ return acos($cosangle) * $radius;
+}
+
+/*
+ * Returns the SQL fragment needed to add a column called 'distance'
+ * to a query that includes the location table
+ *
+ * @param $longitude The measurement point
+ * @param $latibude The measurement point
+ * @param $tbl_alias If necessary, the alias name of the location table to work from. Only required when working with named {location} tables
+ */
+function earth_distance_sql($longitude, $latitude, $tbl_alias = '') {
+ // Make a SQL expression that estimates the distance to the given location.
+ $long = deg2rad($longitude);
+ $lat = deg2rad($latitude);
+ $radius = earth_radius($latitude);
+
+ // If the table alias is specified, add on the separator.
+ $tbl_alias = empty($tbl_alias) ? $tbl_alias : ($tbl_alias .'.');
+
+ $coslong = cos($long);
+ $coslat = cos($lat);
+ $sinlong = sin($long);
+ $sinlat = sin($lat);
+ return "(IFNULL(ACOS($coslat*COS(RADIANS({$tbl_alias}latitude))*($coslong*COS(RADIANS({$tbl_alias}longitude)) + $sinlong*SIN(RADIANS({$tbl_alias}longitude))) + $sinlat*SIN(RADIANS({$tbl_alias}latitude))), 0.00000)*$radius)";
+}
+
+/**
+ * @todo This function uses earth_asin_safe so is not accurate for all possible
+ * parameter combinations. This means this function doesn't work properly
+ * for high distance values. This function needs to be re-written to work properly for
+ * larger distance values. See http://drupal.org/node/821628
+ */
+function earth_longitude_range($longitude, $latitude, $distance) {
+ // Estimate the min and max longitudes within $distance of a given location.
+ $long = deg2rad($longitude);
+ $lat = deg2rad($latitude);
+ $radius = earth_radius($latitude);
+
+ $angle = $distance / $radius;
+ $diff = earth_asin_safe(sin($angle)/cos($lat));
+ $minlong = $long - $diff;
+ $maxlong = $long + $diff;
+ if ($minlong < -pi()) { $minlong = $minlong + pi()*2; }
+ if ($maxlong > pi()) { $maxlong = $maxlong - pi()*2; }
+ return array(rad2deg($minlong), rad2deg($maxlong));
+}
+
+function earth_latitude_range($longitude, $latitude, $distance) {
+ // Estimate the min and max latitudes within $distance of a given location.
+ $long = deg2rad($longitude);
+ $lat = deg2rad($latitude);
+ $radius = earth_radius($latitude);
+
+ $angle = $distance / $radius;
+ $minlat = $lat - $angle;
+ $maxlat = $lat + $angle;
+ $rightangle = pi()/2;
+ if ($minlat < -$rightangle) { // wrapped around the south pole
+ $overshoot = -$minlat - $rightangle;
+ $minlat = -$rightangle + $overshoot;
+ if ($minlat > $maxlat) { $maxlat = $minlat; }
+ $minlat = -$rightangle;
+ }
+ if ($maxlat > $rightangle) { // wrapped around the north pole
+ $overshoot = $maxlat - $rightangle;
+ $maxlat = $rightangle - $overshoot;
+ if ($maxlat < $minlat) { $minlat = $maxlat; }
+ $maxlat = $rightangle;
+ }
+ return array(rad2deg($minlat), rad2deg($maxlat));
+}
+
+/**
+ * This is a helper function to avoid errors when using the asin() PHP function.
+ * asin is only real for values between -1 and 1.
+ * If a value outside that range is given it returns NAN (not a number), which
+ * we don't want to happen.
+ * So this just rounds values outside this range to -1 or 1 first.
+ *
+ * This means that calculations done using this function with $x outside the range
+ * will not be accurate. The alternative though is getting NAN, which is an error
+ * and won't give accurate results anyway.
+ */
+function earth_asin_safe($x) {
+ return asin(max(-1, min($x, 1)));
+}
diff --git a/geolocation_proximity.info b/geolocation_proximity.info
index dfa1b95..973f472 100644
--- a/geolocation_proximity.info
+++ b/geolocation_proximity.info
@@ -8,3 +8,4 @@ dependencies[] = views
files[] = handlers/geolocation_proximity_views_handler_filter_distance.inc
files[] = handlers/geolocation_proximity_views_handler_field_distance.inc
+files[] = handlers/geolocation_proximity_views_handler_argument_distance.inc
diff --git a/geolocation_proximity.module b/geolocation_proximity.module
index 058c191..4af5907 100644
--- a/geolocation_proximity.module
+++ b/geolocation_proximity.module
@@ -33,6 +33,9 @@ function geolocation_proximity_field_views_data_alter(&$result, $field, $module)
// This is use by the table display plugin.
'click sortable' => TRUE,
),
+ 'argument' => array(
+ 'handler' => 'geolocation_proximity_views_handler_argument_distance',
+ ),
);
}
}
@@ -55,6 +58,9 @@ function geolocation_proximity_views_data_alter(&$data) {
$data[$field_name][$field['field_name']]['filter'] = array(
'handler' => 'geolocation_proximity_views_handler_filter_distance',
);
+ $data[$field_name][$field['field_name']]['argument'] = array(
+ 'handler' => 'geolocation_proximity_views_handler_argument_distance',
+ );
}
return $data;
}
@@ -97,4 +103,4 @@ function _proximity_sql_fragment($filter_lat, $filter_lng, $field_latsin, $field
* $field_latsin
) * $earth_radius
)";
-}
\ No newline at end of file
+}
diff --git a/handlers/geolocation_proximity_views_handler_argument_distance.inc b/handlers/geolocation_proximity_views_handler_argument_distance.inc
new file mode 100644
index 0000000..bf030bd
--- /dev/null
+++ b/handlers/geolocation_proximity_views_handler_argument_distance.inc
@@ -0,0 +1,353 @@
+field_alias = $this->options['id'] . '_argument'; // Removed: We can use $this->real_field;
+ }
+
+ /**
+ * views_handler_filter::option_definition
+ */
+ function option_definition() {
+ $options = parent::option_definition();
+ $options['datasource'] = array('default' => 'geolocation', );
+ $options['operator'] = array('default' => '<');
+ // from location module:
+ $options['search_units'] = array('default' => 'km');
+ $options['search_distance'] = array('default' => '100');
+ $options['search_method'] = array('default' => 'mbr');
+ $options['search_country'] = array('default' => variable_get('site_default_country', NULL));
+ $options['type'] = array('default' => 'latlon');
+ return $options;
+ }
+
+ /**
+ * Display the filter on the administrative summary
+ */
+ function admin_summary() {
+ return FALSE;
+ }
+
+ /**
+ * Add a form elements to select options for this argument.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['type'] = array(
+ '#title' => t('Coordinate Type'),
+ '#type' => 'select',
+ '#options' => array(
+ 'postal' => t('Postal Code (Zipcode)'),
+ 'latlon' => t('Decimal Latitude and Longitude coordinates, comma delimited'),
+ ),
+ '#default_value' => $this->options['type'],
+ '#description' => t('Type of center point.') . '
'
+ . t('Postal code argument format (requires Geocoder module): country_postcode_distance or postcode_distance') . '
'
+ . t('Lat/Lon argument format: lat,lon_distance') . '
'
+ . t('where distance is either a number or a comma delimited pair of decimal degrees'),
+ );
+
+ // Source of lat/lon (taken from gmap.module)
+ $form['datasource'] = array(
+ '#type' => 'select',
+ '#title' => t('Data Source'),
+ '#options' => array(
+ 'location' => t('Location.module'),
+ 'geofield' => t('Geofield.module'),
+ 'geolocation' => t('Geolocation.module'),
+ //'fields' => t('Choose latitude and longitude fields'),
+ //'geocode' => t('Just-in-time geocoding on field named "address"'),
+ ),
+ '#default_value' => $this->options['datasource'],
+ '#multiple' => FALSE,
+ );
+
+ $form['latfield'] = array(
+ '#title' => t('Latitude field'),
+ '#description' => t('Format must be degrees decimal.'),
+ '#type' => 'select',
+ '#options' => $field_options,
+ '#default_value' => $this->options['latfield'],
+ '#process' => array('ctools_dependent_process'),
+ '#dependency' => array('edit-style-options-datasource' => array('fields')),
+ );
+
+ $form['lonfield'] = array(
+ '#title' => t('Longitude field'),
+ '#description' => t('Format must be degrees decimal.'),
+ '#type' => 'select',
+ '#options' => $field_options,
+ '#default_value' => $this->options['lonfield'],
+ '#process' => array('ctools_dependent_process'),
+ '#dependency' => array('edit-style-options-datasource' => array('fields')),
+ );
+
+ $form['geofield'] = array(
+ '#title' => t('Geofield field'),
+ '#description' => t('Select the Geofield source field.'),
+ '#type' => 'select',
+ '#options' => $field_options,
+ '#default_value' => $this->options['geofield'],
+ '#process' => array('ctools_dependent_process'),
+ '#dependency' => array('edit-style-options-datasource' => array('geofield')),
+ );
+
+ // Units used.
+ $form['search_units'] = array(
+ '#type' => 'select',
+ '#title' => t('Distance unit'),
+ '#options' => array(
+ 'km' => t('Kilometers'),
+ 'm' => t('Meters'),
+ 'mile' => t('Miles'),
+ 'dd' => t('Decimal degrees'),
+ ),
+ '#default_value' => $this->options['search_units'],
+ '#description' => t('Select the unit of distance. Decimal degrees should be comma delimited.'),
+ );
+
+ $form['search_method'] = array(
+ '#title' => t('Method'),
+ '#type' => 'select',
+ '#options' => array(
+ 'dist' => t('Circular Proximity'),
+ 'mbr' => t('Rectangular Proximity'),
+ ),
+ '#default_value' => $this->options['search_method'],
+ '#description' => t('Method of determining proximity. Please note that Circular Proximity does not work with Decimal degrees.'),
+ );
+
+ $form['search_country'] = array(
+ '#title' => t('Default country'),
+ '#type' => 'select',
+ '#options' => array(
+ 'none' => t('Determine from zipcode/location'),
+ 'default' => t('Default site country'),
+ ),
+ '#default_value' => $this->options['search_country'],
+ '#description' => t('For Postal Code (Zipcode) type, the method of determining the country, if not provided in argument.'),
+ );
+ }
+
+ /**
+ * Set up the query for this argument.
+ *
+ * The argument sent may be found at $this->argument.
+ */
+ function query($group_by = FALSE) {
+ // Get and process argument.
+ $this->value = array();
+ foreach ($this->view->argument as $argument) {
+ if ($argument->field == 'field_geolocation_distance') {
+ if ($this->options['type'] == 'latlon') {
+ // Argument like '50.91,4.43_50' (Brussels area)
+ $arg_parts = explode('_', $this->view->args[$argument->position]);
+ list($coords, $this->value['search_distance']) = $arg_parts;
+ list($this->value['latitude'], $this->value['longitude']) = explode(',', $coords);
+ }
+ elseif ($this->options['type'] == 'postal') {
+ if (module_exists('geocoder')) {
+ include_once(DRUPAL_ROOT . '/' . drupal_get_path('module', 'geocoder') . '/plugins/geocoder_handler/google.inc');
+ }
+ else {
+ watchdog(WATCHDOG_ERROR, t('The Geocoder module must be installed to use this handler.'));
+ return;
+ }
+
+ $arg_parts = explode('_', $this->view->args[$argument->position]);
+ if (count($arg_parts) == 3) {
+ // Argument like 'BE_Brussels_50'
+ $this->value['country'] = drupal_strtolower($arg_parts[0]);
+ $this->value['postal_code'] = $arg_parts[1];
+ $this->value['search_distance'] = $arg_parts[2];
+ $google_address = $this->value['postal_code'] . ', ' . $this->value['country'];
+ }
+ else {
+ // Argument like 'Brussels_50'
+ $this->value['postal_code'] = $arg_parts[0];
+ $this->value['search_distance'] = $arg_parts[1];
+ $google_address = $this->value['postal_code'];
+ }
+
+ $google_point = geocoder_google($google_address, array('address' => $google_address));
+ if ($google_point) {
+ $this->value['latitude'] = $google_point->coords[1];
+ $this->value['longitude'] = $google_point->coords[0];
+ }
+ }
+ break;
+ }
+ }
+
+ // Coordinates available?
+ if (empty($this->value['latitude']) ||empty($this->value['longitude'])) {
+ // Distance set?
+ if (!empty($this->value['search_distance'])) {
+ // Hmm, distance set but unable to resolve coordinates.
+ // Force nothing to match.
+ $this->query->add_where(0, "1 = 0");
+ }
+ return;
+ }
+ $lat = $this->value['latitude'];
+ $lon = $this->value['longitude'];
+ // search_distance
+ if ($this->options['search_units'] == 'dd') {
+ list($lat_distance, $lon_distance) = explode(',', $this->value['search_distance']);
+ $latrange[0] = $lat - $lat_distance;
+ $latrange[1] = $lat + $lat_distance;
+ $lonrange[0] = $lon - $lon_distance;
+ $lonrange[1] = $lon + $lon_distance;
+ }
+ else {
+ $this->load_location_files();
+ $distance = $this->value['search_distance'];
+ $distance_meters = $this->_convert_to_meters($distance, $this->options['search_units']);
+
+ $latrange = earth_latitude_range($lon, $lat, $distance_meters);
+ $lonrange = earth_longitude_range($lon, $lat, $distance_meters);
+ }
+
+ // Prepare filter values.
+ $this->ensure_my_table();
+ $table = $this->table_alias;
+ $field_id = str_replace('_distance', '', $this->options['field']); // Get the base part of the subfields.
+
+ // Provide support other field types. TODO: create a 'field type' option.
+ switch ($argument->field) {
+ case 'field_geolocation_distance':
+ $field_lat = "{$table}.{$field_id}_lat";
+ $field_lng = "{$table}.{$field_id}_lng";
+ $field_latsin = "{$table}.{$field_id}_lat_sin";
+ $field_latcos = "{$table}.{$field_id}_lat_cos";
+ $field_lngrad = "{$table}.{$field_id}_lng_rad";
+ break;
+ case 'field_location': // supports location.module
+ $field_id = '';
+ $field_lat = "{$table}.latitude";
+ $field_lng = "{$table}.longitude";
+ break;
+ case 'field_geofield': // supports geofield.module
+ $field_lat = "{$table}.{$field_id}_lat";
+ $field_lng = "{$table}.{$field_id}_lon";
+ break;
+ }
+
+ // Build the query. (location.module style)
+ // Add MBR check (always).
+ // In case we go past the 180/-180 mark for longitude.
+ if ($lonrange[0] > $lonrange[1]) {
+ $where = "$field_lat > :minlat AND $field_lat < :maxlat AND (($field_lat < 180 AND $field_lat > :minlon) OR ($field_lng < :maxlon AND $field_lat > -180))";
+ }
+ else {
+ $where = "$field_lat > :minlat AND $field_lat < :maxlat AND $field_lng > :minlon AND $field_lng < :maxlon";
+ }
+ $this->query->add_where_expression(0, $where, array(':minlat' => $latrange[0], ':maxlat' => $latrange[1], ':minlon' => $lonrange[0], ':maxlon' => $lonrange[1]));
+ if ($this->options['search_method'] == 'dist') {
+ // Add radius check.
+ $this->query->add_where_expression(0, earth_distance_sql($lon, $lat, $this->table_alias) . ' < :distance', array(':distance' => $distance_meters));
+ }
+
+// // Build the query. (geolocation.module style)
+// $sql = _proximity_sql_fragment($lat, $lon, $field_latsin, $field_latcos, $field_lngrad);
+// // We use having to be able to reuse the query on field handlers
+//// $this->query->add_field(NULL, $sql, $this->field_alias);
+// $this->query->add_field(NULL, $sql, $this->real_field);
+// $this->query->add_having($this->options['group'], $field_id, $distance_meters, $this->operator);
+ }
+
+ function load_location_files() {
+ // Include the external libraries from location.module
+ // They are duplicated in geolocation_proximity module directory.
+ // Therefore, avoid loading the files twice, by first checking the location module, then this module.
+ $found = false;
+ $file = DRUPAL_ROOT . '/' . drupal_get_path('module', 'location') . '/earth.inc';
+ if (file_exists($file)) { // Read the country file.
+ $found = include_once($file);
+ }
+ if(!$found) {
+ $file = DRUPAL_ROOT . '/' . drupal_get_path('module', 'geolocation_proximity') . '/earth.inc';
+ $found = include_once($file);
+ }
+ }
+
+ /**
+ * Validate the options form.
+ */
+// function value_validate($form, &$form_state) {
+// $this->_latlng_validate($form['value'], $form_state['values']['options']['value']);
+// }
+//
+// function exposed_validate(&$form, &$form_state) {
+// $this->_latlng_validate($form[$this->options['id']], $form_state['values'][$this->options['id']]);
+// }
+
+ /**
+ * Validate the latitude and longitude values
+ */
+/*
+ function _latlng_validate(&$elements, &$values) {
+ switch (TRUE) {
+ case !is_numeric($values['latitude']):
+ form_error($elements['latitude'], t('Invalid Latitude. Value must be numeric.'));
+ break;
+
+ case $values['latitude'] > 90:
+ case $values['latitude'] < -90:
+ form_error($elements['latitude'], t('Invalid Latitude. Value must be between 90 and -90.'));
+ break;
+ }
+
+ switch (TRUE) {
+ case !is_numeric($values['longitude']):
+ form_error($elements['longitude'], t('Invalid Longitude. Value must be numeric.'));
+ break;
+
+ case $values['longitude'] > 180:
+ case $values['longitude'] < -180:
+ form_error($elements['longitude'], t('Invalid Longitude. Value must be between 180 and -180.'));
+ break;
+ }
+
+ if (!is_numeric($values['search_distance']) || $values['search_distance'] < 0) {
+ form_error($elements['search_distance'], t('Invalid Distance. Value must be a positive number.'));
+ }
+ }
+*/
+
+ function _convert_to_meters($distance, $distance_unit = 'km') {
+ if (!is_numeric($distance)) {
+ return NULL;
+ }
+
+ if ($distance == 0) {
+ return NULL;
+ }
+
+ if ($distance_unit == 'm') {
+ return $distance;
+ }
+
+ 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;
+ }
+}