You are here

AddressToGeo.php in Geolocation Address Link 8

File

src/AddressToGeo.php
View source
<?php

namespace Drupal\geolocation_address_link;

use Drupal\geolocation\GeolocationCore;
use CommerceGuys\Addressing\Address;
use CommerceGuys\Addressing\Formatter\PostalLabelFormatter;
use CommerceGuys\Addressing\AddressFormat\AddressFormatRepositoryInterface;
use CommerceGuys\Addressing\Country\CountryRepositoryInterface;
use CommerceGuys\Addressing\Subdivision\SubdivisionRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class AddressToGeo.
 *
 * This class is designed to take an array of address values, convert it to a
 * string value, then geocode the value.
 *
 * The geocoder expects a string value, and it expects values ordered
 * appropriately for the country of the address. Using the PostalLabelFormatter
 * ensures that the string sent to the geocoder is formatted correctly no
 * matter which country the address contains.
 *
 * @package Drupal\geolocation_address_link
 */
class AddressToGeo {

  /**
   * The geocoder manager.
   *
   * @var \Drupal\geolocation\GeolocationCore
   */
  protected $geolocationManager;

  /**
   * The geocoder
   */
  protected $geocoder;

  /**
   * The address format repository.
   *
   * @var \CommerceGuys\Addressing\AddressFormat\AddressFormatRepositoryInterface
   */
  protected $addressFormatRepository;

  /**
   * The country repository.
   *
   * @var \CommerceGuys\Addressing\Country\CountryRepositoryInterface
   */
  protected $countryRepository;

  /**
   * The subdivision repository.
   *
   * @var \CommerceGuys\Addressing\Subdivision\SubdivisionRepositoryInterface
   */
  protected $subdivisionRepository;

  /**
   * The postal label formatter.
   *
   * @var \CommerceGuys\Addressing\Formatter\PostalLabelFormatter;
   */
  protected $formatter;

  /**
   * Format the address as a postal label sent from what country?
   */
  protected $formatCountry;

  /**
   * Format the address in what language?
   */
  protected $formatLanguage;

