Drupal 8 custom Google Maps and automatic geocoding with Composer, Drupal Console and Drush

Submitted by christophe on Mon, 09/01/2017 - 18:20
Markers

 

The scope of this article is to illustrate the responsibilities of several tools and techniques used in the Drupal ecosystem (Composer, Drupal Console, Drush, Configuration management) to achieve quickly a simple custom Google Maps use case: an Address field will be defined on a content type and we want the address to be geocoded automatically for each node on save to display a Google Map.
The display of the map will be achieved by a custom module to allow further extension.
In a real world scenario, for such a trivial case, the module would probably be replaced by Simple Google Maps for basic use cases or Geolocation Field for more advanced ones.

The code of the example module is available on GitHub.

 

Install contributed modules (and dependencies) with Composer

We will need 3 contributed modules for this case : Address, Geofield, Geocoder.

They will be installed with Composer, see a previous article for more details about Composer.

The name and the version of each module can be found easily on Drupal.org:

Geofield name

For the version, pick the section after the 8.x-

Geofield version

Check the versions, they evolve quickly, but at the time of writing it looks like the following.
You can still omit the version but check also the minimum-stability in your composer.json, it is by default dev with "prefer-stable": true. Most of the modules are still in alpha, beta or release candidate and you will probably want a non dev release for production.

Type these commands in your terminal at your project docroot (public_html, www, ...).
It will download the modules and their dependencies.

# If not done yet, define the repository for Drupal 8
composer config repositories.drupal composer https://packages.drupal.org/8
# Then download the module
composer require drupal/address:1.0-rc3
composer require drupal/geofield:1.0-alpha2
composer require drupal/geocoder:2.0-alpha5

Then enable the modules (and geocoder sub modules)  with drush, without any validation (-y).

drush en -y address geofield geocoder geocoder_field geocoder_geofield geocoder_address

 

Get a Google Maps API Key

Follow these instructions for getting a Google API Key.
You can focus on the following steps in most cases.

  1. Go to the Google API Console.
  2. Create or select a project.
  3. Click Continue to enable the API and any related services.
  4. On the Credentials page, get an API key (and set the API key restrictions).
    Note: If you have an existing unrestricted API key, or a key with browser restrictions, you may use that key.
  5. To prevent quota theft, secure your API key following these best practices.
  6. (Optional) Enable billing. See Usage Limits for more information.

Just pick the Google Maps JavaScript API in 3 and select the API key as credentials in 4.

Google Maps JavaScript API
Google Maps API key credentials

 

Site building : create your fields and automate geocoding

1. Create the Address field

Add the Address field to your content type and make sure you have chosen the "fields" configuration that will allow geocoding.

Add Address field

Address field configuration

2. Geocode automatically on save

Add the Geofield to your content type, it will store the latitude and longitude.

Add Geofield field

Then choose the following options on the field configuration:

  • Set the Geocode method to Geocode from an exisiting field
  • Select the previously created Address field
  • Define your prefered Geocoder plugins, for example Google Maps and OpenStreetMap

Geofield configuration

 

Now let's test our configuration by creating a new node: fill the Address field and leave the Geofield field empty.

Address and Geofield fields

The Geofield values are automatically being geocoded, sweet!

Geofield values front

Geofield values edit

 

Scaffold your custom module with Drupal Console

Use the generate:module Drupal Console command in your project directory.

# alias for drupal generate:module
drupal gm

In the latest version of Drupal Console, it is now possible to ask for a themeable template. Use the default options after having defined a name for your module. We will only skip the default composer.json and unit test class for this example.

Drupal Console generate module

Generate a Block with Drupal Console

Let's continue by generating a Block Plugin

# alias for drupal generate:plugin:block
drupal gpb

Here, we will still use most of the default options: just reference  your module name, define a class name for your block (e.g. CustomMapBlock).

We will then load a service for fetching other nodes within the block: entity_type.manager.
This will be described in another post.

Then, we will ask for a form structure generation to give a custom description to our block 
It will be used in our template.

Drupal Console generate Plugin Block

Create your own template

We will now edit the my_maps.module file to add variables for our template. Just define the description previously defined while creating the block.

/**
 * Implements hook_theme().
 */
function my_maps_theme() {
  return [
    'my_maps' => [
      'variables' => [
        'description' => null,
      ],
      'template' => 'my_maps',
      'render element' => 'children',
    ],
  ];
}


Then edit the templates/my_maps.html.twig to print your variables and append the map wrapper.

