You are here

farm_movement.location.inc in farmOS 7

Code for managing the location of assets with movement logs.

File

modules/farm/farm_movement/farm_movement.location.inc
View source
<?php

/**
 * @file
 * Code for managing the location of assets with movement logs.
 */

/**
 * Generate markup that describes an asset's current location.
 *
 * @param FarmAsset $asset
 *   The farm asset.
 *
 * @return string
 *   Returns rendered HTML.
 */
function farm_movement_asset_location_markup($asset) {

  // Start an output string.
  $output = '<strong>' . t('Location') . ':</strong> ';

  // Get the asset's location.
  $areas = farm_movement_asset_location($asset);

  // If locations were found, add links to them.
  if (!empty($areas)) {
    $area_links = array();
    foreach ($areas as $area) {
      if (!empty($area->tid)) {
        $area_links[] = l($area->name, 'taxonomy/term/' . $area->tid);
      }
    }
    $output .= implode(', ', $area_links);
  }
  else {
    $output .= 'N/A';
  }

  // Get the asset's most recent movement.
  $log = farm_movement_asset_latest_movement($asset);

  // Load the log's movement field, if it exists.
  if (!empty($log->field_farm_movement[LANGUAGE_NONE][0]['value'])) {
    $movement = field_collection_item_load($log->field_farm_movement[LANGUAGE_NONE][0]['value']);
  }

  // If a geofield exists on the movement, display it.
  if (!empty($movement->field_farm_geofield[LANGUAGE_NONE][0]['geom'])) {

    // Build the geofield map and add it to the page content.
    $field_instance = field_info_instance('field_collection_item', 'field_farm_geofield', 'field_farm_movement');
    $geofield = field_view_field('field_collection_item', $movement, 'field_farm_geofield', $field_instance['display']['default']);
    $geofield['#title'] = t('Geometry');
    $output .= drupal_render($geofield);
  }

  // Return the output markup.
  return $output;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function farm_movement_form_farm_asset_form_alter(&$form, &$form_state, $form_id) {

  // Get the farm asset entity from the form.
  $asset = $form['farm_asset']['#value'];

  // Get the asset's current location.
  $areas = farm_movement_asset_location($asset);
  $area_names = array();
  if (!empty($areas)) {
    foreach ($areas as $area) {
      if (!empty($area->name)) {

        // Get the area name.
        $name = $area->name;

        // If the area name contains commas, wrap it in quotes.
        if (strpos($area->name, ',') !== FALSE) {
          $name = '"' . $area->name . '"';
        }

        // Add the name to the list.
        $area_names[] = $name;
      }
    }
  }

  // Assemble the list of areas into a string.
  $location = implode(', ', $area_names);

  // Add a field for setting the asset's current location.
  $form['location'] = array(
    '#type' => 'fieldset',
    '#title' => t('Location'),
    '#description' => t('Set the current areas(s) that this asset is in. Separate multiple areas with commas. A movement observation log will be created automatically if you change this field.'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#weight' => 100,
    '#tree' => TRUE,
  );
  $form['location']['areas'] = array(
    '#type' => 'textfield',
    '#title' => t('Current location'),
    '#autocomplete_path' => 'taxonomy/autocomplete/field_farm_area',
    '#default_value' => $location,
    '#maxlength' => NULL,
  );

  // Add validation function to validate location input.
  $form['actions']['submit']['#validate'][] = 'farm_movement_asset_location_validate';

  // Add submit function to process the location.
  $form['actions']['submit']['#submit'][] = 'farm_movement_asset_location_submit';

  // Put the location fieldset into the "General" field group.
  $form['#group_children']['location'] = 'group_farm_general';
}

/**
 * Validation handler for processing the asset location field.
 *
 * @param array $form
 *   The form array.
 * @param array $form_state
 *   The form state array.
 */
function farm_movement_asset_location_validate(array $form, array &$form_state) {

  // Only proceed if current location field has a value.
  if (empty($form_state['values']['location']['areas'])) {
    return;
  }

  // Explode the value into an array and only take the first value.
  // (Same behavior as taxonomy autocomplete widget.)
  $values = drupal_explode_tags($form_state['values']['location']['areas']);

  // Iterate over the values.
  foreach ($values as $value) {

    // If the area name is over 255 characters long, throw a form validation
    // error.
    if (strlen($value) > 255) {
      $message = t('The area name "%name" is too long. It must be under 255 characters.', array(
        '%name' => $value,
      ));
      form_set_error('location][areas', $message);
    }
  }
}

/**
 * Submit handler for processing the asset location field.
 *
 * @param array $form
 *   The form array.
 * @param array $form_state
 *   The form state array.
 */
function farm_movement_asset_location_submit(array $form, array &$form_state) {

  // Only proceed if current location field has a value.
  if (empty($form_state['values']['location']['areas'])) {
    return;
  }

  // Only proceed if the value is not the default value.
  if ($form_state['values']['location']['areas'] == $form['location']['areas']['#default_value']) {
    return;
  }

  // If an asset doesn't exist, bail.
  if (empty($form_state['values']['farm_asset'])) {
    return;
  }

  // Grab the asset.
  $asset = $form_state['values']['farm_asset'];

  // Load the areas.
  $areas = farm_term_parse_names($form_state['values']['location']['areas'], 'farm_areas', TRUE);

  // Create an observation log to record the movement.
  farm_movement_create($asset, $areas, REQUEST_TIME);
}

/**
 * Find the location of an asset, based on movement logs.
 *
 * @param FarmAsset $asset
 *   The farm_asset object to look for.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param bool|null $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   this is set to NULL, no filtering will be applied. Defaults to TRUE.
 *
 * @return array
 *   Returns an array of areas that the asset is in.
 */
function farm_movement_asset_location(FarmAsset $asset, $time = REQUEST_TIME, $done = TRUE) {
  $areas = array();

  // Load the log using our helper function.
  $log = farm_movement_asset_latest_movement($asset, $time, $done);

  // If a movement field doesn't exist, bail.
  if (empty($log->field_farm_movement[LANGUAGE_NONE][0]['value'])) {
    return $areas;
  }

  // Load the log's movement field
  $movement = field_collection_item_load($log->field_farm_movement[LANGUAGE_NONE][0]['value']);

  // Load the areas referenced in the "Move to" field.
  if (!empty($movement->field_farm_move_to[LANGUAGE_NONE])) {
    foreach ($movement->field_farm_move_to[LANGUAGE_NONE] as $area_reference) {
      if (!empty($area_reference['tid'])) {
        $term = taxonomy_term_load($area_reference['tid']);
        if (!empty($term)) {
          $areas[] = $term;
        }
      }
    }
  }
  return $areas;
}

/**
 * Find the geometry of an asset, based on movement logs.
 *
 * @param FarmAsset $asset
 *   The farm_asset object to look for.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param bool|null $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   this is set to NULL, no filtering will be applied. Defaults to TRUE.
 *
 * @return string
 *   Returns the asset's current geometry, in WKT (well-known text).
 */
function farm_movement_asset_geometry(FarmAsset $asset, $time = REQUEST_TIME, $done = TRUE) {
  $geometry = '';

  // Load the log using our helper function.
  $log = farm_movement_asset_latest_movement($asset, $time, $done);

  // If a movement field doesn't exist, bail.
  if (empty($log->field_farm_movement[LANGUAGE_NONE][0]['value'])) {
    return $geometry;
  }

  // Load the log's movement field
  $movement = field_collection_item_load($log->field_farm_movement[LANGUAGE_NONE][0]['value']);

  // Load the movement geometry.
  if (!empty($movement->field_farm_geofield[LANGUAGE_NONE][0]['geom'])) {
    $geometry = $movement->field_farm_geofield[LANGUAGE_NONE][0]['geom'];
  }
  return $geometry;
}

/**
 * Retrieve an area's movement history. This will provide an array of arrival
 * and departure logs for each asset that has been moved to the area. Only
 * movement logs that have been marked "done" will be included in the history.
 *
 * @param $area
 *   The farm area (taxonomy term object).
 * @param string|array $asset_types
 *   Limit to only include certain asset types. This can be a single asset type
 *   as a string, or an array of asset types. Defaults to empty array, which
 *   will include all asset types.
 * @param int $start_time
 *   How far back to look? This should be a UNIX timestamp. Defaults to NULL,
 *   which looks back through all movement logs in the system.
 * @param int $end_time
 *   How far forward to look? This should be a UNIX timestamp. Defaults to the
 *   current time, which causes future arrival movements to be excluded.
 *
 * @return array
 *   Returns an array of movement history for each asset in the area. Array
 *   keys are asset IDs, and each asset will contain an array of arrays that
 *   contain arrival and departure logs for each movement through the area.
 *   If an asset moved through the area more than once, it will have multiple
 *   sub-arrays for each arrival+departure. If a departure log is not found
 *   (eg: if the asset has not left the area), the 'depart' key will be NULL.
 *
 *   Example:
 *     array(
 *       '50' => array(
 *         array(
 *           'arrive' => [$log],
 *           'depart' => [$log],
 *         ),
 *         array(
 *           'arrive' => [$log],
 *           'depart' => NULL,
 *         ),
 *       ),
 *       '51' => array(
 *         array(
 *           'arrive' => [$log],
 *           'depart' => [$log],
 *         ),
 *       ),
 *     );
 */
function farm_movement_area_history($area, $asset_types = array(), $start_time = NULL, $end_time = REQUEST_TIME) {

  // Start an empty history array.
  $history = array();

  // If the area doesn't have an id, bail.
  if (empty($area->tid)) {
    return $history;
  }

  // If $asset_types is not an array, wrap it in one.
  if (!is_array($asset_types)) {
    $asset_types = array(
      $asset_types,
    );
  }

  // Build a query to retrieve movement logs to this area.
  $query = farm_movement_area_movement_query($area->tid, $end_time);

  // Add the log ID field.
  $query
    ->addField('ss_log', 'id');

  // Filter to only include logs that happened AFTER the start time.
  if (!empty($start_time)) {
    $query
      ->condition('ss_log.timestamp', $start_time, '>');
  }

  // Join in asset references, and then the farm_asset table record for each.
  $query
    ->join('field_data_field_farm_asset', 'ss_fdffa', "ss_fdffa.entity_type = 'log' AND ss_fdffa.entity_id = ss_log.id AND ss_fdffa.deleted = 0");
  $query
    ->join('farm_asset', 'ss_fa', 'ss_fa.id = ss_fdffa.field_farm_asset_target_id');

  // Filter to only include certain asset types.
  if (!empty($asset_types)) {
    $query
      ->condition('ss_fa.type', $asset_types, 'IN');
  }

  // Group by log ID so that we don't get duplicate rows from logs that
  // reference multiple assets.
  $query
    ->groupBy('ss_log.id');

  // Execute the query to get a list of log IDs.
  $result = $query
    ->execute();

  // Iterate through the log IDs.
  foreach ($result as $row) {

    // If the log ID is empty, skip it.
    if (empty($row->id)) {
      continue;
    }

    // Load the asset's arrival log.
    $log_arrive = log_load($row->id);

    // Create an entity metadata wrapper for the log.
    $log_wrapper = entity_metadata_wrapper('log', $log_arrive);

    // Iterate through the assets.
    foreach ($log_wrapper->field_farm_asset as $asset_wrapper) {

      // Get the asset object.
      $asset = $asset_wrapper
        ->value();

      // The the asset doesn't have an ID, skip it.
      if (empty($asset->id)) {
        continue;
      }

      // If the asset is not one of the desired types, skip it.
      if (!empty($asset_types) && !in_array($asset->type, $asset_types)) {
        continue;
      }

      // Look up the asset's next movement log (departure from the area). Only
      // include logs that have been marked "done".
      $log_depart = farm_movement_asset_next_movement($asset, $log_arrive->timestamp, TRUE);

      // Record the asset's time spent in this area.
      $history[$asset->id][] = array(
        'arrive' => $log_arrive,
        'depart' => !empty($log_depart) ? $log_depart : NULL,
      );
    }
  }

  // Return the history.
  return $history;
}

/**
 * Load an asset's latest log that defines a movement.
 *
 * @param FarmAsset $asset
 *   The farm_asset object to look for.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   any other value is used, no filtering will be applied. Defaults to TRUE.
 *
 * @return Log|bool
 *   Returns a log entity. FALSE if something goes wrong.
 */
function farm_movement_asset_latest_movement(FarmAsset $asset, $time = REQUEST_TIME, $done = TRUE) {

  /**
   * Please read the comments in farm_movement_asset_movement_query() to
   * understand how this works, and to be aware of the limitations and
   * responsibilities we have in this function with regard to sanitizing query
   * inputs.
   */

  // If the asset doesn't have an ID (for instance if it is new and hasn't been
  // saved yet), bail.
  if (empty($asset->id)) {
    return FALSE;
  }

  // Make a query for loading the latest movement log.
  $query = farm_movement_asset_movement_query($asset->id, $time, $done);

  // Execute the query and gather the log id.
  $result = $query
    ->execute();
  $log_id = $result
    ->fetchField();

  // If a log id exists, load and return it.
  if (!empty($log_id)) {
    return log_load($log_id);
  }
  return FALSE;
}

/**
 * Load an asset's next log that defines a movement.
 *
 * @param FarmAsset $asset
 *   The farm_asset object to look for.
 * @param int $time
 *   Unix timestamp limiter. Only logs after this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute first.
 * @param bool|null $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   this is set to NULL, no filtering will be applied. Defaults to FALSE
 *   because the default $time is now, and future logs are generally not done
 *   yet.
 *
 * @return Log|bool
 *   Returns a log entity. FALSE if something goes wrong.
 */
function farm_movement_asset_next_movement(FarmAsset $asset, $time = REQUEST_TIME, $done = FALSE) {

  /**
   * Please read the comments in farm_movement_asset_movement_query() to
   * understand how this works, and to be aware of the limitations and
   * responsibilities we have in this function with regard to sanitizing query
   * inputs.
   */

  // Ensure $time is valid, because it may be used directly in the query
  // string. This is defensive code. See note about
  // farm_movement_asset_movement_query() above.
  if (!is_numeric($time) || $time < 0) {
    $time = REQUEST_TIME;
  }

  // If the asset doesn't have an ID (for instance if it is new and hasn't been
  // saved yet), bail.
  if (empty($asset->id)) {
    return FALSE;
  }

  // Make a query to load all movement logs for the asset. Use a timestamp of 0
  // to include future logs.
  $query = farm_movement_asset_movement_query($asset->id, 0, $done, FALSE);

  // Filter to only include movements after the specified timestamp.
  $query
    ->where('ss_log.timestamp > ' . $time);

  // Order by timestamp and log ID ascending so we can get the first one (this
  // overrides the default sort added by farm_log_query())
  $query
    ->orderBy('ss_log.timestamp', 'ASC');
  $query
    ->orderBy('ss_log.id', 'ASC');

  // Limit to 1 record.
  $query
    ->range(0, 1);

  // Execute the query and gather the log id.
  $result = $query
    ->execute();
  $log_id = $result
    ->fetchField();

  // If a log id exists, load and return it.
  if (!empty($log_id)) {
    return log_load($log_id);
  }
  return FALSE;
}

/**
 * Build a query to find movement logs of a specific asset.
 *
 * @param int|string $asset_id
 *   The asset id to search for. This can either be a specific id, or a field
 *   alias string from another query (ie: 'mytable.assetid'). For an example
 *   of field alias string usage, see the Views relationship handler code in
 *   farm_movement_handler_relationship_location::query().
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   any other value is used, no filtering will be applied. Defaults to TRUE.
 * @param bool $single
 *   Whether or not to limit the query to a single result. Defaults to TRUE.
 * @param string $field
 *   If the log id is desired, use "log_id. If the movement field_collection id
 *   is desired, use "movement_id".
 *
 * @return \SelectQuery
 *   Returns a SelectQuery object.
 */
function farm_movement_asset_movement_query($asset_id, $time = REQUEST_TIME, $done = TRUE, $single = TRUE, $field = 'log_id') {

  /**
   * Please read the comments in farm_log_asset_query() to understand how this
   * works, and to be aware of the limitations and responsibilities we have in
   * this function with regard to sanitizing query inputs.
   */

  // Use the farm_log_asset_query() helper function to start a query object.
  $query = farm_log_asset_query($asset_id, $time, $done, NULL, $single);

  // Add a query tag to identify where this came from.
  $query
    ->addTag('farm_movement_asset_movement_query');

  // Join in the Movement field collection and filter to only include logs with
  // movements. Use an inner join to exclude logs that do not have any
  // movement field collections attached.
  $query
    ->innerJoin('field_data_field_farm_movement', 'ss_fdffm', "ss_fdffm.entity_type = 'log' AND ss_fdffm.entity_id = ss_log.id AND ss_fdffm.deleted = 0");

  // Join in the movement's "move to" field, and filter to only include logs
  // that have a movement with a "move to" value. Use an inner join to exclude
  // logs that do not have a "move to" area reference.
  $query
    ->innerJoin('field_data_field_farm_move_to', 'ss_fdffmt', "ss_fdffmt.entity_type = 'field_collection_item' AND ss_fdffmt.bundle = 'field_farm_movement' AND ss_fdffmt.entity_id = ss_fdffm.field_farm_movement_value AND ss_fdffmt.deleted = 0");

  // If $field is 'log_id', then add the log ID field.
  if ($field == 'log_id') {
    $query
      ->addField('ss_log', 'id');
  }
  elseif ($field == 'movement_id') {
    $query
      ->addField('ss_fdffm', 'field_farm_movement_value');
  }

  // Return the query object.
  return $query;
}

/**
 * Build a query to find movement logs to a specific area.
 *
 * @param int $area_id
 *   The area id to search for.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   any other value is used, no filtering will be applied. Defaults to TRUE.
 *
 * @return \SelectQuery
 *   Returns a SelectQuery object.
 */
function farm_movement_area_movement_query($area_id, $time = REQUEST_TIME, $done = TRUE) {

  /**
   * Please read the comments in farm_log_query() to understand how this works,
   * and to be aware of the limitations and responsibilities we have in this
   * function with regard to sanitizing query inputs.
   */

  // Ensure $area_id is valid, because it will be used directly in the query
  // string. This is defensive code. See note about farm_log_query() above.
  if (!is_numeric($area_id) || $area_id < 0) {
    $area_id = db_escape_field($area_id);
  }

  // Use the farm_log_query() helper function to start a query object. Do not
  // limit the results to a single row because by the very nature of this we
  // want to find all assets in the area, which may come from multiple logs.
  $query = farm_log_query($time, $done, NULL, FALSE);

  // Add a query tag to identify where this came from.
  $query
    ->addTag('farm_movement_area_movement_query');

  // Join in the Movement field collection and filter to only include logs with
  // movements. Use an inner join to exclude logs that do not have a movement
  // field collection attached.
  $query
    ->innerJoin('field_data_field_farm_movement', 'ss_fdffm', "ss_fdffm.entity_type = 'log' AND ss_fdffm.entity_id = ss_log.id AND ss_fdffm.deleted = 0");

  // Join in the movement's "move to" field, and filter to only include logs
  // that have a movement with a "move to" the specified area. Use an inner
  // join to exclude logs that do not have a "move to" area reference.
  $query
    ->innerJoin('field_data_field_farm_move_to', 'ss_fdffmt', "ss_fdffmt.entity_type = 'field_collection_item' AND ss_fdffmt.bundle = 'field_farm_movement' AND ss_fdffmt.entity_id = ss_fdffm.field_farm_movement_value AND ss_fdffmt.deleted = 0");
  $query
    ->where('ss_fdffmt.field_farm_move_to_tid = ' . $area_id);

  // Return the query object.
  return $query;
}

/**
 * Load all assets in an area.
 *
 * @param $area
 *   The area to load assets from.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param bool|null $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   this is set to NULL, no filtering will be applied. Defaults to TRUE.
 * @param bool $archived
 *   Whether or not to include archived assets. Defaults to FALSE.
 *
 * @return array
 *   Returns an array of the area's assets, keyed by asset ID.
 */
function farm_movement_area_assets($area, $time = REQUEST_TIME, $done = TRUE, $archived = FALSE) {

  /**
   * @todo
   * Merge/abstract with farm_group_members().
   */

  // Start an empty array of assets.
  $assets = array();

  // If the area doesn't have an id, bail.
  if (empty($area->tid)) {
    return $assets;
  }

  // Build a query to find all assets in the area.
  $query = farm_movement_area_assets_query($area->tid, $time, $done, $archived);

  // Execute the query to get a list of asset IDs.
  $result = $query
    ->execute();

  // Iterate through the results.
  foreach ($result as $row) {

    // If the asset ID is empty, skip it.
    if (empty($row->asset_id)) {
      continue;
    }

    // If the asset has already been loaded, skip it.
    if (array_key_exists($row->asset_id, $assets)) {
      continue;
    }

    // Load the asset.
    $assets[$row->asset_id] = farm_asset_load($row->asset_id);
  }

  // Return the array of assets.
  return $assets;
}

/**
 * Build a query to find assets in a given area.
 *
 * @param int $area_id
 *   The area's taxonomy term id to search for.
 * @param int $time
 *   Unix timestamp limiter. Only logs before this time will be included.
 *   Defaults to the current time. Set to 0 to load the absolute last.
 * @param $done
 *   Whether or not to only show logs that are marked as "done". TRUE will limit
 *   to logs that are done, and FALSE will limit to logs that are not done. If
 *   any other value is used, no filtering will be applied. Defaults to TRUE.
 * @param bool $archived
 *   Whether or not to include archived assets. Defaults to FALSE.
 *
 * @return \SelectQuery
 *   Returns a SelectQuery object.
 */
function farm_movement_area_assets_query($area_id, $time = REQUEST_TIME, $done = TRUE, $archived = FALSE) {

  /**
   * @todo
   * Merge/abstract with farm_group_members_query().
   */

  /**
   * Please read the comments in farm_log_asset_query() to understand how this
   * works, and to be aware of the limitations and responsibilities we have in
   * this function with regard to sanitizing query inputs.
   */

  // Ensure $area_id is valid, because it will be used directly in the query
  // string. This is defensive code. See note about farm_log_query() above.
  if (!is_numeric($area_id) || $area_id < 0) {
    $area_id = db_escape_field($area_id);
  }

  // Use the farm_log_asset_query() helper function to start a subquery object.
  // Do not limit the results to a single row because by the very nature of
  // this we want to find all assets in the area, which may come from multiple
  // logs.
  $subquery = farm_log_asset_query(NULL, $time, $done, NULL, FALSE);

  // Add a query tag to identify where this came from.
  $subquery
    ->addTag('farm_movement_area_assets_query');

  // Join in the Movement field collection. Use an inner join to exclude logs
  // that do not have a movement field collection attached.
  $subquery
    ->innerJoin('field_data_field_farm_movement', 'ss_fdffm', "ss_fdffm.entity_type = 'log' AND ss_fdffm.entity_id = ss_log.id AND ss_fdffm.deleted = 0");

  // Add the asset ID field.
  $subquery
    ->addField('ss_fdffa', 'field_farm_asset_target_id');

  // Add an expression to extract the assets most recent movement log ID.
  $subquery
    ->addExpression("SUBSTRING_INDEX(GROUP_CONCAT(ss_log.id ORDER BY ss_log.timestamp DESC, ss_log.id DESC SEPARATOR ','), ',', 1)", 'ss_current_log_id');

  // Group by asset ID.
  $subquery
    ->groupBy('ss_fdffa.field_farm_asset_target_id');

  // Create a query that selects from the subquery.
  $query = db_select($subquery, 'ss_asset_current_log');

  // Join in the asset's current log.
  $query
    ->join('log', 'ss_current_log', 'ss_current_log.id = ss_asset_current_log.ss_current_log_id');

  // Join in the Movement field collection. Use an inner join to exclude logs
  // that do not have a movement field collection attached.
  $query
    ->innerJoin('field_data_field_farm_movement', 'ss_current_log_fdffm', "ss_current_log_fdffm.entity_type = 'log' AND ss_current_log_fdffm.entity_id = ss_current_log.id AND ss_current_log_fdffm.deleted = 0");

  // Join in the movement's "move to" field, and filter to only include logs
  // that have a movement that references the specified area. Use an inner
  // join to exclude logs that do not have an area reference.
  $query
    ->innerJoin('field_data_field_farm_move_to', 'ss_current_log_fdffmt', "ss_current_log_fdffmt.entity_type = 'field_collection_item' AND ss_current_log_fdffmt.bundle = 'field_farm_movement' AND ss_current_log_fdffmt.entity_id = ss_current_log_fdffm.field_farm_movement_value AND ss_current_log_fdffmt.deleted = 0");
  $query
    ->where('ss_current_log_fdffmt.field_farm_move_to_tid = ' . $area_id);

  // Exclude archived assets, if requested.
  if (empty($archived)) {
    $query
      ->join('farm_asset', 'ss_current_log_fa', "ss_asset_current_log.field_farm_asset_target_id = ss_current_log_fa.id");
    $query
      ->where('ss_current_log_fa.archived = 0');
  }

  // Add the asset ID field.
  $query
    ->addField('ss_asset_current_log', 'field_farm_asset_target_id', 'asset_id');

  // Return the query object.
  return $query;
}

/**
 * Implements hook_action_info().
 */
function farm_movement_action_info() {
  return array(
    'farm_movement_asset_move_action' => array(
      'type' => 'farm_asset',
      'label' => t('Move'),
      'configurable' => TRUE,
      'triggers' => array(
        'any',
      ),
      'aggregate' => TRUE,
    ),
  );
}

/**
 * Configuration form for farm_movement_asset_move action.
 *
 * @param array $context
 *   The context passed into the action form function.
 * @param array $form_state
 *   The form state passed into the action form function.
 *
 * @return array
 *   Returns a form array.
 */
function farm_movement_asset_move_action_form(array $context, array $form_state) {

  // Date field.
  $form['date'] = array(
    '#type' => 'date_select',
    '#title' => t('Date'),
    '#date_format' => 'M j Y',
    '#date_type' => DATE_FORMAT_UNIX,
    '#date_year_range' => '-10:+3',
    '#default_value' => date('Y-m-d H:i', REQUEST_TIME),
    '#required' => TRUE,
  );

  // Area reference field.
  $form['areas'] = array(
    '#type' => 'textfield',
    '#title' => t('Location'),
    '#autocomplete_path' => 'taxonomy/autocomplete/field_farm_area',
    '#required' => TRUE,
  );

  // Done field.
  $form['done'] = array(
    '#type' => 'checkbox',
    '#title' => t('This movement has taken place (mark the log as done)'),
    '#default_value' => TRUE,
  );

  // Return the form.
  return $form;
}

/**
 * Submit handler for farm_movement_asset_move action configuration form.
 *
 * @param array $form
 *   The form array.
 * @param array $form_state
 *   The form state array.
 *
 * @return array
 *   Returns an array that will end up in the action's context.
 */
function farm_movement_asset_move_action_submit(array $form, array $form_state) {

  // Start to build the context array.
  $context = array();

  // Load the areas.
  $context['areas'] = farm_term_parse_names($form_state['values']['areas'], 'farm_areas', TRUE);

  // Convert the date to a timestamp.
  $timestamp = strtotime($form_state['values']['date']);

  // The action form only includes month, day, and year. If the movement is
  // today, then we assume that the current time should also be included.
  if (date('Ymd', $timestamp) == date('Ymd', REQUEST_TIME)) {
    $context['timestamp'] = REQUEST_TIME;
  }
  else {
    $context['timestamp'] = $timestamp;
  }

  // Copy the "done" value as a boolean.
  $context['done'] = !empty($form_state['values']['done']) ? TRUE : FALSE;

  // Return the context array.
  return $context;
}

/**
 * Action function for farm_movement_asset_move.
 *
 * Creates a new movement activity log for the specified assets.
 *
 * @param array $assets
 *   An array of asset entities to move.
 * @param array $context
 *   Array with parameters for this action.
 */
function farm_movement_asset_move_action(array $assets, $context = array()) {

  // If we're missing assets, areas, or a timestamp, bail.
  if (empty($assets) || empty($context['areas']) || empty($context['timestamp'])) {
    drupal_set_message(t('Could not perform movement because required information was missing.'), 'error');
    return;
  }

  // Create a movement activity log.
  farm_movement_create($assets, $context['areas'], $context['timestamp'], 'farm_activity', $context['done']);
}

/**
 * Create a log for moving assets to areas.
 *
 * @param array|FarmAsset $assets
 *   Array of assets to include in the move.
 * @param array $areas
 *   An array of areas to move to.
 * @param int $timestamp
 *   The timestamp of the move. Defaults to the current time.
 * @param string $log_type
 *   The type of log to create. Defaults to "farm_observation".
 * @param bool $done
 *   Boolean indicating whether or not the log should be marked "done". Defaults
 *   to TRUE.
 * @param string $geom
 *   Optionally provide a movement geometry in WKT format. If this is empty, the
 *   geometry will be copied from the referenced area(s).
 *
 * @return \Log
 *   Returns the log that was created.
 */
function farm_movement_create($assets, $areas = array(), $timestamp = REQUEST_TIME, $log_type = 'farm_observation', $done = TRUE, $geom = '') {

  // If no areas are defined, bail.
  if (empty($areas)) {
    return;
  }

  // If $assets isn't an array, wrap it.
  if (!is_array($assets)) {
    $assets = array(
      $assets,
    );
  }

  // If the log is an observation, set the name to:
  // "[assets] located in [areas]".
  // If the log is an activity, set the name to:
  // "Move [assets] to [areas]".
  $log_name = '';
  $assets_summary = farm_log_entity_label_summary('farm_asset', $assets);
  $areas_summary = farm_log_entity_label_summary('taxonomy_term', $areas);
  $arguments = array(
    '!assets' => $assets_summary,
    '!areas' => $areas_summary,
  );
  if ($log_type == 'farm_observation') {
    $log_name = t('!assets located in !areas', $arguments);
  }
  elseif ($log_type == 'farm_activity') {
    $log_name = t('Move !assets to !areas', $arguments);
  }

  // Create a new farm log entity.
  $log = farm_log_create($log_type, $log_name, $timestamp, $done, $assets);

  // Create a new movement field_collection entity attached to the log.
  $movement = entity_create('field_collection_item', array(
    'field_name' => 'field_farm_movement',
  ));
  $movement
    ->setHostEntity('log', $log);

  // Create an entity wrapper for the adjustment.
  $movement_wrapper = entity_metadata_wrapper('field_collection_item', $movement);

  // Iterate through the areas and add each to the "Move to" field.
  foreach ($areas as $area) {
    $movement_wrapper->field_farm_move_to[] = $area;
  }

  // Add geometry, if provided.
  if (!empty($geom)) {
    $movement_wrapper->field_farm_geofield
      ->set(array(
      array(
        'geom' => $geom,
      ),
    ));
  }

  // Save the movement.
  $movement_wrapper
    ->save();

  // Return the log.
  return $log;
}

Functions

Namesort descending Description
farm_movement_action_info Implements hook_action_info().
farm_movement_area_assets Load all assets in an area.
farm_movement_area_assets_query Build a query to find assets in a given area.
farm_movement_area_history Retrieve an area's movement history. This will provide an array of arrival and departure logs for each asset that has been moved to the area. Only movement logs that have been marked "done" will be included in the history.
farm_movement_area_movement_query Build a query to find movement logs to a specific area.
farm_movement_asset_geometry Find the geometry of an asset, based on movement logs.
farm_movement_asset_latest_movement Load an asset's latest log that defines a movement.
farm_movement_asset_location Find the location of an asset, based on movement logs.
farm_movement_asset_location_markup Generate markup that describes an asset's current location.
farm_movement_asset_location_submit Submit handler for processing the asset location field.
farm_movement_asset_location_validate Validation handler for processing the asset location field.
farm_movement_asset_movement_query Build a query to find movement logs of a specific asset.
farm_movement_asset_move_action Action function for farm_movement_asset_move.
farm_movement_asset_move_action_form Configuration form for farm_movement_asset_move action.
farm_movement_asset_move_action_submit Submit handler for farm_movement_asset_move action configuration form.
farm_movement_asset_next_movement Load an asset's next log that defines a movement.
farm_movement_create Create a log for moving assets to areas.
farm_movement_form_farm_asset_form_alter Implements hook_form_FORM_ID_alter().