You are here

farm_area_generate.module in farmOS 7

Farm area generate module.

File

modules/farm/farm_area/farm_area_generate/farm_area_generate.module
View source
<?php

/**
 * @file
 * Farm area generate module.
 */

/**
 * Implements hook_permission().
 */
function farm_area_generate_permission() {
  return array(
    'use farm area generator' => array(
      'title' => t('Use farm area generator tool'),
      'description' => t('Use the farm area generator tool.'),
    ),
  );
}

/**
 * Implements hook_farm_access_perms().
 */
function farm_area_generate_farm_access_perms($role) {

  // Load the list of farm roles.
  $roles = farm_access_roles();

  // If this role has 'config' access, grant area generator access.
  if (!empty($roles[$role]['access']['config'])) {
    return array(
      'use farm area generator',
    );
  }
  else {
    return array();
  }
}

/**
 * Implements hook_menu().
 */
function farm_area_generate_menu() {

  // Area generator form.
  $items['farm/areas/generate'] = array(
    'title' => 'Bed generator',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'farm_area_generate_form',
    ),
    'access arguments' => array(
      'use farm area generator',
    ),
    'type' => MENU_LOCAL_TASK,
  );

  // Area generator callback.
  $items['farm/areas/generate/callback'] = array(
    'page callback' => 'farm_area_generate_callback',
    'access arguments' => array(
      'use farm area generator',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Check to see if GEOS is installed and available.
 *
 * @return bool
 *   Returns TRUE if GEOS is installed, FALSE otherwise.
 */
function farm_area_generate_geos_exists() {
  geophp_load();
  return geoPHP::geosInstalled();
}

/**
 * Implements hook_page_build().
 */
function farm_area_generate_page_build(&$page) {

  // If the user does not have access to use the area generator, bail.
  if (!user_access('use farm area generator')) {
    return;
  }

  // If this is the area generator form page, add the areas map.
  if (current_path() == 'farm/areas/generate') {

    // Build the map and add it to the page content.
    $page['content']['farm_areas'] = farm_map_build('farm_area_generate');

    // Set the weight to 100 so that it appears on bottom.
    $page['content']['farm_areas']['#weight'] = 100;

    // Set the content region #sorted flag to FALSE so that it resorts.
    $page['content']['#sorted'] = FALSE;
  }
}

/**
 * Area generator form.
 */
function farm_area_generate_form($form, &$form_state) {

  // Set the page title.
  drupal_set_title('Bed generator');

  // Add javascript.
  drupal_add_js(drupal_get_path('module', 'farm_area_generate') . '/js/farm_area_generate.js');

  // Hidden field for generated area WKT. If the areas were generated via the
  // "Preview" button, WKT will be stored in $form_state['storage']['wkt'].
  $form['wkt'] = array(
    '#type' => 'hidden',
    '#value' => !empty($form_state['storage']['wkt']) ? $form_state['storage']['wkt'] : '',
    '#prefix' => '<div id="generated-wkt">',
    '#suffix' => '</div>',
  );

  // Description.
  $form['description'] = array(
    '#type' => 'markup',
    '#markup' => t('This area generator tool is used to generate multiple parallel child areas within a parent area. This is useful for automatically drawing beds within a field. Select a parent area, specify how many child areas you want in it, and the area orientation. Areas will be generated to fill the parent.'),
  );

  // Area select.
  $form['area'] = array(
    '#type' => 'textfield',
    '#title' => t('Area'),
    '#description' => t('Set the parent area that the generated areas will be within. This area must have a single polygon geometry. Click on an area in the map below to auto-fill this field.'),
    '#autocomplete_path' => 'taxonomy/autocomplete/field_farm_area',
    '#required' => TRUE,
  );

  // Number of child areas to generate.
  $form['count'] = array(
    '#type' => 'textfield',
    '#title' => t('Number of child areas'),
    '#description' => t('How many child areas should be generated within the area? The width of each child area will be automatically calculated.'),
    '#required' => TRUE,
    '#element_validate' => array(
      'element_validate_integer_positive',
    ),
  );

  // Orientation.
  $form['orientation'] = array(
    '#type' => 'textfield',
    '#title' => t('Orientation'),
    '#description' => t('Specify the orientation of the child areas in degrees (between 0 and 360). For example: areas running east/west would have an orientation of 0 or 180, while areas running north/south would have an orientation of 90 or 270. Areas will be numbered automatically in the direction of the orientation. For example: an orientation of 90 will number the areas from west to east, while an orientation of 270 will number the areas from east to west. Note that 0 and 360 are considered equivalent.'),
    '#required' => TRUE,
    '#default_value' => 0,
    '#element_validate' => array(
      'element_validate_integer',
    ),
  );

  // Add a collapsed fieldset containing other options.
  $form['options'] = array(
    '#type' => 'fieldset',
    '#title' => t('Options'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  // Area type.
  $area_type_options = farm_area_type_options();
  $form['options']['area_type'] = array(
    '#type' => 'select',
    '#title' => t('Area type'),
    '#description' => t('Select the type of areas that will be generated.'),
    '#options' => $area_type_options,
    '#requited' => TRUE,
  );

  // If the 'bed' area type is available, default to that.
  // Otherwise, default to 'other' because it's the only one we can depend on.

  /**
   * @todo
   * This was added to make it clear that the area generator can be used to
   * generate parallel beds, while also maintaining the original flexibility.
   * In the future, if the area generator expands to be used in other cases,
   * we may want to remove this default.
   */
  $default_area_type = 'other';
  if (array_key_exists('bed', $area_type_options)) {
    $default_area_type = 'bed';
  }
  $form['more']['area_type']['#default_value'] = $default_area_type;

  // If GEOS is not installed, print a warning and hide the submit button.
  if (!farm_area_generate_geos_exists()) {
    drupal_set_message(t('This area generator tool requires the GEOS libary') . ': ' . l('http://trac.osgeo.org/geos', 'http://trac.osgeo.org/geos'), 'warning');
  }
  else {
    $form['actions']['preview'] = array(
      '#type' => 'submit',
      '#value' => t('Preview'),
      '#submit' => array(
        'farm_area_generate_form_preview',
      ),
      '#ajax' => array(
        'callback' => 'farm_area_generate_form_ajax',
      ),
    );
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Generate'),
    );
  }
  return $form;
}

/**
 * Ajax callback for farm_area_generate_form().
 */
function farm_area_generate_form_ajax($form, $form_state) {

  // Get the "wkt" form element and CSS selector.
  $element = $form['wkt'];
  $selector = '#generated-wkt';

  // Assemble commands...
  $commands = array();

  // Replace the hidden field.
  $commands[] = ajax_command_replace($selector, render($element));

  // Execute Javascript to add WKT to the map.
  $commands[] = array(
    'command' => 'farmAreaGeneratePreview',
  );

  // Display status messages.
  $commands[] = ajax_command_prepend($selector, theme('status_messages'));

  // Return ajax commands.
  return array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );
}

/**
 * Area generator form validate.
 */
function farm_area_generate_form_validate(&$form, &$form_state) {

  // If GEOS is not installed, prevent submission.
  if (!farm_area_generate_geos_exists()) {
    form_set_error('', 'This area generator tool requires the GEOS libary: ' . l('http://trac.osgeo.org/geos', 'http://trac.osgeo.org/geos'));
    return;
  }

  // Load the area and store it in the form state.
  $area = farm_term($form_state['values']['area'], 'farm_areas', FALSE);
  $form_state['storage']['area'] = $area;

  // Ensure the area exists.
  if (empty($area)) {
    form_set_error('area', 'The area does not exist.');
    return;
  }

  // Ensure that the area doesn't already have child areas.
  $children = taxonomy_get_children($area->tid);
  $children_exist = FALSE;
  foreach ($children as $child) {
    if (!empty($child->field_farm_area_type[LANGUAGE_NONE][0]['value'])) {
      $children_exist = TRUE;
      break;
    }
  }
  if ($children_exist) {
    form_set_error('area', 'The selected area already has child areas.');
  }

  // Ensure that the area has a geometry.
  if (empty($area->field_farm_geofield[LANGUAGE_NONE][0]['geom'])) {
    form_set_error('area', 'The selected area does not have any geometry defined.');
    return;
  }

  // Ensure that the area's geometry is a single polygon.
  geophp_load();
  $geom = $area->field_farm_geofield[LANGUAGE_NONE][0]['geom'];
  $polygon = geoPHP::load($geom, 'wkt');
  $polygon = geoPHP::geometryReduce($polygon);
  $form_state['storage']['polygon'] = $polygon;
  if ($polygon
    ->geometryType() != 'Polygon') {
    form_set_error('area', 'The selected area is not a single polygon. Areas cannot be generated within points, lines, or complex geometries.');
    return;
  }

  // Put an upper limit on the number of child that can be created per area.
  if ($form_state['values']['count'] > 150) {
    form_set_error('count', 'The area generator tool has a limit of 150 areas. If you need to add more, consider breaking your area up into sub-areas first.');
  }

  // Ensure that the orientation is between 0 and 360.
  if ($form_state['values']['orientation'] < 0 || $form_state['values']['orientation'] > 360) {
    form_set_error('orientation', 'The orientation must be a number between 0 and 360.');
  }
}

/**
 * Farm area generator form preview.
 */
function farm_area_generate_form_preview(&$form, &$form_state) {

  // If the area and polygon was not stored in validation, bail.
  if (empty($form_state['storage']['area']) || empty($form_state['storage']['polygon'])) {
    return;
  }

  // Get all the necessary variables from the form state.
  $polygon = $form_state['storage']['polygon'];
  $count = $form_state['values']['count'];
  $orientation = $form_state['values']['orientation'];

  // Generate child area geometries.
  $geometries = farm_area_generate_geometries($polygon, $count, $orientation);
  if (!empty($geometries)) {
    $collection = new GeometryCollection($geometries);
    $form_state['storage']['wkt'] = $collection
      ->asText();
  }

  // Rebuild the form if WKT was generated.
  if (!empty($form_state['storage']['wkt'])) {
    $form_state['rebuild'] = TRUE;
  }
}

/**
 * Farm area generate form submit.
 */
function farm_area_generate_form_submit(&$form, &$form_state) {

  // If the area and polygon was not stored in validation, bail.
  if (empty($form_state['storage']['area']) || empty($form_state['storage']['polygon'])) {
    return;
  }

  // Get all the necessary variables from the form state.
  $area = $form_state['storage']['area'];
  $polygon = $form_state['storage']['polygon'];
  $area_type = $form_state['values']['area_type'];
  $count = $form_state['values']['count'];
  $orientation = $form_state['values']['orientation'];

  // Load the area type label and convert to lowercase.
  $area_types = farm_area_type_options();
  $area_type_label = strtolower($area_types[$area_type]);

  // Generate child geometries.
  $geometries = farm_area_generate_geometries($polygon, $count, $orientation);

  // Iterate through the geometries and save each one as a new area.
  foreach ($geometries as $key => $geometry) {

    // Assemble the new area name.
    $area_name = $area->name . ' ' . $area_type_label . ' ' . ($key + 1);

    // Create a new area term object.
    $new_area = farm_term($area_name, 'farm_areas', TRUE, FALSE);

    // Set the area type, geofield, parent, and weight.
    $new_area->field_farm_area_type = array(
      LANGUAGE_NONE => array(
        0 => array(
          'value' => $area_type,
        ),
      ),
    );
    $new_area->field_farm_geofield = array(
      LANGUAGE_NONE => array(
        0 => array(
          'geom' => $geometry
            ->asText(),
        ),
      ),
    );
    $new_area->parent = $area->tid;
    $new_area->weight = $key;
    taxonomy_term_save($new_area);
  }

  // Announce how many areas were generated.
  drupal_set_message(t('@count areas were generated.', array(
    '@count' => count($geometries),
  )));
}

/**
 * Generate geometries within a polygon at a given orientation.
 *
 * @param $polygon
 *   A Polygon geometry object that represents the parent area. Child area
 *   geometries will be generated within this polygon.
 * @param $count
 *   The number of child geometries to generate.
 * @param $orientation
 *   The orientation of child geometries, in degrees. This should be a positive
 *   integer between 0 and 359.
 *
 * @return array
 *   Returns an array of child geometries.
 */
function farm_area_generate_geometries($polygon, $count, $orientation = 0) {

  // If the orientation isn't within an acceptable range, bail.
  if ($orientation < 0 || $orientation > 360) {
    return array();
  }

  // Set BCMath scale.
  farm_map_set_bcscale();

  // If the orientation is 360, reset to 0.
  if ($orientation == 360) {
    $orientation = 0;
  }

  // Rotate the polygon around its centroid so that the orientation is 0.
  $origin = $polygon
    ->centroid();
  $angle = 359 - $orientation;
  $rotated_polygon = farm_area_generate_rotate_polygon($polygon, $origin, $angle);

  // Calculate the bounding box of the rotated polygon.
  $bbox = $rotated_polygon
    ->getBBox();

  // Generate horizontal rectangles that fill the bounding box.
  $geometries = farm_area_generate_bbox_geometries($bbox, $count);

  // Rotate child geometries back to the original orientation and trim to fit
  // the original polygon.
  $final_geometries = array();
  foreach ($geometries as $geometry) {
    $rotated = farm_area_generate_rotate_polygon($geometry, $origin, $orientation);
    $trimmed = $rotated
      ->intersection($polygon);
    $final_geometries[] = $trimmed;
  }

  // Reset BCMath scale.
  farm_map_reset_bcscale();
  return $final_geometries;
}

/**
 * Generates a set of rectangle geometries running horizontally to fill a
 * bounding box.
 *
 * @param array $bbox
 *   An array containing bounding box information (in the same format that
 *   GeoPHP generates).
 * @param int $count
 *   The number of rectangles to fit into the box.
 *
 * @return array
 *   Returns an array of rectangle geometries that fit into the bounding box.
 */
function farm_area_generate_bbox_geometries($bbox, $count) {

  // Load GeoPHP.
  geophp_load();

  // Set BCMath scale.
  farm_map_set_bcscale();

  // Calculate how wide each rectangle needs to be.
  if (geoPHP::bcmathInstalled()) {
    $total_width = bcsub($bbox['maxy'], $bbox['miny']);
    $geom_width = bcdiv($total_width, $count);
  }
  else {
    $total_width = $bbox['maxy'] - $bbox['miny'];
    $geom_width = $total_width / $count;
  }

  // Fill the bounding box with rectangles.
  $geometries = array();
  $starting_point = new Point($bbox['minx'], $bbox['maxy']);
  for ($i = 1; $i <= $count; $i++) {
    $points = array();
    $points[] = $starting_point;
    if (geoPHP::bcmathInstalled()) {
      $points[] = new Point($bbox['maxx'], bcsub($bbox['maxy'], bcmul($geom_width, $i - 1)));
      $points[] = new Point($bbox['maxx'], bcsub($bbox['maxy'], bcmul($geom_width, $i)));
      $points[] = new Point($bbox['minx'], bcsub($bbox['maxy'], bcmul($geom_width, $i)));
    }
    else {
      $points[] = new Point($bbox['maxx'], $bbox['maxy'] - $geom_width * ($i - 1));
      $points[] = new Point($bbox['maxx'], $bbox['maxy'] - $geom_width * $i);
      $points[] = new Point($bbox['minx'], $bbox['maxy'] - $geom_width * $i);
    }
    $points[] = $starting_point;
    $geometries[] = new Polygon(array(
      new LineString($points),
    ));
    $starting_point = $points[3];
  }

  // Reset BCMath scale.
  farm_map_reset_bcscale();

  // Return the geometries.
  return $geometries;
}

/**
 * Rotate a polygon around an origin.
 *
 * @param $polygon
 *   A Polygon geometry object.
 * @param $origin
 *   An origin point to rotate around.
 * @param $angle
 *   The angle of rotation, in degrees.
 *
 * @return \Polygon
 *   Returns a Polygon geometry object that has been rotated around an origin.
 */
function farm_area_generate_rotate_polygon($polygon, $origin, $angle) {

  // Load GeoPHP.
  geophp_load();

  // If the geometry is not a polygon, bail.
  if ($polygon
    ->geometryType() != 'Polygon' || $polygon->components[0]
    ->geometryType() != 'LineString') {
    return $polygon;
  }

  // Iterate through the polygon's points, and rotate each around the origin.
  $linestring = $polygon->components[0];
  $new_points = array();
  if (!empty($linestring->components)) {
    foreach ($linestring->components as $point) {
      $new_points[] = farm_area_generate_rotate_point($point, $origin, $angle);
    }
  }

  // Return a new Polygon object.
  return new Polygon(array(
    new LineString($new_points),
  ));
}

/**
 * Rotate a point around an origin.
 *
 * @param $point
 *   A Point geometry object.
 * @param $origin
 *   An origin point to rotate around.
 * @param $angle
 *   The angle of rotation, in degrees.
 *
 * @return \Point
 *   Returns a Point geometry object that has been rotated around an origin.
 */
function farm_area_generate_rotate_point($point, $origin, $angle) {

  // Load GeoPHP.
  geophp_load();

  // If $point and $origin are not points, or $angle is not an integer between
  // 0 and 359, bail.
  if ($point
    ->geometryType() != 'Point' || $origin
    ->geometryType() != 'Point' || $angle < 0 || $angle > 359) {
    return $point;
  }

  // Set BCMath scale.
  farm_map_set_bcscale();

  // Convert the angle to radians.
  $angle = deg2rad($angle);

  // Calculate the new rotated points.
  if (geoPHP::bcmathInstalled()) {
    $x = bcadd($origin
      ->x(), bcsub(bcmul(bcsub($point
      ->x(), $origin
      ->x()), cos($angle)), bcmul(bcsub($point
      ->y(), $origin
      ->y()), sin($angle))));
    $y = bcadd($origin
      ->y(), bcadd(bcmul(bcsub($point
      ->x(), $origin
      ->x()), sin($angle)), bcmul(bcsub($point
      ->y(), $origin
      ->y()), cos($angle))));
  }
  else {
    $x = $origin
      ->x() + ($point
      ->x() - $origin
      ->x()) * cos($angle) - ($point
      ->y() - $origin
      ->y()) * sin($angle);
    $y = $origin
      ->y() + ($point
      ->x() - $origin
      ->x()) * sin($angle) + ($point
      ->y() - $origin
      ->y()) * cos($angle);
  }

  // Reset BCMath scale.
  farm_map_reset_bcscale();

  // Return a new Point object.
  return new Point($x, $y);
}

Functions

Namesort descending Description
farm_area_generate_bbox_geometries Generates a set of rectangle geometries running horizontally to fill a bounding box.
farm_area_generate_farm_access_perms Implements hook_farm_access_perms().
farm_area_generate_form Area generator form.
farm_area_generate_form_ajax Ajax callback for farm_area_generate_form().
farm_area_generate_form_preview Farm area generator form preview.
farm_area_generate_form_submit Farm area generate form submit.
farm_area_generate_form_validate Area generator form validate.
farm_area_generate_geometries Generate geometries within a polygon at a given orientation.
farm_area_generate_geos_exists Check to see if GEOS is installed and available.
farm_area_generate_menu Implements hook_menu().
farm_area_generate_page_build Implements hook_page_build().
farm_area_generate_permission Implements hook_permission().
farm_area_generate_rotate_point Rotate a point around an origin.
farm_area_generate_rotate_polygon Rotate a polygon around an origin.