Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for click listeners on GeoJSON layers. #286

Merged
merged 8 commits into from Jun 10, 2016
Expand Up @@ -11,6 +11,7 @@

import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.IOException;
Expand Down Expand Up @@ -83,6 +84,10 @@ private void addColorsToMarkers() {
}
}

private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}

private class DownloadGeoJsonFile extends AsyncTask<String, Void, JSONObject> {

@Override
Expand Down Expand Up @@ -122,6 +127,13 @@ protected void onPostExecute(JSONObject jsonObject) {
// Add the layer onto the map
addColorsToMarkers();
mLayer.addLayerToMap();

mLayer.setOnFeatureClickListener(new GeoJsonLayer.GeoJsonOnFeatureClickListener() {
@Override
public void onFeatureClick(GeoJsonFeature feature) {
showToast("Feature clicked: " + feature.getProperty("title"));
}
});
}
}

Expand Down
83 changes: 83 additions & 0 deletions library/src/com/google/maps/android/geojson/BiMultiMap.java
@@ -0,0 +1,83 @@
package com.google.maps.android.geojson;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
* Extension of HashMap that provides two main features. Firstly it allows reverse lookup
* for a key given a value, by storing a second HashMap internally which maps values to keys.
* Secondly, it supports Collection values, in which case, each item in the collection is
* used as a key in the internal reverse HashMap. It's therefore up to the caller to ensure
* the overall set of values, and collection values, are unique.
*
* Used by GeoJsonRenderer to store GeoJsonFeature instances mapped to corresponding Marker,
* Polyline, and Polygon map objects. We want to look these up in reverse to provide access
* to GeoJsonFeature instances when map objects are clicked.
*/
/* package */ class BiMultiMap<K> extends HashMap<K, Object> {

private final Map<Object, K> mValuesToKeys = new HashMap<>();

@Override
public void putAll(Map<? extends K, ?> map) {
// put() manages the reverse map, so call it on each entry.
for (Entry<? extends K, ?> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}

@Override
public Object put(K key, Object value) {
// Store value/key in the reverse map.
mValuesToKeys.put(value, key);
return super.put(key, value);
}

public Object put(K key, Collection values) {
// Store values/key in the reverse map.
for (Object value : values) {
mValuesToKeys.put(value, key);
}
return super.put(key, values);
}

@Override
public Object remove(Object key) {
Object value = super.remove(key);
// Also remove the value(s) and key from the reverse map.
if (value instanceof Collection) {
for (Object valueItem : (Collection) value) {
mValuesToKeys.remove(valueItem);
}
} else {
mValuesToKeys.remove(value);
}
return value;
}

@Override
public void clear() {
super.clear();
mValuesToKeys.clear();
}

@SuppressWarnings("unchecked")
@Override
public BiMultiMap<K> clone() {
BiMultiMap<K> cloned = new BiMultiMap<>();
cloned.putAll((Map<K, Object>) super.clone());
return cloned;
}

/**
* Reverse lookup of key by value.
*
* @param value Value to lookup
* @return Key for the given value
*/
public K getKey(Object value) {
return mValuesToKeys.get(value);
}

}
88 changes: 88 additions & 0 deletions library/src/com/google/maps/android/geojson/GeoJsonLayer.java
Expand Up @@ -2,6 +2,9 @@

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.Polyline;

import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -70,6 +73,91 @@ public GeoJsonLayer(GoogleMap map, int resourceId, Context context)
this(map, createJsonFileObject(context.getResources().openRawResource(resourceId)));
}

/**
* Sets a single click listener for the entire GoogleMap object, that will be called
* with the corresponding GeoJsonFeature object when an object on the map (Polygon,
* Marker, Polyline) is clicked.
*
* Note that if multiple GeoJsonLayer objects are bound to a GoogleMap object, calling
* setOnFeatureClickListener on one will override the listener defined on the other. In
* that case, you must define each of the GoogleMap click listeners manually
* (OnPolygonClickListener, OnMarkerClickListener, OnPolylineClickListener), and then
* use the GeoJsonLayer.getFeature(mapObject) method on each GeoJsonLayer instance to
* determine if the given mapObject belongs to the layer.
*
* @param listener Listener providing the onFeatureClick method to call.
*/
public void setOnFeatureClickListener(final GeoJsonOnFeatureClickListener listener) {

GoogleMap map = getMap();

map.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() {
@Override
public void onPolygonClick(Polygon polygon) {
listener.onFeatureClick(getFeature(polygon));
}
});

map.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
@Override
public boolean onMarkerClick(Marker marker) {
listener.onFeatureClick(getFeature(marker));
return false;
}
});

map.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() {
@Override
public void onPolylineClick(Polyline polyline) {
listener.onFeatureClick(getFeature(polyline));
}
});

}

