geocoder.widget.inc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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', 'postgis'),
  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. case 'country':
  48. $all_fields['name'] = array(
  49. 'field_name' => 'name',
  50. 'type' => 'text',
  51. );
  52. $entity_fields['name']['label'] = t('Name');
  53. break;
  54. case 'country':
  55. $all_fields['name'] = array(
  56. 'field_name' => 'name',
  57. 'type' => 'text',
  58. );
  59. $entity_fields['name']['label'] = t('Name');
  60. break;
  61. }
  62. // Get a list of all valid fields that we both support and are part of this entity
  63. foreach ($all_fields as $field) {
  64. if (array_key_exists($field['field_name'], $entity_fields)) {
  65. if (in_array($field['type'], array_keys($supported_field_types)) && ($field['field_name'] != $this_field['field_name'])) {
  66. $valid_fields[$field['field_name']] = $entity_fields[$field['field_name']]['label'];
  67. foreach ($supported_field_types[$field['type']] as $handler) {
  68. $available_handlers[$handler] = $processors[$handler]['title'];
  69. $handlers_by_type[$field['type']][] = $handler;
  70. $field_types[$field['field_name']] = $field['type'];
  71. }
  72. }
  73. }
  74. }
  75. // Extend with virtual fields.
  76. $info = entity_get_all_property_info($instance['entity_type']);
  77. foreach ($info as $property_name => $property) {
  78. if (isset($property['type']) && in_array($property['type'], array('location', 'text'))) {
  79. if (!isset($valid_fields[$property_name])) {
  80. $valid_fields[$property_name] = $property['label'];
  81. }
  82. }
  83. }
  84. natcasesort($valid_fields);
  85. $form['geocoder_field'] = array(
  86. '#type' => 'select',
  87. '#title' => t('Geocode from field'),
  88. '#default_value' => isset($settings['geocoder_field']) ? $settings['geocoder_field']: '',
  89. '#options' => $valid_fields,
  90. '#description' => t('Select which field you would like to geocode from.'),
  91. '#required' => TRUE,
  92. );
  93. $form['geocoder_handler'] = array(
  94. '#type' => 'select',
  95. '#title' => t('Geocoder'),
  96. '#prefix' => '<div id="geocoder-handler-div">',
  97. '#suffix' => '</div>',
  98. '#default_value' => isset($settings['geocoder_handler']) ? $settings['geocoder_handler']: '',
  99. '#options' => $available_handlers,
  100. '#description' => t('Select which type of geocoding handler you would like to use'),
  101. '#required' => TRUE,
  102. );
  103. $form['handler_settings'] = array(
  104. '#tree' => TRUE,
  105. );
  106. // Add the handler settings forms
  107. foreach ($processors as $handler_id => $handler) {
  108. if (isset($handler['settings_callback']) || isset($handler['terms_of_service'])) {
  109. $default_values = isset($settings['handler_settings'][$handler_id]) ? $settings['handler_settings'][$handler_id] : array();
  110. $form['handler_settings'][$handler_id] = array();
  111. $form['handler_settings'][$handler_id]['#type'] = 'fieldset';
  112. $form['handler_settings'][$handler_id]['#attributes'] = array('class' => array('geocoder-handler-setting', 'geocoder-handler-setting-' . $handler_id));
  113. $form['handler_settings'][$handler_id]['#title'] = $handler['title'] . ' Settings';
  114. $form['handler_settings'][$handler_id]['#states'] = array(
  115. 'visible' => array(
  116. ':input[id="edit-instance-widget-settings-geocoder-handler"]' => array('value' => $handler_id),
  117. ),
  118. );
  119. if (isset($handler['terms_of_service'])) {
  120. $form['handler_settings'][$handler_id]['tos'] = array(
  121. '#type' => 'item',
  122. '#markup' => t('This handler has terms of service. Click the following link to learn more.') . ' ' . l($handler['terms_of_service'], $handler['terms_of_service']),
  123. );
  124. }
  125. if (isset($handler['settings_callback'])) {
  126. // Load the file.
  127. geocoder_get_handler($handler_id);
  128. $settings_callback = $handler['settings_callback'];
  129. $form['handler_settings'][$handler_id] = array_merge($form['handler_settings'][$handler_id], $settings_callback($default_values));
  130. }
  131. }
  132. }
  133. $form['delta_handling'] = array(
  134. '#type' => 'select',
  135. '#title' => t('Multi-value input handling'),
  136. '#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>'),
  137. '#default_value' => isset($settings['delta_handling']) ? $settings['delta_handling']: 'default',
  138. '#options' => array(
  139. 'default' => 'Match Multiples (default)',
  140. 'm_to_s' => 'Multiple to Single',
  141. 's_to_m' => 'Single to Multiple',
  142. 'c_to_s' => 'Concatenate to Single',
  143. 'c_to_m' => 'Concatenate to Multiple',
  144. ),
  145. '#required' => TRUE,
  146. );
  147. // 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
  148. drupal_add_js(array('geocoder_widget_settings' => array('handlers' => $handlers_by_type, 'types' => $field_types)), 'setting');
  149. drupal_add_js(drupal_get_path('module', 'geocoder') . '/geocoder.admin.js', 'file');
  150. return $form;
  151. }
  152. /**
  153. * Implements hook_field_attach_presave().
  154. *
  155. * Geocoding for the geocoder widget is done here to ensure that only validated
  156. * and fully processed fields values are accessed.
  157. */
  158. function geocoder_field_attach_presave($entity_type, $entity) {
  159. // Loop over any geofield using our geocode widget
  160. $entity_info = entity_get_info($entity_type);
  161. $bundle_name = empty($entity_info['entity keys']['bundle']) ? $entity_type : $entity->{$entity_info['entity keys']['bundle']};
  162. foreach (field_info_instances($entity_type, $bundle_name) as $field_instance) {
  163. if ($field_instance['widget']['type'] === 'geocoder') {
  164. if (($field_value = geocoder_widget_get_field_value($entity_type, $field_instance, $entity)) !== FALSE) {
  165. $entity->{$field_instance['field_name']} = $field_value;
  166. }
  167. }
  168. }
  169. }
  170. /**
  171. * Find a field instance's or entity property's relevant meta data.
  172. */
  173. function geocoder_widget_get_field_info($entity_type, $field_instance, $entity) {
  174. $entity_info = entity_get_info($entity_type);
  175. $field_name = is_array($field_instance['widget']['settings']['geocoder_field']) ? reset($field_instance['widget']['settings']['geocoder_field']) : $field_instance['widget']['settings']['geocoder_field'];
  176. // Determine the source type, if it's a entity-key, we mock it as a "text" field
  177. if (in_array($field_name, $entity_info['entity keys']) && $entity) {
  178. $field_info = array('type' => 'text', 'entity_key' => TRUE);
  179. }
  180. else {
  181. $field_info = field_info_field($field_name);
  182. if (!$field_info) {
  183. $info = entity_get_all_property_info($entity_type);
  184. $field_info = $info[$field_name];
  185. }
  186. $field_info['entity_key'] = FALSE;
  187. }
  188. return $field_info;
  189. }
  190. /**
  191. * Return the value for the given proxy-field for the given entity.
  192. */
  193. function geocoder_widget_get_entity_field_value($entity_type, $field_instance, $entity) {
  194. $field_name = is_array($field_instance['widget']['settings']['geocoder_field']) ? reset($field_instance['widget']['settings']['geocoder_field']) : $field_instance['widget']['settings']['geocoder_field'];
  195. $field_info = geocoder_widget_get_field_info($entity_type, $field_instance, $entity);
  196. // Get the source values
  197. if ($field_info['entity_key'] && $entity) {
  198. $source_field_values = array(array('value' => $entity->$field_name));
  199. }
  200. else if ($entity) {
  201. $wrapper = entity_metadata_wrapper($entity_type, $entity);
  202. $field_wrapper = $wrapper->$field_name;
  203. $value = $field_wrapper->value();
  204. $values = array_filter(is_array($value) && isset($value[0]) ? $value : array($value));
  205. $source_field_values = array_map(function($value) {
  206. if (is_array($value)) {
  207. // Clean up array from Addressfield, for diff.
  208. unset($value['element_key']);
  209. // Clean up array from Location, for diff.
  210. unset($value['location_settings'], $value['country_name'], $value['latitude'], $value['longitude'], $value['lid']);
  211. return array_filter($value);
  212. }
  213. return array('value' => $value);
  214. }, $values);
  215. }
  216. else {
  217. // We can't find the source values
  218. return FALSE;
  219. }
  220. return $source_field_values;
  221. }
  222. /**
  223. * Get a field's value based on geocoded data.
  224. *
  225. * @param $entity_type
  226. * Type of entity
  227. * @para field_instance
  228. * Field instance definition array
  229. * @param $entity
  230. * Optionally, the entity. You must pass either the entity or $source_field_values
  231. * @param $source_field_values
  232. * Array of deltas / source field values. You must pass either this or $entity.
  233. *
  234. * @return
  235. * Three possibilities could be returned by this function:
  236. * - FALSE: do nothing.
  237. * - An empty array: use it to unset the existing field value.
  238. * - A populated array: assign a new field value.
  239. */
  240. function geocoder_widget_get_field_value($entity_type, $field_instance, $entity = NULL, $source_field_values = NULL) {
  241. if (!$source_field_values && !$entity) {
  242. trigger_error('geocoder_widget_get_field_value: You must pass either $source_field_values OR $entity', E_USER_ERROR);
  243. return FALSE;
  244. }
  245. // Required settings
  246. if (isset($field_instance['widget']['settings']['geocoder_handler']) && isset($field_instance['widget']['settings']['geocoder_field'])) {
  247. $handler = geocoder_get_handler($field_instance['widget']['settings']['geocoder_handler']);
  248. $field_name = is_array($field_instance['widget']['settings']['geocoder_field']) ? reset($field_instance['widget']['settings']['geocoder_field']) : $field_instance['widget']['settings']['geocoder_field'];
  249. $target_info = field_info_field($field_instance['field_name']);
  250. $field_info = geocoder_widget_get_field_info($entity_type, $field_instance, $entity);
  251. // Get the source values
  252. if (!$source_field_values) {
  253. $source_field_values = geocoder_widget_get_entity_field_value($entity_type, $field_instance, $entity);
  254. }
  255. // If no valid source values were passed.
  256. if (empty($source_field_values)) {
  257. return array();
  258. }
  259. // Get the handler-specific-settings
  260. if (isset($field_instance['widget']['settings']['handler_settings'][$handler['name']])) {
  261. $handler_settings = $field_instance['widget']['settings']['handler_settings'][$handler['name']];
  262. }
  263. else {
  264. $handler_settings = array();
  265. }
  266. // Determine how we deal with deltas (multi-value fields)
  267. if (empty($field_instance['widget']['settings']['delta_handling'])) {
  268. $delta_handling = 'default';
  269. }
  270. else {
  271. $delta_handling = $field_instance['widget']['settings']['delta_handling'];
  272. }
  273. // Check to see if we should be concatenating
  274. if ($delta_handling == 'c_to_s' || $delta_handling == 'c_to_m') {
  275. $source_field_values = geocoder_widget_get_field_concat($source_field_values);
  276. }
  277. // Allow other modules to alter values before we geocode them.
  278. drupal_alter('geocoder_geocode_values', $source_field_values, $field_info, $handler_settings, $field_instance);
  279. if (is_array($source_field_values) && count($source_field_values)) {
  280. // Geocode geometries.
  281. $geometries = array();
  282. foreach ($source_field_values as $delta => $item) {
  283. $geometry = NULL;
  284. if (!variable_get('geocoder_recode', 0)) {
  285. // Attempt to retrieve from persistent cache.
  286. $geometry = geocoder_cache_get($handler['name'], $item, $handler_settings);
  287. }
  288. // No cache record, so fetch live.
  289. if ($geometry === NULL) {
  290. // Geocode any value from our source field.
  291. try {
  292. $geometry = call_user_func($handler['field_callback'], $field_info, $item, $handler_settings);
  293. }
  294. // No-results or errors throw exceptions, which affects 1 field item, not all.
  295. catch (Exception $e) {
  296. $geometry = FALSE;
  297. $uri_info = entity_uri($entity_type, $entity);
  298. list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
  299. $label = t('View offending @entity_type (@bundle # @id)', array(
  300. '@entity_type' => $entity_type,
  301. '@bundle' => $bundle,
  302. '@id' => $id,
  303. ));
  304. watchdog_exception('geocoder', $e, NULL, array(), WATCHDOG_WARNING, l($label, $uri_info['path'], $uri_info));
  305. }
  306. // Save result persistently.
  307. geocoder_cache_set($geometry, $handler['name'], $item, $handler_settings);
  308. }
  309. if ($geometry instanceof Geometry) {
  310. $geometries[] = $geometry;
  311. }
  312. }
  313. if (empty($geometries)) {
  314. // This field has no data, so set the field to an empty array in
  315. // order to delete its saved data.
  316. return array();
  317. }
  318. else {
  319. // Resolve multiple-values - get back values from our delta-resolver
  320. $values = geocoder_widget_resolve_deltas($geometries, $delta_handling, $target_info);
  321. // Set the values - geofields do not support languages
  322. return array(LANGUAGE_NONE => $values);
  323. }
  324. }
  325. }
  326. }
  327. /**
  328. * Get field items and info
  329. *
  330. * We always pass the full field-item array (with all columns) to the handler, but there is some preprocessing
  331. * that we need to do for the special case of entity-labels and multi-field concatenation
  332. * For these two special cases we "mock-up" a text-field and pass it back for geocoding
  333. */
  334. function geocoder_widget_get_field_concat($items) {
  335. // Check if we should concatenate
  336. $concat = '';
  337. foreach ($items as $item) {
  338. if (!empty($item['value'])) {
  339. $concat .= trim($item['value']) . ', ';
  340. }
  341. }
  342. $concat = trim($concat, ', ');
  343. $items = array(array('value' => $concat));
  344. return $items;
  345. }
  346. /**
  347. * Geocoder Widget - Resolve Deltas
  348. *
  349. * Given a list of geometries, and user configuration on how to handle deltas,
  350. * we created a list of items to be inserted into the fields.
  351. */
  352. function geocoder_widget_resolve_deltas($geometries, $delta_handling = 'default', $target_info) {
  353. $values = array();
  354. // Default delta handling: just pass one delta to the next
  355. if ($delta_handling == 'default') {
  356. foreach ($geometries as $geometry) {
  357. $values[] = geocoder_widget_values_from_geometry($geometry, $target_info);
  358. }
  359. }
  360. // Single-to-multiple handling - if we can, explode out the component geometries
  361. if ($delta_handling == 's_to_m' || $delta_handling == 'c_to_m') {
  362. $type = $geometries[0]->geometryType();
  363. if (in_array($type, array('MultiPoint', 'MultiLineString', 'MultiPolygon', 'GeometryCollection'))) {
  364. $components = $geometries[0]->getComponents();
  365. foreach ($components as $component) {
  366. $values[] = geocoder_widget_values_from_geometry($component, $target_info);
  367. }
  368. }
  369. else {
  370. $values[] = geocoder_widget_values_from_geometry($geometries[0], $target_info);
  371. }
  372. }
  373. // For multiple-to-single handling, run it though geometryReduce
  374. if ($delta_handling == 'm_to_s' || $delta_handling == 'c_to_s') {
  375. $reduced_geom = geoPHP::geometryReduce($geometries);
  376. $values[] = geocoder_widget_values_from_geometry($reduced_geom, $target_info);
  377. }
  378. return $values;
  379. }
  380. /**
  381. * Geocoder Widget - Field values from geometry
  382. *
  383. * Given a geometry and the field type, return back a values array for that field.
  384. * The passed back array represents a single delta.
  385. */
  386. function geocoder_widget_values_from_geometry($geometry, $target_info) {
  387. if ($target_info['type'] == 'geofield') return geofield_get_values_from_geometry($geometry);
  388. if ($target_info['type'] == 'geolocation_latlng') {
  389. $centroid = $geometry->centroid();
  390. $lat = $centroid->y();
  391. $lng = $centroid->x();
  392. return array(
  393. 'lat' => $lat,
  394. 'lng' => $lng,
  395. 'lat_sin' => sin(deg2rad($lat)),
  396. 'lat_cos' => cos(deg2rad($lat)),
  397. 'lng_rad' => deg2rad($lng),
  398. );
  399. }
  400. if ($target_info['type'] == 'location') {
  401. $centroid = $geometry->centroid();
  402. return array(
  403. 'latitude' => $centroid->y(),
  404. 'longitude' => $centroid->x(),
  405. 'source' => 2,
  406. );
  407. }
  408. if ($target_info['type'] == 'postgis') {
  409. $srid = $geometry->getSRID() ? $geometry->getSRID() : '4326';
  410. $type = $target_info['settings']['type'];
  411. $postgis_geometry = new PostgisGeometry($type, $srid);
  412. $postgis_geometry->fromText($geometry->asText());
  413. $postgis_geometry->transform($target_info['settings']['srid']);
  414. return array(
  415. 'geometry' => $postgis_geometry->getGeometry(),
  416. );
  417. }
  418. }
  419. /**
  420. * Geocoder Widget - Parse an address field.
  421. */
  422. function geocoder_widget_parse_addressfield($field_item) {
  423. $address = array();
  424. $address[] = !empty($field_item['organization']) ? $field_item['organization'] : NULL;
  425. $address[] = !empty($field_item['premise']) ? $field_item['premise'] : NULL;
  426. $address[] = !empty($field_item['sub_premise']) ? $field_item['sub_premise'] : NULL;
  427. $address[] = !empty($field_item['thoroughfare']) ? $field_item['thoroughfare'] : NULL;
  428. $address[] = !empty($field_item['locality']) ? $field_item['locality'] : NULL;
  429. $address[] = !empty($field_item['administrative_area']) ? $field_item['administrative_area'] : NULL;
  430. $address[] = !empty($field_item['sub_administrative_area']) ? $field_item['sub_administrative_area'] : NULL;
  431. if (!empty($field_item['country'])) {
  432. if (module_exists('countries')) {
  433. $country = country_load($field_item['country']);
  434. $field_item['country'] = $country->name;
  435. }
  436. else {
  437. // Convert country code to country name.
  438. include_once DRUPAL_ROOT . '/includes/locale.inc';
  439. $countries = country_get_list();
  440. if (array_key_exists($field_item['country'], $countries)) {
  441. $field_item['country'] = $countries[$field_item['country']];
  442. }
  443. }
  444. $address[] = $field_item['country'];
  445. }
  446. $address[] = !empty($field_item['postal_code']) ? $field_item['postal_code'] : NULL;
  447. return implode(',', array_filter($address));
  448. }
  449. /**
  450. * Geocoder Widget - Parse a location field
  451. */
  452. function geocoder_widget_parse_locationfield($field_item) {
  453. $address = '';
  454. if (!empty($field_item['name'])) $address .= $field_item['name'] . ',';
  455. if (!empty($field_item['street'])) $address .= $field_item['street'] . ',';
  456. if (!empty($field_item['additional'])) $address .= $field_item['additional'] . ',';
  457. if (!empty($field_item['city'])) $address .= $field_item['city'] . ',';
  458. if (!empty($field_item['province']) && function_exists('location_province_name')) {
  459. $province_fullname = location_province_name($field_item['country'], $field_item['province']);
  460. $address .= $province_fullname . ',';
  461. }
  462. if (!empty($field_item['country'])) $address .= $field_item['country'] . ',';
  463. if (!empty($field_item['postal_code'])) $address .= $field_item['postal_code'] . ',';
  464. $address = rtrim($address, ', ');
  465. return $address;
  466. }