You are here

views_geojson_bbox_argument.inc in Views GeoJSON 7

Same filename and directory in other branches
  1. 6 views/views_geojson_bbox_argument.inc

File

views/views_geojson_bbox_argument.inc
View source
<?php

/**
 * @file
 */

/**
 * Custom filter to return only points within a provided bounding box.
 */
class views_geojson_bbox_argument extends views_handler_argument {

  /**
   * Override the default argument form.
   */
  public function default_argument_form(&$form, &$form_state) {
    parent::default_argument_form($form, $form_state);

    // Clarify this, since we're treating pulling from the query string as
    // different than a normal arg.
    $form['no_argument']['#title'] = 'When the filter value is NOT in the URL <em>as a normal Drupal argument</em>';
    $form['empty_result'] = array(
      '#type' => 'checkbox',
      '#title' => t('Empty result on missing bounding box value'),
      '#description' => t('When the argument is not found you may choose to show no result.'),
      '#default_value' => $this->options['empty_result'],
    );
  }

  /**
   * Filter options definition.
   */
  public function option_definition() {
    $options = parent::option_definition();
    $options['arg_id'] = array(
      'default' => 'bbox',
    );
    $options['empty_result'] = array(
      'default' => FALSE,
      'bool' => TRUE,
    );
    $options['bbox_wrap'] = array(
      'default' => TRUE,
      'bool' => TRUE,
    );
    return $options;
  }