  /**
   * Constructor.
   */
  public function __construct(GeolocationCore $geolocationManager, AddressFormatRepositoryInterface $address_format_repository, CountryRepositoryInterface $country_repository, SubdivisionRepositoryInterface $subdivision_repository) {
    $this->geolocationManager = $geolocationManager;
    $this->addressFormatRepository = $address_format_repository;
    $this->countryRepository = $country_repository;
    $this->subdivisionRepository = $subdivision_repository;

    // Set up the geocoder and address formatter.
    $this
      ->setGeocoder();
    $this
      ->setFormatCountry();
    $this
      ->setFormatLanguage();
    $this
      ->setFormatter();
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('geolocation.core'), $container
      ->get('address.address_format_repository'), $container
      ->get('address.country_repository'), $container
      ->get('address.subdivision_repository'));
  }

  /**
   * Set the geocoder.
   */
  public function setGeocoder($geocoder = 'google_geocoding_api') {
    $this->geocoder = $this->geolocationManager
      ->getGeocoderManager()
      ->getGeocoder($geocoder);
  }

  /**
   * Set the format country.
   *
   * The country format determines if the country name is appended to
   * a postal address. If set to 'US', the country name would be added for all
   * countries except the USA (it assumes this is a postal label for mail
   * sent from the US). This probably matches the geocoding service's
   * expectations.
   */
  public function setFormatCountry($country = 'US') {
    $this->formatCountry = $country;
  }

  /**
   * Set the format language.
   *
   * The language format is for deciding what language to use to create the
   * address that we send to the geocoding service. The geocoordinates
   * will be the same in any language, so it shouldn't matter.
   */
  public function setFormatLanguage($language = 'en') {
    $this->formatLanguage = $language;
  }

  /**
   * Set formatter.
   *
   * You could override this method to use the DefaultFormatter() instead.
   */
  public function setFormatter() {
    $default_options = [
      'locale' => $this->formatLanguage,
      'origin_country' => $this->formatCountry,
    ];
    $this->formatter = new PostalLabelFormatter($this->addressFormatRepository, $this->countryRepository, $this->subdivisionRepository, $default_options);
  }

  /**
   * Convert an address array into a string address suitable for geocoding.
   *
   * Expects array structured like the Address module as the input values.
   * @see \Drupal\address\Element\Address::applyDefaults().
   */
  public function addressArrayToString(array $address_array) {

    // Make sure the address_array has all values populated.
    $address_array = \Drupal\address\Element\Address::applyDefaults($address_array);

    // Use the Address formatter to create a string ordered appropriately
    // for the country in the address.
    $address = new \CommerceGuys\Addressing\Address();
    $address = $address
      ->withCountryCode($address_array['country_code'])
      ->withPostalCode($address_array['postal_code'])
      ->withAdministrativeArea($address_array['administrative_area'])
      ->withDependentLocality($address_array['dependent_locality'])
      ->withLocality($address_array['locality'])
      ->withAddressLine1($address_array['address_line1'])
      ->withAddressLine2($address_array['address_line2'])
      ->withOrganization($address_array['organization']);
    $address_string = $this->formatter
      ->format($address);

    // Clean up the returned address to turn it into a single line of text.
    $address_string = str_replace("\n", ' ', $address_string);
    $address_string = str_replace("<br>", ' ', $address_string);
    $address_string = strip_tags($address_string);
    return $address_string;
  }

  /**
   * Geocode an address.
   *
   * Note that the returned address may be cleaned up and expanded
   * by the address formatter and the geocoding service.
   *
   * @param mixed address
   *  Either a string address or an associative array using the architecture
   *  provided by the Address module.
   *  @see \Drupal\address\Element\Address::applyDefaults().
   *
   * @return array
   *   'lat': string LATITUDE
   *   'long': string LONGITUDE
   *   'data': array Map settings
   */
  public function geocode($address, $map_size = '400x400') {
    if (is_array($address)) {
      $address = $this
        ->addressArrayToString($address);
    }
    $address = str_replace(' ', '+', $address);
    if ($result = $this->geocoder
      ->geocode($address)) {

      // Store the boundary and address data returned by the geocoding service,
      // and use the boundary to compute a logical zoom setting for this specific
      // location.
      return [
        'lat' => $result['location']['lat'],
        'lng' => $result['location']['lng'],
        'lat_sin' => sin(deg2rad($result['location']['lat'])),
        'lat_cos' => cos(deg2rad($result['location']['lat'])),
        'lng_rad' => deg2rad($result['location']['lng']),
        'data' => [
          'boundary' => $result['boundary'],
          'address' => $result['address'],
          'zoom' => $this
            ->getZoom($result['boundary'], $map_size),
        ],
      ];
    }
    return FALSE;
  }

  /**
   * A method to roughly calculate the right zoom level for a place.
   *
   * Uses the boundary information and an assumption about the pixel width
   * that the map will be displayed at.
   *
   * @param array $boundary
   *   An array of boundary values as returned by Google's geocoding service.
   * @param integer $pixel_width
   *   The estimated pixel width of the map display.
   *
   * @return integer $zoom
   *   A zoom level that will display everything in the boundary box.
   *
   * @see https://stackoverflow.com/questions/6048975/google-maps-v3-how-to-calculate-the-zoom-level-for-a-given-bounds
   */
  public function calcZoom($boundary, $pixel_width = 800) {
    $globe_width = 256;

    // a constant in Google's map projection
    $west = $boundary['lng_south_west'];
    $east = $boundary['lng_north_east'];
    $angle = $east - $west;
    if ($angle < 0) {
      $angle += 360;
    }
    $zoom = round(log($pixel_width * 360 / $angle / $globe_width) / log(2));
    return $zoom;
  }

  /**
   * A method to roughly calculate the right zoom level for a place.
   *
   * Uses the boundary information and an assumption about the pixel width
   * that the map will be displayed at.
   *
   * @param array $boundary
   *   An array of boundary values as returned by Google's geocoding service.
   * @param integer $map_size
   *   The estimated pixel dimensions of the map display.
   *
   * @return integer $zoom
   *   A zoom level that will display everything in the boundary box.
   *
   * @see https://stackoverflow.com/questions/6048975/google-maps-v3-how-to-calculate-the-zoom-level-for-a-given-bounds
   */
  function getZoom($boundary, $map_size = '400x400') {

    // Map dimensions
    $dimensions = explode('x', $map_size);
    $width = $dimensions[0];
    $height = $dimensions[1];

    // The entire world fits in a 256 pixel square at zoom level 0.
    $world_height = 256;
    $world_width = 256;
    $zoom_max = 21;
    $ne_lat = $boundary['lat_north_east'];
    $ne_lng = $boundary['lng_north_east'];
    $sw_lat = $boundary['lat_south_west'];
    $sw_lng = $boundary['lng_south_west'];
    $latFraction = ($this
      ->latRad($ne_lat) - $this
      ->latRad($sw_lat)) / pi();
    $lngDiff = $ne_lng - $sw_lng;
    $lngFraction = ($lngDiff < 0 ? $lngDiff + 360 : $lngDiff) / 360;
    $latZoom = $this
      ->zoom($height, $world_height, $latFraction);
    $lngZoom = $this
      ->zoom($width, $world_width, $lngFraction);
    return min($latZoom, $lngZoom, $zoom_max);
  }

  /**
   * Helper for getZoom().
   */
  function latRad($lat) {
    $sin = sin($lat * pi() / 180);
    $radX2 = log((1 + $sin) / (1 - $sin)) / 2;
    return max(min($radX2, pi()), -pi()) / 2;
  }

  /**
   * Helper for getZoom().
   */
  function zoom($mapPx, $worldPx, $fraction) {
    return floor(log($mapPx / $worldPx / $fraction) / log(2));
  }

}

Classes

Namesort descending Description
AddressToGeo Class AddressToGeo.