{#
/**
* @file
* Theme implementation of the store map.
*
* @ingroup themeable
*/
#}

<p>{{ description }}</p>
<div id="my-map" style="width:500px; height:500px;"></div>

 

Initialize the map with custom Javascript 

Create a file under js/custom_maps.js and define the init function that will fetch the geolocation object and node title from the drupalSettings that will be passed via your block (see below).

/**
 * @file
 * Attaches behaviors for the custom Google Maps.
 */

(function ($, Drupal) {

    /**
     * Initializes the map.
     */
    function init (geofield, title) {
        //console.log(geofield);
        var point = {lat: geofield.lat, lng: geofield.lon};

        var map = new google.maps.Map(document.getElementById('my-map'), {
            center: point,
            scrollwheel: false,
            zoom: 12
        });

        var infowindow = new google.maps.InfoWindow({
            content: title
        });

        var marker = new google.maps.Marker({
            position: point,
            map: map,
            title: title
        });
        marker.addListener('click', function() {
            infowindow.open(map, marker);
        });
    }

    Drupal.behaviors.customMapBehavior = {
        attach: function (context, settings) {
            init(settings.geofield, settings.title);
        }
    };

})(jQuery, Drupal);


Add your libraries

Create a new file at your module root (close to my_maps.info.yml) : my_maps.libraries.yml
You will define the external Google Maps library with your API key, then the custom Javascript file that was just created.

google.maps:
  version: 3
  js:
    https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY: { type: external, minified: true }

custom_map:
  version: VERSION
  js:
    js/custom_map.js: {}
  dependencies:
    - core/jquery
    - core/drupal
    - my_maps/google.maps

What is interesting here is that the Google Maps library is referenced as a dependency of custom_map, straight from the libraries.yml file, so we will avoid defining two libraries in any #attached declaration, which is much more maintainable (see below).

Attach libraries to the block, get the the result of the geolocation, and pass it to your Javascript

Now, attach the library to the custom block created previously (src/Plugin/Block/CustomMapBlock.php).

Get rid of the Drupal Console generated stub.

Block Drupal Console generated stub

Replace by the following, where we will

  • Get the first value of the Geofield of the current node and its title that will be used by Javascript.
  • Use a render array on the build method that will define the description and the drupalSettings for Javascript.
  // Import the Node class, after the namespace
  use Drupal\node\Entity\Node;

  (...)

  /**
   * {@inheritdoc}
   */
  public function build() {
    $build = [];

    // Get the current node object
    $node = \Drupal::routeMatch()->getParameter('node');
    if ($node instanceof Node) {
      $build = [
        '#theme' => 'my_maps',
        '#description' => $this->configuration['description'],
        '#attached' => array(
          'library' => array(
            'my_maps/custom_map',
          ),
          'drupalSettings' => array(
            // Return the first Geofield value
            'geofield' => $node->get('field_geofield')->getValue()[0],
            'title' => $node->getTitle(),
          ),
        ),
      ];
    }

    return $build;
  }


Place your custom block

It is now time to enable your custom module.

drush en my_maps -y
# Run cache rebuild to make sure that your block and template are available
drush cr

Then head to the block layout page (/admin/structure/block) and place it on the region of your choice for the content type for which you defined the Address and Geofield fields.

Place custom map block

Configure custom map block

 

Et voilà, a map in all its glory!

Map and description

Deploy in production

Export your configuration: it includes the following configuration made via Configuration Entities (but not the contents, that are handled via Content Entities):

  • modules enabled
  • fields added to the content type (and their configuration)
  • the block instanciation and its configuration

A common practice is to move the configuration directory from sites/default/files/config_XXX to an adjacent directory of your docroot. It is safer and will allow you to include the configuration in your VCS (git, svn, ...).
To achieve that, just change the last line of your settings.php file (under sites/default in a single site Drupal installation)

$config_directories['sync'] = 'sites/default/files/config_XXX/sync';
// becomes
$config_directories['sync'] = '../config';

Export on your local environment with drush

# alias for drush config-export
drush cex

Make the config directory available to your staging or production environment (via any continuous integration mechanism) then run import:

# alias for drush config-import
drush cim

 

Going further

The purpose of this article was to expose several Drupal techniques that are becoming standards in Drupal 8. However, you can now, for example, make use of features like client geolocation on custom_map.js, use the entity type manager service to display addresses for other nodes on the same map, ... To be continued in another post.

Comments

I simply install geofield or geolocation field and leaflet module and pick up my lat long from somewhere like Google, Bing or LatLong.net in the mix and viola: I have my map in 2 seconds. I don't understand all the devlopment trouble and server side libraries in the background running and all the hours long tutorials around just for getting the lat long automatically from a restricted and limited service (Google) which is sniffing you and parttime unusable for login based portals with sensitive data where you are not allowed to install such services to protect the users. Sorry. THis is a typical DrupalWTF. Apart from that you can use jquery to get lat long from addresses without the need to manually navigate to the maps for getting lat/long by the way.

I invite you to re-read the title and the introduction. This tutorial is here to expose Composer, Drupal Console and Drush to newcomers, and show the first steps of module development for further extensions. So if you don't need it, just ignore it. Nevertheless, I fully agree on one of your assertion, if there is a way to avoid custom code in favor of contributed modules, just go for it.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.