geocoder.widget.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. /**
  3. * Implements hook_field_widget_info().
  4. */
  5. function geocoder_field_widget_info() {
  6. return array(
  7. 'geocoder' => array(
  8. 'label' => t('Geocode from another field'),
  9. 'field types' => array('geofield', 'geolocation_latlng', 'location'),
  10. 'behaviors' => array(
  11. 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
  12. 'default value' => FIELD_BEHAVIOR_NONE,
  13. ),
  14. ),
  15. );
  16. }
  17. /**
  18. * Implements field_widget_settings_form().
  19. */
  20. function geocoder_field_widget_settings_form($this_field, $instance) {
  21. $settings = $instance['widget']['settings'];
  22. $entity_fields = field_info_instances($instance['entity_type'], $instance['bundle']);
  23. $all_fields = field_info_fields();
  24. $supported_field_types = geocoder_supported_field_types();
  25. $processors = geocoder_handler_info();
  26. $handlers_by_type = array();
  27. $field_types = array();
  28. $valid_fields = array();
  29. $available_handlers = array();
  30. // Add in the title/name
  31. //@@TODO Do this programatically by getting entity_info
  32. switch ($instance['entity_type']) {
  33. case 'node':
  34. $all_fields['title'] = array(
  35. 'field_name' => 'title',
  36. 'type' => 'text',
  37. );
  38. $entity_fields['title']['label'] = t('Title');
  39. break;
  40. case 'taxonomy_term':
  41. $all_fields['name'] = array(
  42. 'field_name' => 'name',
  43. 'type' => 'text',
  44. );
  45. $entity_fields['name']['label'] = t('Name');
  46. break;
  47. }
  48. // Get a list of all valid fields that we both support and are part of this entity
  49. foreach ($all_fields as $field) {
  50. if (array_key_exists($field['field_name'], $entity_fields)) {
  51. if (in_array($field['type'], array_keys($supported_field_types)) && ($field['field_name'] != $this_field['field_name'])) {
  52. $valid_fields[$field['field_name']] = $entity_fields[$field['field_name']]['label'];
  53. foreach ($supported_field_types[$field['type']] as $handler) {
  54. $available_handlers[$handler] = $processors[$handler]['title'];
  55. $handlers_by_type[$field['type']][] = $handler;
  56. $field_types[$field['field_name']] = $field['type'];
  57. }
  58. }
  59. }
  60. }
  61. $form['geocoder_field'] = array(
  62. '#type' => 'select',
  63. '#title' => t('Geocode from field'),
  64. '#default_value' => isset($settings['geocoder_field']) ? $settings['geocoder_field']: '',
  65. '#options' => $valid_fields,
  66. '#description' => t('Select which field you would like to geocode from.'),
  67. '#required' => TRUE,
  68. );
  69. $form['geocoder_handler'] = array(
  70. '#type' => 'select',
  71. '#title' => t('Geocoder'),
  72. '#prefix' => '<div id="geocoder-handler-div">',
  73. '#suffix' => '</div>',
  74. '#default_value' => isset($settings['geocoder_handler']) ? $settings['geocoder_handler']: '',
  75. '#options' => $available_handlers,
  76. '#description' => t('Select which type of geocoding handler you would like to use'),
  77. '#required' => TRUE,
  78. );
  79. $form['handler_settings'] = array(
  80. '#tree' => TRUE,
  81. );
  82. // Add the handler settings forms
  83. foreach ($processors as $handler_id => $handler) {
  84. if (isset($handler['settings_callback']) || isset($handler['terms_of_service'])) {
  85. $default_values = isset($settings['handler_settings'][$handler_id]) ? $settings['handler_settings'][$handler_id] : array();
  86. $form['handler_settings'][$handler_id] = array();
  87. $form['handler_settings'][$handler_id]['#type'] = 'fieldset';
  88. $form['handler_settings'][$handler_id]['#attributes'] = array('class' => array('geocoder-handler-setting', 'geocoder-handler-setting-' . $handler_id));
  89. $form['handler_settings'][$handler_id]['#title'] = $handler['title'] . ' Settings';
  90. $form['handler_settings'][$handler_id]['#states'] = array(
  91. 'visible' => array(
  92. ':input[id="edit-instance-widget-settings-geocoder-handler"]' => array('value' => $handler_id),
  93. ),
  94. );
  95. if (isset($handler['terms_of_service'])) {
  96. $form['handler_settings'][$handler_id]['tos'] = array(
  97. '#type' => 'item',
  98. '#markup' => t('This handler has terms of service. Click the following link to learn more.') . ' ' . l($handler['terms_of_service'], $handler['terms_of_service']),
  99. );
  100. }
  101. if (isset($handler['settings_callback'])) {
  102. $settings_callback = $handler['settings_callback'];
  103. $form['handler_settings'][$handler_id] = array_merge($form['handler_settings'][$handler_id], $settings_callback($default_values));
  104. }
  105. }
  106. }
  107. $form['delta_handling'] = array(
  108. '#type' => 'select',
  109. '#title' => t('Multi-value input handling'),
  110. '#description' => t('Should geometries from multiple inputs be: <ul><li>Matched with each input (e.g. One POINT for each address field)</li><li>Aggregated into a single MULTIPOINT geofield (e.g. One MULTIPOINT polygon from multiple address fields)</li><li>Broken up into multiple geometries (e.g. One MULTIPOINT to multiple POINTs.)</li></ul>'),
  111. '#default_value' => isset($settings['delta_handling']) ? $settings['delta_handling']: 'default',
  112. '#options' => array(
  113. 'default' => 'Match Multiples (default)',
  114. 'm_to_s' => 'Multiple to Single',
  115. 's_to_m' => 'Single to Multiple',
  116. 'c_to_s' => 'Concatenate to Single',
  117. 'c_to_m' => 'Concatenate to Multiple',
  118. ),
  119. '#required' => TRUE,
  120. );
  121. // Add javascript to sync allowed values. Note that we are not using AJAX because we do not have access to the raw form_state here
  122. drupal_add_js(array('geocoder_widget_settings' => array('handlers' => $handlers_by_type, 'types' => $field_types)), 'setting');
  123. drupal_add_js(drupal_get_path('module', 'geocoder') . '/geocoder.admin.js', 'file');
  124. return $form;
  125. }
  126. /**
  127. * Implements hook_field_attach_presave().
  128. *
  129. * Geocoding for the geocoder widget is done here to ensure that only validated
  130. * and fully processed fields values are accessed.
  131. */
  132. function geocoder_field_attach_presave($entity_type, $entity) {
  133. // Loop over any geofield using our geocode widget
  134. $entity_info = entity_get_info($entity_type);
  135. $bundle_name = empty($entity_info['entity keys']['bundle']) ? $entity_type : $entity->{$entity_info['entity keys']['bundle']};
  136. foreach (field_info_instances($entity_type, $bundle_name) as $field_instance) {
  137. if ($field_instance['widget']['type'] === 'geocoder') {
  138. if (($field_value = geocoder_widget_get_field_value($entity_type, $field_instance, $entity)) !== FALSE) {
  139. $entity->{$field_instance['field_name']} = $field_value;
  140. }
  141. else {
  142. $entity->{$field_instance['field_name']} = array();
  143. }
  144. }
  145. }
  146. }
  147. /**
  148. * Get a field's value based on geocoded data.
  149. *
  150. * @param $entity_type
  151. * Type of entity
  152. * @para field_instance
  153. * Field instance definition array
  154. * @param $entity
  155. * Optionally, the entity. You must pass either the entity or $source_field_values
  156. * @param $source_field_values
  157. * Array of deltas / source field values. You must pass either this or $entity.
  158. *
  159. * @return
  160. * Three possibilities could be returned by this function:
  161. * - FALSE: do nothing.
  162. * - An empty array: use it to unset the existing field value.
  163. * - A populated array: assign a new field value.
  164. */
  165. function geocoder_widget_get_field_value($entity_type, $field_instance, $entity = NULL, $source_field_values = NULL) {
  166. if (!$source_field_values && !$entity) {
  167. trigger_error('geocoder_widget_get_field_value: You must pass either $source_field_values OR $entity', E_USER_ERROR);
  168. return FALSE;
  169. }
  170. $entity_info = entity_get_info($entity_type);
  171. // Required settings
  172. if (isset($field_instance['widget']['settings']['geocoder_handler']) && isset($field_instance['widget']['settings']['geocoder_field'])) {
  173. $handler = geocoder_get_handler($field_instance['widget']['settings']['geocoder_handler']);
  174. $field_name = is_array($field_instance['widget']['settings']['geocoder_field']) ? reset($field_instance['widget']['settings']['geocoder_field']) : $field_instance['widget']['settings']['geocoder_field'];
  175. $target_info = field_info_field($field_instance['field_name']);
  176. $target_type = $target_info['type'];
  177. // Determine the source type, if it's a entity-key, we mock it as a "text" field
  178. if (in_array($field_name, $entity_info['entity keys']) && $entity) {
  179. $field_info = array('type' => 'text', 'entity_key' => TRUE);
  180. }
  181. else {
  182. $field_info = field_info_field($field_name);
  183. $field_info['entity_key'] = FALSE;
  184. }
  185. // Get the source values
  186. if (!$source_field_values) {
  187. if ($field_info['entity_key'] && $entity) {
  188. $source_field_values = array(array('value' => $entity->$field_name));
  189. }
  190. else if ($entity) {
  191. $source_field_values = field_get_items($entity_type, $entity, $field_name, isset($entity->language) ? $entity->language : NULL);
  192. }
  193. else {
  194. // We can't find the source values
  195. return FALSE;
  196. }
  197. }
  198. // Remove source values that are not valid.
  199. if ($source_field_values) {
  200. foreach ($source_field_values as $delta => $item) {
  201. if (!is_numeric($delta)) {
  202. unset($source_field_values[$delta]);
  203. }
  204. }
  205. }
  206. // If no valid source values were passed.
  207. if (empty($source_field_values)) {
  208. return FALSE;
  209. }
  210. // For entities being updated, determine if another geocode is necessary
  211. if ($entity) {
  212. if (!empty($entity->original)) {
  213. //@@TODO: Deal with entity-properties (non-fields)
  214. //@@TODO: This isn't working with file fields. Should use some kind of lookup / map
  215. $field_original = field_get_items($entity_type, $entity->original, $field_name, isset($entity->original->language) ? $entity->original->language : NULL);
  216. if (!empty($field_original)) {
  217. $diff = geocoder_widget_array_recursive_diff($field_original, $source_field_values);
  218. if (empty($diff)) {
  219. return FALSE;
  220. }
  221. }
  222. }
  223. }
  224. // Get the handler-specific-settings
  225. if (isset($field_instance['widget']['settings']['handler_settings'][$handler['name']])) {
  226. $handler_settings = $field_instance['widget']['settings']['handler_settings'][$handler['name']];
  227. }
  228. // Determine how we deal with deltas (multi-value fields)
  229. if (empty($field_instance['widget']['settings']['delta_handling'])) {
  230. $delta_handling = 'default';
  231. }
  232. else {
  233. $delta_handling = $field_instance['widget']['settings']['delta_handling'];
  234. }
  235. // Check to see if we should be concatenating
  236. if ($delta_handling == 'c_to_s' || $delta_handling == 'c_to_m') {
  237. $source_field_values = geocoder_widget_get_field_concat($source_field_values);
  238. }
  239. if (is_array($source_field_values) && count($source_field_values)) {
  240. $values = array();
  241. // Geocode geometries
  242. $geometries = array();
  243. foreach ($source_field_values as $delta => $item) {
  244. // Geocode any value from our source field.
  245. try {
  246. if (isset($handler_settings)) {
  247. $geometry = call_user_func($handler['field_callback'], $field_info, $item, $handler_settings);
  248. }
  249. else {
  250. $geometry = call_user_func($handler['field_callback'], $field_info, $item);
  251. }
  252. if ($geometry instanceof Geometry) {
  253. $geometries[] = $geometry;
  254. }
  255. else {
  256. // An error occured
  257. return FALSE;
  258. }
  259. }
  260. catch (Exception $e) {
  261. watchdog_exception('geocoder', $e);
  262. return FALSE;
  263. }
  264. }
  265. if (empty($geometries)) {
  266. // This field has no data, so set the field to an empty array in
  267. // order to delete its saved data.
  268. $values = array(NULL);
  269. return array();
  270. }
  271. else {
  272. // Resolve multiple-values - get back values from our delta-resolver
  273. $values = geocoder_widget_resolve_deltas($geometries, $delta_handling, $target_type);
  274. // Set the values - geofields do not support languages
  275. return array(LANGUAGE_NONE => $values);
  276. }
  277. }
  278. }
  279. }
  280. /**
  281. * Get field items and info
  282. *
  283. * We always pass the full field-item array (with all columns) to the handler, but there is some preprocessing
  284. * that we need to do for the special case of entity-labels and multi-field contactenation
  285. * For these two special cases we "mock-up" a text-field and pass it back for geocoding
  286. */
  287. function geocoder_widget_get_field_concat($items) {
  288. // Check if we should concatenate
  289. $concat = '';
  290. foreach ($items as $item) {
  291. $concat .= trim($item['value']) . ', ';
  292. }
  293. $concat = trim($concat, ', ');
  294. $items = array(array('value' => $concat));
  295. return $items;
  296. }
  297. /**
  298. * Geocder Widget - Resolve Deltas
  299. *
  300. * Given a list of geometries, and user configuration on how to handle deltas,
  301. * we created a list of items to be inserted into the fields.
  302. */
  303. function geocoder_widget_resolve_deltas($geometries, $delta_handling = 'default', $field_type) {
  304. $values = array();
  305. // Default delta handling: just pass one delta to the next
  306. if ($delta_handling == 'default') {
  307. foreach ($geometries as $geometry) {
  308. $values[] = geocoder_widget_values_from_geometry($geometry, $field_type);
  309. }
  310. }
  311. // Single-to-multiple handling - if we can, explode out the component geometries
  312. if ($delta_handling == 's_to_m' || $delta_handling == 'c_to_m') {
  313. $type = $geometries[0]->geometryType();
  314. if (in_array($type, array('MultiPoint', 'MultiLineString', 'MultiPolygon', 'GeometryCollection'))) {
  315. $components = $geometries[0]->getComponents();
  316. foreach ($components as $component) {
  317. $values[] = geocoder_widget_values_from_geometry($component, $field_type);
  318. }
  319. }
  320. else {
  321. $values[] = geocoder_widget_values_from_geometry($geometries[0], $field_type);
  322. }
  323. }
  324. // For multiple-to-single handling, run it though geometryReduce
  325. if ($delta_handling == 'm_to_s' || $delta_handling == 'c_to_s') {
  326. $reduced_geom = geoPHP::geometryReduce($geometries);
  327. $values[] = geocoder_widget_values_from_geometry($reduced_geom, $field_type);
  328. }
  329. return $values;
  330. }
  331. /**
  332. * Geocder Widget - Field values from geometry
  333. *
  334. * Given a geometry and the field type, return back a values array for that field.
  335. * The passed back array represents a single delta.
  336. */
  337. function geocoder_widget_values_from_geometry($geometry, $field_type) {
  338. if ($field_type == 'geofield') return geofield_get_values_from_geometry($geometry);
  339. if ($field_type == 'geolocation_latlng') {
  340. $centroid = $geometry->centroid();
  341. $lat = $centroid->y();
  342. $lng = $centroid->x();
  343. return array(
  344. 'lat' => $lat,
  345. 'lng' => $lng,
  346. 'lat_sin' => sin(deg2rad($lat)),
  347. 'lat_cos' => cos(deg2rad($lat)),
  348. 'lng_rad' => deg2rad($lng),
  349. );
  350. }
  351. if ($field_type == 'location') {
  352. $centroid = $geometry->centroid();
  353. return array(
  354. 'latitude' => $centroid->y(),
  355. 'longitude' => $centroid->x(),
  356. 'source' => 2,
  357. );
  358. }
  359. }
  360. /**
  361. * Geocoder Widget - Parse an address field
  362. */
  363. function geocoder_widget_parse_addressfield($field_item) {
  364. $address = '';
  365. if (!empty($field_item['premise'])) $address .= $field_item['premise'] . ',';
  366. if (!empty($field_item['thoroughfare'])) $address .= $field_item['thoroughfare'] . ',';
  367. if (!empty($field_item['locality'])) $address .= $field_item['locality'] . ',';
  368. if (!empty($field_item['administrative_area'])) $address .= $field_item['administrative_area'] . ',';
  369. if (!empty($field_item['sub_administrative_area'])) $address .= $field_item['sub_administrative_area'] . ',';
  370. if (!empty($field_item['country'])) $address .= $field_item['country'] . ',';
  371. if (!empty($field_item['postal_code'])) $address .= $field_item['postal_code'] . ',';
  372. $address = rtrim($address, ', ');
  373. return $address;
  374. }
  375. /**
  376. * Geocoder Widget - Parse a location field
  377. */
  378. function geocoder_widget_parse_locationfield($field_item) {
  379. $address = '';
  380. if (!empty($field_item['name'])) $address .= $field_item['name'] . ',';
  381. if (!empty($field_item['street'])) $address .= $field_item['street'] . ',';
  382. if (!empty($field_item['additional'])) $address .= $field_item['additional'] . ',';
  383. if (!empty($field_item['city'])) $address .= $field_item['city'] . ',';
  384. if (!empty($field_item['province']) && function_exists('location_province_name')) {
  385. $province_fullname = location_province_name($field_item['country'], $field_item['province']);
  386. $address .= $province_fullname . ',';
  387. }
  388. if (!empty($field_item['country'])) $address .= $field_item['country'] . ',';
  389. if (!empty($field_item['postal_code'])) $address .= $field_item['postal_code'] . ',';
  390. $address = rtrim($address, ', ');
  391. return $address;
  392. }
  393. function geocoder_widget_array_recursive_diff($aArray1, $aArray2) {
  394. $aReturn = array();
  395. if (empty($aArray1)) {
  396. return $aReturn;
  397. }
  398. foreach ($aArray1 as $mKey => $mValue) {
  399. if (array_key_exists($mKey, $aArray2)) {
  400. if (is_array($mValue)) {
  401. $aRecursiveDiff = geocoder_widget_array_recursive_diff($mValue, $aArray2[$mKey]);
  402. if (count($aRecursiveDiff)) { $aReturn[$mKey] = $aRecursiveDiff; }
  403. } else {
  404. if ($mValue != $aArray2[$mKey]) {
  405. $aReturn[$mKey] = $mValue;
  406. }
  407. }
  408. } else {
  409. $aReturn[$mKey] = $mValue;
  410. }
  411. }
  412. return $aReturn;
  413. }