574 lines
18 KiB
Plaintext
574 lines
18 KiB
Plaintext
<?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);
|
|
}
|