/**
* Callback interface for when a GeoJsonLayer's map object is clicked.
*/
public interface GeoJsonOnFeatureClickListener {
void onFeatureClick(GeoJsonFeature feature);
}

/**
* Retrieves a corresponding GeoJsonFeature instance for the given Polygon
* Allows maps with multiple layers to determine which layer the Polygon
* belongs to.
*
* @param polygon Polygon
* @return GeoJsonFeature for the given polygon
*/
public GeoJsonFeature getFeature(Polygon polygon) {
return mRenderer.getFeature(polygon);
}

/**
* Retrieves a corresponding GeoJsonFeature instance for the given Polyline
* Allows maps with multiple layers to determine which layer the Polyline
* belongs to.
*
* @param polyline Polyline
* @return GeoJsonFeature for the given polyline
*/
public GeoJsonFeature getFeature(Polyline polyline) {
return mRenderer.getFeature(polyline);
}

/**
* Retrieves a corresponding GeoJsonFeature instance for the given Marker
* Allows maps with multiple layers to determine which layer the Marker
* belongs to.
*
* @param marker Marker
* @return GeoJsonFeature for the given marker
*/
public GeoJsonFeature getFeature(Marker marker) {
return mRenderer.getFeature(marker);
}

/**
* Takes a character input stream and converts it into a JSONObject
*
Expand Down
26 changes: 18 additions & 8 deletions library/src/com/google/maps/android/geojson/GeoJsonRenderer.java
Expand Up @@ -27,11 +27,7 @@

private final static Object FEATURE_NOT_ON_MAP = null;

/**
* Value is a Marker, Polyline, Polygon or an array of these that have been created from the
* corresponding key
*/
private final HashMap<GeoJsonFeature, Object> mFeatures;
private final BiMultiMap<GeoJsonFeature> mFeatures = new BiMultiMap<>();

private final GeoJsonPointStyle mDefaultPointStyle;

Expand All @@ -50,7 +46,7 @@
*/
/* package */ GeoJsonRenderer(GoogleMap map, HashMap<GeoJsonFeature, Object> features) {
mMap = map;
mFeatures = features;
mFeatures.putAll(features);
mLayerOnMap = false;
mDefaultPointStyle = new GeoJsonPointStyle();
mDefaultLineStringStyle = new GeoJsonLineStringStyle();
Expand Down Expand Up @@ -128,6 +124,16 @@ private static void removeFromMap(Object mapObject) {
return mFeatures.keySet();
}

/**
* Gets a GeoJsonFeature for the given map object, which is a Marker, Polyline or Polygon.
*
* @param mapObject Marker, Polyline or Polygon
* @return GeoJsonFeature for the given map object
*/
/* package */ GeoJsonFeature getFeature(Object mapObject) {
return mFeatures.getKey(mapObject);
}

/**
* Checks for each style in the feature and adds a default style if none is applied
*
Expand Down Expand Up @@ -297,7 +303,9 @@ private Polyline addLineStringToMap(GeoJsonLineStringStyle lineStringStyle,
PolylineOptions polylineOptions = lineStringStyle.toPolylineOptions();
// Add coordinates
polylineOptions.addAll(lineString.getCoordinates());
return mMap.addPolyline(polylineOptions);
Polyline addedPolyline = mMap.addPolyline(polylineOptions);
addedPolyline.setClickable(true);
return addedPolyline;
}

/**
Expand Down Expand Up @@ -333,7 +341,9 @@ private Polygon addPolygonToMap(GeoJsonPolygonStyle polygonStyle, GeoJsonPolygon
i++) {
polygonOptions.addHole(polygon.getCoordinates().get(i));
}
return mMap.addPolygon(polygonOptions);
Polygon addedPolygon = mMap.addPolygon(polygonOptions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the rest of the geometries?

addedPolygon.setClickable(true);
return addedPolygon;
}

/**
Expand Down
@@ -0,0 +1,42 @@
package com.google.maps.android.geojson;

import junit.framework.TestCase;

import java.util.Arrays;
import java.util.List;

public class BiMultiMapTest extends TestCase {

public void testSingle() {
BiMultiMap<String> map = new BiMultiMap<>();
String key = "foo";
String value = "bar";
map.put(key, value);
assertEquals(1, map.size());
assertEquals(value, map.get(key));
assertEquals(key, map.getKey(value));
map.remove(key);
assertEquals(0, map.size());
assertNull(map.get(key));
assertNull(map.getKey(value));
}

public void testMulti() {
BiMultiMap<String> map = new BiMultiMap<>();
String key = "foo";
List<String> values = Arrays.asList("bar", "baz");
map.put(key, values);
assertEquals(1, map.size());
assertEquals(values, map.get(key));
for (String value : values) {
assertEquals(key, map.getKey(value));
}
map.remove(key);
assertEquals(0, map.size());
assertNull(map.get(key));
for (String value : values) {
assertEquals(null, map.getKey(value));
}
}

}