Extending Cordova Google Maps Plugin

July 7th 2017 Cordova Google Maps Android

While you could use Google Maps JavaScript API in Cordova hybrid mobile applications, native Google Maps SDKs for Android and iOS offer much better user experience. There is a plugin available, to use them from Cordova. If you're using Ionic, there's another wrapper for the plugin to make it even easier to use.

However, using the plugin, you're limited to the functionalities it provides and the way it wraps them for use from JavaScript. Although there's a handy Google Maps utility library available for both Android and iOS with additional features such as marker clustering, you cannot use it from the Cordova plugin.

Of course, you could fork the plugin and extend it, but you don't need to. You can take advantage of the plugin's extensibility model instead. Unfortunately, I couldn't find any documentation for it. I had to learn everything by analyzing the source code of the plugin itself and an unfinished prototype extension I stumbled upon.

This post is a short step-by-step guide to getting your own plugin for the Cordova Google Maps plugin up and running.

Java Code

There are a couple of conventions to follow in Java code:

  • The entry class must be placed in the plugin.google.maps package, which would typically be the android/plugin/google/maps folder in the plugin folder structure.
  • The class name must be prefixed with Plugin, i.e. PluginClustering.
  • The class must extend the MyPlugin base class.
  • The entry methods accept 2 arguments: a JSONArray and a CallbackContext. They don't need to be public at all. It will work even with private methods.

Here's a very simple wrapper for the marker clustering functionality:

package plugin.google.maps;

import com.google.maps.android.clustering.ClusterManager;

import org.apache.cordova.CallbackContext;
import org.json.JSONArray;
import org.json.JSONException;

import plugin.google.maps.clustering.MapClusterItem;

public class PluginClustering extends MyPlugin {

  private ClusterManager<MapClusterItem> clusterManager;

  private void createCluster(final JSONArray args,
                             final CallbackContext callbackContext)
    throws JSONException {
    clusterManager = new ClusterManager<MapClusterItem>(cordova.getActivity(), map);
    map.setOnCameraIdleListener(clusterManager);
    map.setOnMarkerClickListener(clusterManager);

    JSONArray markers = args.getJSONArray(1);
    for (int i = 0; i < markers.length(); i++) {
      MapClusterItem item = new MapClusterItem(markers.getJSONObject(i));
      clusterManager.addItem(item);
    }
    clusterManager.cluster();

    callbackContext.success();
  }
}

Except for the JSON parsing, the code is mostly based on the marker clustering documentation. You can also notice my use of cordova.getActivity() function and map field provided by the base class to get the reference to activity and map objects.

The library requires you to implement your own ClusterItem which I've placed in the plugin.google.maps.clustering package. It parses the standard Google Maps plugin marker options:

package plugin.google.maps.clustering;

import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.clustering.ClusterItem;

import org.json.JSONException;
import org.json.JSONObject;

public class MapClusterItem implements ClusterItem {
  private LatLng position;
  private String title;
  private String snippet;

  public MapClusterItem(JSONObject markerOptions) throws JSONException {
    if (markerOptions.has("title")) {
      title = markerOptions.getString("title");
    }

    if (markerOptions.has("snippet")) {
      snippet = markerOptions.getString("snippet");
    }

    JSONObject positionJson = markerOptions.getJSONObject("position");
    position = new LatLng(positionJson.getDouble("lat"),
                          positionJson.getDouble("lng"));
  }

  @Override
  public LatLng getPosition() {
    return position;
  }

  @Override
  public String getTitle() {
    return title;
  }

  @Override
  public String getSnippet() {
    return snippet;
  }
}

JavaScript Code

From JavaScript, you won't invoke your method directly. Instead, you'll use the GoogleMaps plugin exec method which will pass on the call to your class:

function createCluster(markers, success, error) {
  cordova.exec(success, error, 'GoogleMaps', 'exec',
               ['Clustering.createCluster', markers]);
}

The first argument in the call identifies your class (without the Plugin prefix) and method.

The rest of the JavaScript code makes the method available to the Cordova application. I based it on the GoogleMaps plugin JavaScript code:

module.exports = {
  createCluster: createCluster
};

cordova.addConstructor(function() {
  if (!window.Cordova) {
    window.Cordova = cordova;
  };
  window.plugin = window.plugin || {};
  window.plugin.clustering = window.plugin.clustering || module.exports;
});

Manifest File

The manifest file is pretty standard:

  • The js-module element maps the JavaScript file to plugin.clustering.
  • The source-file elements are required for each source file. They specify where they must be copied to in the final Android project.
  • The framework element adds a reference to the Google Maps utility library.
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
        id="plugin.google.maps.clustering" version="0.1">
  <name>plugin-google-maps-clustering</name>
  <license>Apache 2.0</license>

  <js-module src="www/clustering.js" name="clustering">
    <clobbers target="plugin.clustering" />
  </js-module>

  <platform name="android">
    <framework src="com.google.maps.android:android-maps-utils:0.5" />
    <config-file target="res/xml/config.xml" parent="/*">
      <feature name="GoogleMapsClustering">
        <param name="android-package"
               value="plugin.google.maps.Clustering" />
      </feature>
    </config-file>
    <source-file src="android/plugin/google/maps/PluginClustering.java"
                 target-dir="src/plugin/google/maps" />
    <source-file src="android/plugin/google/maps/clustering/MapClusterItem.java"
                 target-dir="src/plugin/google/maps/clustering" />
    </platform>
</plugin>

The only part I'm not really sure about is the feature element. The feature name is usually used in the cordova.exec JavaScript function, but seems unnecessary here since we're invoking the methods through the GoogleMaps plugin. I still left the declaration in the file.

Usage

The plugin first needs to be installed. You don't need to publish the plugin to NPM, you can also pass in a Git URL or a local project subfolder:

cordova platform add ./local-plugins/cordova-plugin-googlemaps-clustering

This will result in the following line being added to Cordova's config.xml file:

<plugin name="plugin.google.maps.clustering"
        spec="./local-plugins/cordova-plugin-googlemaps-clustering" />

In the Cordova application the plugin method will now be available as plugin.clustering.createCluster():

var markers = [
  {
    position: {lat: 46.436705, lng: 14.052606},
    title: 'Jesenice'
  }
  // other markers to be clustered
];
plugin.clustering.createCluster(markers);

Of course, this sample is far from production ready, but it should be enough to base your own Cordova GoogleMaps plugin extension on it.

Copyright
Creative Commons License