  /**
   * Provide additional form options.
   */
  public function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);
    $form['bbox_wrap'] = array(
      '#type' => 'checkbox',
      '#title' => t('Wrap the bounding box around 360 degrees.'),
      '#description' => t('Depending on the projection of the data it is possible that the bounding box wraps around the edges of the map. This requires extra conditions in the views query. By default wrapping is enabled. Turn it of if the provided coordinates never wrap.'),
      '#default_value' => $this->options['bbox_wrap'],
    );
  }

  /**
   * Use the argument to modify the query.
   */
  public function query($group_by = FALSE) {

    // Apply no bbox filtering if the argument is absent or if this is a
    // search_api based view (which doesn't support sql specific query changes).
    if (empty($this->argument) || $this->view->base_field === 'search_api_id') {

      // @TODO: Implement Search API filtering if Solr BBOX is not an option.
      // @see: https://www.drupal.org/project/search_api_location_solr_bboxfield
      return;
    }
    $this
      ->ensure_my_table();

    // Process coordinates: split string and make sure coordinates are clean.
    $bbox = $this
      ->_explode_bbox_coords($this->argument);
    if (empty($bbox)) {

      // Return no values if arg is invalid.
      $this->argument = FALSE;
      $this->view->built = TRUE;
      $this->view->executed = TRUE;
      return;
    }
    if (isset($this->view->display_handler->display->display_options['style_options'])) {
      $data_source = $this->view->display_handler->display->display_options['style_options']['data_source'];
    }
    else {
      $data_source = $this->view->display['default']->display_options['style_options']['data_source'];
    }

    // We handle latlon and geofield data_source types.
    // No support yet for WKT.
    if ($data_source['value'] === 'latlon') {
      $lat_field_obj = $this->view->field[$data_source['latitude']];
      $lon_field_obj = $this->view->field[$data_source['longitude']];
      $lat_field_table = $lat_field_obj->table;
      $lon_field_table = $lon_field_obj->table;
    }
    elseif ($data_source['value'] === 'geofield') {

      // Geofield includes both Lat and Lon fields.
      // We pretend they're distinct fields so we can use same code as for
      // latlon, since we're just going to get lat & lon from geofield anyway.
      $lat_field_obj = $lon_field_obj = $this->view->field[$data_source['geofield']];
      $lat_field_table = $lon_field_table = $lat_field_obj->table;
    }
    else {

      // @TODO: Implement WKT (and possibly other data source types).
      $this->argument = FALSE;
      $this->view->built = TRUE;
      $this->view->executed = TRUE;
      return;
    }

    // If the Latitude / Longitude fields are really geofields,
    // we need to dig a bit deeper to find field names.
    if (isset($lat_field_obj->field_info) && $lat_field_obj->field_info['type'] === 'geofield') {

      // @TODO: Maybe the field name can really just be "${lat_field_obj}_lat"?
      $lat_field_name = $lat_field_obj->field_info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$lat_field_table]['lat'];
    }
    else {
      $lat_field_name = $lat_field_obj->real_field;
    }
    if (isset($lon_field_obj->field_info) && $lon_field_obj->field_info['type'] === 'geofield') {
      $lon_field_name = $lon_field_obj->field_info['storage']['details']['sql']['FIELD_LOAD_CURRENT'][$lon_field_table]['lon'];
    }
    else {
      $lon_field_name = $lon_field_obj->real_field;
    }

    // Add JOIN(s) to query.
    $this->query
      ->ensure_table($lat_field_table);
    if ($lon_field_table !== $lat_field_table) {
      $this->query
        ->ensure_table($lon_field_table);
    }

    // Add WHERE(s) to query.
    $left = $bbox['left'];
    $bottom = $bbox['bottom'];
    $right = $bbox['right'];
    $top = $bbox['top'];
    if ($this->options['bbox_wrap']) {

      // OpenLayers' longitude bbox values can be >180 or <-180 when the map
      // wraps around. We need to turn these into BETWEENs with OR.
      if ($right - $left < 360) {
        $group = $this->query
          ->set_where_group('AND');
        if ($left > -180 && $left <= 180) {
          $this->query
            ->add_where($group, "{$lon_field_table}.{$lon_field_name}", $left, '>=');
        }
        else {
          $this->query
            ->set_where_group('OR', $group);
          $left = ($left + 180) % 360;
          if ($left <= 0) {
            $left += 360;
          }

          // -12 % 7 will result in -5 and not 2 as expected
          $left -= 180;
          $this->query
            ->add_where($group, "{$lon_field_table}.{$lon_field_name}", array(
            $left,
            180,
          ), 'BETWEEN');
        }
        if ($right > -180 && $right <= 180) {
          $this->query
            ->add_where($group, "{$lon_field_table}.{$lon_field_name}", $right, '<=');
        }
        else {
          $this->query
            ->set_where_group('OR', $group);
          $right = ($right + 180) % 360;
          if ($right <= 0) {
            $right += 360;
          }

          // -12 % 7 will result in -5 and not 2 as expected
          $right -= 180;
          $this->query
            ->add_where($group, "{$lon_field_table}.{$lon_field_name}", array(
            -180,
            $right,
          ), 'BETWEEN');
        }
      }
    }
    else {
      $group = $this->query
        ->set_where_group('AND');
      $this->query
        ->add_where($group, "{$lon_field_table}.{$lon_field_name}", $left, '>=');
      $this->query
        ->add_where($group, "{$lon_field_table}.{$lon_field_name}", $right, '<=');
    }
    $group = $this->query
      ->set_where_group('AND');
    $this->query
      ->add_where($group, "{$lat_field_table}.{$lat_field_name}", $bottom, '>=');
    $this->query
      ->add_where($group, "{$lat_field_table}.{$lat_field_name}", $top, '<=');
  }

  /**
   * Check if the BBOX coordinates are valid coordinates.
   *
   * Currently only checks if coordinates are numeric.
   *
   * @TODO: if supported by projection this could include degree checking.
   */
  private function _check_bbox_coords($bbox_coords) {
    foreach ($bbox_coords as $coord) {
      if (!is_numeric($coord)) {
        return FALSE;
      }
    }
    return TRUE;
  }

  /**
   * Split BBOX string into {left, bottom, right, top}.
   *
   * @param string $bbox_coords_str
   *   4 comma delimited coordinates.
   *
   * @return array
   *   Array of 4 valid bbox coordinates or
   *   empty array if coordinates are invalid.
   */
  private function _explode_bbox_coords($bbox_coords_str) {
    if (!is_string($bbox_coords_str)) {
      return array();
    }
    $bbox_coords = explode(',', $bbox_coords_str);
    if (count($bbox_coords) === 4) {
      if (!$this
        ->_check_bbox_coords($bbox_coords)) {
        return array();
      }
      return array(
        'left' => $bbox_coords[0],
        'bottom' => $bbox_coords[1],
        'right' => $bbox_coords[2],
        'top' => $bbox_coords[3],
      );
    }
    return array();
  }

}

Classes

Namesort descending Description
views_geojson_bbox_argument Custom filter to return only points within a provided bounding box.