array(
'label' => 'Geofield',
'description' => t('This field stores geo information.'),
'default_widget' => 'geofield_wkt',
'default_formatter' => 'geofield_wkt',
'settings' => array(
'srid' => '4326',
'backend' => 'default',
),
'property_type' => 'geofield',
'property_callbacks' => array('geofield_property_info_callback'),
'microdata' => TRUE,
),
);
}
/**
* Implements hook_field_update_field.
*
* If a geofield has been created, check to see if the plugin controlling it
* defines a 'postinstall' callback, if so, call it.
*/
function geofield_field_update_field($field, $prior_field, $has_data) {
if ($field['type'] == 'geofield') {
$backend = ctools_get_plugins('geofield', 'geofield_backend', $field['settings']['backend']);
if (!empty($backend['update_field'])) {
$postinstall_callback = $backend['update_field'];
$postinstall_callback($field, $prior_field, $has_data);
}
}
}
/**
* Implements hook_field_update_instance().
*
* We implement this hook to prevent instance settings that may not apply to our different
* widgets from breaking when switching widgets. See http://drupal.org/node/1840920.
*/
function geofield_field_update_instance($instance, $prior_instance) {
if (!empty($instance['widget']['type']) && !empty($prior_instance['widget']['type']) && $instance['widget']['type'] != $prior_instance['widget']['type']) {
$instance['default_value'] = array();
_field_write_instance($instance, TRUE);
field_cache_clear();
}
}
/**
* Implements hook_field_delete_field.
*
* If a geofield has been deleted, check to see if the plugin controlling it
* defines a 'postdelete' callback, if so, call it.
*/
function geofield_field_delete_field($field) {
if ($field['type'] == 'geofield') {
$backend = ctools_get_plugins('geofield', 'geofield_backend', $field['settings']['backend']);
if (!empty($backend['delete_field'])) {
$delete_field_callback = $backend['delete_field'];
$delete_field_callback($field);
}
}
}
/**
* Implements hook_field_settings_form().
*/
function geofield_field_settings_form($field, $instance, $has_data) {
ctools_include('plugins');
$settings = $field['settings'];
$backend_options = array();
$backends = ctools_get_plugins('geofield', 'geofield_backend');
foreach ($backends as $id => $backend) {
if (isset($backend['requirements'])) {
if ($backend['requirements']) {
$callback = $backend['requirements'];
$error = '';
if (!$callback($error)) {
$form['backend_error'][] = array(
//@@TODO: Use t() func
//@@TODO: css to add some red and bold
'#markup' => '
' . $backend['title'] . ' not usable because ' . $error . '
',
);
continue;
}
}
}
$backend_options[$id] = $backend['title'];
}
$form['backend'] = array(
'#type' => 'select',
'#title' => 'Storage Backend',
'#default_value' => $settings['backend'],
'#options' => $backend_options,
'#description' => "Select the Geospatial storage backend you would like to use to store geofield geometry data. If you don't know what this means, select 'Default'.",
'#disabled' => $has_data,
);
$form['settings'] = array(
'#tree' => TRUE,
);
// Expose backend-settings, if they have them
foreach ($backends as $id => $backend) {
if (isset($backend['settings'])) {
if ($backend['settings']) {
$callback = $backend['settings'];
$form[$id] = array(
'#type' => 'fieldset',
'#tree' => TRUE,
'#title' => $backend['title'] . ' Settings',
'#states' => array(
'visible' => array(
':input[name="field[settings][backend]"]' => array('value' => $id),
),
),
);
$form[$id] = array_merge($form[$id], $callback($field, $instance, $has_data));
}
}
}
return $form;
}
/**
* Implements hook_field_validate().
*/
function geofield_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
ctools_include('plugins');
$backend = ctools_get_plugins('geofield', 'geofield_backend', $field['settings']['backend']);
foreach ($items as $delta => $item) {
$geom_empty = geofield_geom_is_empty($item);
// Required field empty.
if ($instance['required'] && $geom_empty) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'data_missing',
'message' => t('%name is required and must not be empty.', array('%name' => $instance['label'])),
);
}
else {
// Geometry errors.
if ($geom_empty) {
return FALSE;
}
else {
$error = geofield_validate_geom($item);
if ($error) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'data_faulty',
'message' => t('%name: Specified location data is invalid.', array('%name' => $instance['label'])),
);
}
if (!empty($backend['validate'])) {
$validate_callback = $backend['validate'];
$error = $validate_callback($item);
if ($error) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'data_faulty',
'message' => t('%name: Specified location data is invalid.', array('%name' => $instance['label'])),
);
}
}
}
}
}
}
/**
* Validates input data against the geometry processor
* @param array $item
* Geometry field submission
* @return \Exception|null
* If validates, return NULL, else error text
*/
function geofield_validate_geom($item) {
if (is_string($item)) {
try {
$values = geofield_compute_values($item);
}
catch (Exception $e) {
return $e;
}
}
else {
try {
$input_format = !empty($item['input_format']) ? $item['input_format'] : NULL;
$values = geofield_compute_values($item['geom'], $input_format);
}
catch (Exception $e) {
return $e;
}
}
return NULL;
}
/**
* Check whether geometry is empty
* @param array $item
* Geometry field submission
* @return boolean
* If empty, return TRUE
*/
function geofield_geom_is_empty($item) {
if (!empty($item['input_format'])) {
switch ($item['input_format']) {
case 'wkt':
if (empty($item['geom'])) {
return TRUE;
}
break;
case 'lat/lon':
if (empty($item['geom']['lat']) || empty($item['geom']['lon'])) {
return TRUE;
}
break;
case 'bounds':
if (empty($item['geom']['top']) || empty($item['geom']['right']) || empty($item['geom']['bottom']) || empty($item['geom']['left'])) {
return TRUE;
}
break;
case 'json':
if (empty($item['geom'])) {
return TRUE;
}
break;
}
}
else {
return empty($item['geom']);
}
}
/**
* Implements hook_field_presave().
*/
function geofield_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
if ($field['type'] === 'geofield') {
/**
* Edge case. Currently, Drupal will set a field value to the default value if the current value
* is empty, even if it's set by the user. This bypasses our validation, and currently non-valid WKB
* data in geom causes catastrophic failures in entity_load. To compensate, we add the default value
* in early. When the core issue is fixed, we should drop this code.
*
* Geofield Issue: http://drupal.org/node/1886852
* Core Issue: http://drupal.org/node/1253820
*/
if ($instance['required'] == 0 && empty($items)) {
$entity_ids = entity_extract_ids($entity_type, $entity);
if (empty($entity_ids[0])) {
$items = isset($instance['default_value']) ? array($instance['default_value']) : array();
}
}
ctools_include('plugins');
$backend = ctools_get_plugins('geofield', 'geofield_backend', $field['settings']['backend']);
$save_callback = $backend['save'];
// For each delta, we compute all the auxiliary columns and transform the geom column into a geometry object
// We then pass the geometry object (now stored in the geom column) to the backend to prepare it for insertion into the database
foreach ($items as $delta => $item) {
$items[$delta] = geofield_compute_values($item);
if (isset($items[$delta]['geom']) && $items[$delta]['geom']) {
$items[$delta]['geom'] = $save_callback($items[$delta]['geom']);
}
}
}
}
/**
* Implements hook_field_load().
*
* Geofield stores it's data as WKB, but a binary format can cause
* issues/confusion with working with other modules, notably Services.
* To improve DX/discoverability of what we're storing, we convert
* to WKT on load.
*/
function geofield_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
geophp_load();
if (geoPHP::geosInstalled()) {
// process geometry directly with GEOS to help with performance/memory issues.
$reader = new GEOSWKBReader();
$writer = new GEOSWKTWriter();
$writer->setTrim(TRUE);
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
if (!empty($item['geom'])) {
$raw_geom = unpack('H*', $item['geom']);
try {
$geom = $reader->readHEX($raw_geom[1]);
$items[$id][$delta]['geom'] = $writer->write($geom);
}
catch (Exception $e) {
watchdog(WATCHDOG_ERROR, 'Cannot render poorly formatted WKB value %message', array('%message' => $e->getMessage()));
}
}
}
}
}
else {
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
if (!empty($item['geom'])) {
$geom = geophp::load($item['geom']);
if ($geom) {
$items[$id][$delta]['geom'] = $geom->out('wkt');
}
}
}
}
}
}
/**
* Implements hook_content_is_empty().
*/
function geofield_field_is_empty($item, $field) {
if (isset($item['input_format'])) {
switch ($item['input_format']) {
case GEOFIELD_INPUT_LAT_LON:
return ((trim($item['geom']['lat']) == '') || (trim($item['geom']['lon']) == ''));
case GEOFIELD_INPUT_BOUNDS:
return ((trim($item['geom']['top']) == '') || (trim($item['geom']['right']) == '') ||
(trim($item['geom']['bottom']) == '') || (trim($item['geom']['left']) == ''));
}
}
//@@TODO: Check if it's an empty geometry as per geoPHP $geometry->empty()
return empty($item['geom']);
}
/**
* Implements hook_view_api().
*/
function geofield_views_api() {
return array(
'api' => '3.0',
'path' => drupal_get_path('module', 'geofield') . '/views',
);
}
/**
* Implements hook_ctools_plugin_type().
*/
function geofield_ctools_plugin_type() {
return array(
'geofield_backend' => array(),
'behaviors' => array(
'use hooks' => TRUE,
)
);
}
/**
* Implements hook_ctools_plugin_api().
*/
function geofield_ctools_plugin_api($module, $api) {
return array('version' => 1);
}
/**
* Implementation of hook_ctools_plugin_directory().
*/
function geofield_ctools_plugin_directory($module, $plugin) {
if ($plugin == 'geofield_backend') {
return 'includes/' . $plugin;
}
}
/**
* Geofield Compute Values
*
* @todo: documentation
* Steps:
* 1. Load the geoPHP library
* 2. Load the Geometry object from the master-column
* 3. Get out all the computer values from the Geometry object
* 4. Set all the values
*/
function geofield_compute_values($raw_data, $input_format = NULL) {
// If raw_data is NULL, false, or otherwise empty, just return an empty array of values
if (empty($raw_data)) {
return array();
}
// Load up geoPHP to do the conversions
$geophp = geophp_load();
if (!$geophp) {
drupal_set_message(t("Unable to load geoPHP library. Not all values will be calculated correctly"), 'error');
return;
}
$geometry = geofield_geometry_from_values($raw_data, $input_format);
// Get values from geometry
if (!empty($geometry)) {
$values = geofield_get_values_from_geometry($geometry);
}
else {
$values = array();
}
return $values;
}
/**
* Primary function for processing geoPHP geometry objects from raw data.
* @param $raw_data
* The info we're trying to process. Valid input can be a string or an array. If $raw_data is a string,
* the value is passed directly to geophp for parsing. If $raw_data is an array (as is expected for Lat/Lon or
* Bounds input), process into raw WKT and generate geometry object from there.
* @param $input_format
* Geofield module defined constants that specify a specific type of input. Useful for ensuring that only a specific
* type of data is valid (i.e., if we're expecting WKT, valid GeoJSON doesn't get processed).
* @return
* A populated geoPHP geometry object if valid geometry, no return otherwise.
*
* @TODO: Refactor the function to not check for $input_format from both the optional secondary parameter and
* an array item in $raw_data. This is probably an artifact from how Geofield's widgets pass data to various field
* hooks. We should only check the optional secondary parameter.
* @TODO: Move constants from geofield.widgets.inc to geofield.module
* @TODO: Provide useful failure return (FALSE)
*/
function geofield_geometry_from_values($raw_data, $input_format = NULL) {
// Load up geoPHP to do the conversions
$geophp = geophp_load();
if (!$geophp) {
drupal_set_message(t("Unable to load geoPHP library. Not all values will be calculated correctly"), 'error');
return;
}
if (is_array($raw_data)) {
if (!empty($raw_data['input_format'])) {
if ($raw_data['input_format'] == GEOFIELD_INPUT_LAT_LON) {
$geometry = new Point($raw_data['geom']['lon'], $raw_data['geom']['lat']);
}
elseif ($raw_data['input_format'] == GEOFIELD_INPUT_BOUNDS) {
$wkt_bounds_format = 'POLYGON((left bottom,right bottom,right top,left top,left bottom))';
$wkt = strtr($wkt_bounds_format, array('top' => $raw_data['geom']['top'],
'right' => $raw_data['geom']['right'],
'bottom' => $raw_data['geom']['bottom'],
'left' => $raw_data['geom']['left']));
$geometry = geoPHP::load($wkt);
}
else {
$geometry = geoPHP::load($raw_data['geom'], $raw_data['input_format']);
}
} else {
// No input format - let geoPHP figure it out
if (!empty($raw_data['geom'])) {
$geometry = geoPHP::load($raw_data['geom']);
}
// Special case, raw input (Services/Feeds) that only specifies lat/lon.
elseif (!empty($raw_data['lat']) && !empty($raw_data['lon'])) {
$geometry = new Point($raw_data['lon'], $raw_data['lat']);
}
}
}
else {
if ($input_format) {
$geometry = geoPHP::load($raw_data, $input_format);
}
else {
// All we have at this point is a raw string. let GeoPHP figure it out
$geometry = geoPHP::load($raw_data);
}
}
if (isset($geometry)) {
return $geometry;
}
}
/**
* Given a geometry object from geoPHP, return a values array
*/
function geofield_get_values_from_geometry($geometry) {
$values = array();
$centroid = $geometry->getCentroid();
$bounding = $geometry->getBBox();
$values['geom'] = $geometry->out('wkb');
$values['geo_type'] = drupal_strtolower($geometry->getGeomType());
if ($centroid) {
$values['lat'] = $centroid->y();
$values['lon'] = $centroid->x();
}
$values['top'] = $bounding['maxy'];
$values['bottom'] = $bounding['miny'];
$values['right'] = $bounding['maxx'];
$values['left'] = $bounding['minx'];
// Truncate geohash to max length.
$values['geohash'] = substr($geometry->out('geohash'), 0, GEOFIELD_GEOHASH_LENGTH);
return $values;
}
// Latitude and Longitude string conversion
// ----------------------------------------
/**
* Decimal-Degrees-Seconds to Decimal Degrees
*
* Converts string to decimal degrees. Has some error correction for messy strings
*/
function geofield_latlon_DMStoDEC($dms) {
if (is_numeric($dms)) {
// It's already decimal degrees, just return it
return $dms;
}
// If it contains both an H and M, then it's an angular hours
if (stripos($dms, 'H') !== FALSE && stripos($dms, 'M') !== FALSE) {
$dms = strtr($dms, "'\"HOURSMINTECNDAhoursmintecnda", " ");
$dms = preg_replace('/\s\s+/', ' ', $dms);
$dms = explode(" ", $dms);
$deg = $dms[0];
$min = $dms[1];
$sec = $dms[2];
$dec = floatval(($deg*15) + ($min/4) + ($sec/240));
return $dec;
}
// If it contains an S or a W, then it's a negative
if (stripos($dms, 'S') !== FALSE || stripos($dms, 'W') !== FALSE) {
$direction = -1;
}
else {
$direction = 1;
}
// Strip all characters and replace them with empty space
$dms = strtr($dms, "�'\"NORTHSEAWnorthseaw'", " ");
$dms = preg_replace('/\s\s+/', ' ', $dms);
$dms = explode(" ", $dms);
$deg = $dms[0];
$min = $dms[1];
$sec = $dms[2];
// Direction should be checked only for nonnegative coordinates
$dec = floatval($deg+((($min*60)+($sec))/3600));
if ($dec > 0) {
$dec = $direction * $dec;
}
return $dec;
}
/**
* Decimal Degrees to Decimal-Degrees-Seconds
*
* Converts decimal longitude / latitude to DMS ( Degrees / minutes / seconds )
*/
function geofield_latlon_DECtoDMS($dec, $axis) {
if ($axis == 'lat') {
if ($dec < 0) $direction = 'S';
else $direction = 'N';
}
if ($axis == 'lon') {
if ($dec < 0) $direction = 'W';
else $direction = 'E';
}
$vars = explode(".", $dec);
$deg = abs($vars[0]);
if (isset($vars[1])) {
$tempma = "0." . $vars[1];
}
else {
$tempma = "0";
}
$tempma = $tempma * 3600;
$min = floor($tempma / 60);
$sec = $tempma - ($min*60);
return $deg . "° " . $min . "' " . round($sec, 3) . "\" " . $direction;
}
/**
* Decimal Degrees to Celestial coordinate system (CCS) units
*
* Converts decimal latitude to DMS ( Degrees / minutes / seconds ) and decimal longitude to Angular Hours / Minutes / Seconds
*/
function geofield_latlon_DECtoCCS($dec, $axis) {
// Declination (celestial latitude) should be representeted in Degrees / minutes / seconds
if ($axis == 'lat') {
$vars = explode("." , $dec);
$deg = $vars[0];
if (isset($vars[1])) {
$tempma = "0." . $vars[1];
}
else {
$tempma = "0";
}
$tempma = $tempma * 3600;
$min = floor($tempma / 60);
$sec = $tempma - ($min*60);
return $deg . "° " . $min . "' " . round($sec, 3) . "\"";
}
// Right ascension (celestial longitude) should be representeted in Hours / Minutes / Seconds
if ($axis == 'lon') {
$tempma = $dec / 15;
$vars = explode(".", $tempma);
$hrs = $vars[0];
if (isset($vars[1])) {
$tempma = "0." . $vars[1];
}
else {
$tempma = "0";
}
$tempma = $tempma * 60;
$vars = explode(".", $tempma);
$min = $vars[0];
if (isset($vars[1])) {
$tempma = "0." . $vars[1];
}
else {
$tempma = "0";
}
$sec = $tempma * 60;
return $hrs . "h " . $min . "m " . round($sec, 3) . "s";
}
}
/**
* Haversine formula, useful for injecting into an SQL statement. In instances where it isn't possible to pass in variables dynamically (i.e. field
* definitions), this function will bake in values directly into the snippet.
*
* @param $options
* An array of parameters that can be passed along to be injected directly into the SQL snippet. The following array keys are checked...
* - origin_latitude
* - origin_longitude
* - destination_latitude
* - destination_longitude
* - earth_radius
*
* @return
* A string suitable for injection into DBTNG. Any option passed into the function will be baked into the string directly. Any variable missing will
* be represented as :[variable]. (i.e. :earth_radius).
*/
function geofield_haversine($options = array()) {
$formula = '( :earth_radius * ACOS( COS( RADIANS(:origin_latitude) ) * COS( RADIANS(:destination_latitude) ) * COS( RADIANS(:destination_longitude) - RADIANS(:origin_longitude) ) + SIN( RADIANS(:origin_latitude) ) * SIN( RADIANS(:destination_latitude) ) ) )';
foreach ($options as $key => $option) {
if (is_numeric($option)) {
$formula = str_replace(':' . $key, $option, $formula);
}
else {
$formula = str_replace(':' . $key, db_escape_field($option), $formula);
}
}
return $formula;
}
/**
* Helper function to get all geofield fields.
*
* @return
* an array of field definitions for all geofields as defined by field_info_fields().
*/
function _geofield_get_geofield_fields() {
$geofield_fields = array();
$fields = field_info_fields();
foreach ($fields as $field => $info) {
if ($info['type'] == 'geofield') {
$geofield_fields[$field] = $info;
}
}
return $geofield_fields;
}