diff options
author | Asheem Mamoowala <asheem.mamoowala@mapbox.com> | 2017-10-31 11:45:19 -0700 |
---|---|---|
committer | Asheem Mamoowala <asheem.mamoowala@mapbox.com> | 2017-11-22 13:56:38 -0800 |
commit | eb6de2c434d6cbd5060a9a19cf4f13ec8a792c3b (patch) | |
tree | c8e234957f92ff5f347576fa259f0e120aab743e /platform/android | |
parent | 7c6ede6bb33c6ce3f82b33a9b83dd39dcc31b9ec (diff) | |
download | qtlocation-mapboxgl-eb6de2c434d6cbd5060a9a19cf4f13ec8a792c3b.tar.gz |
[android] Bindings for Custom Geometry Sources
Diffstat (limited to 'platform/android')
9 files changed, 448 insertions, 1 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java index 4fcb91033c..4e22814a2c 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java @@ -231,6 +231,25 @@ public class LatLngBounds implements Parcelable { return new LatLngBounds(latNorth, lonEast, latSouth, lonWest); } + private static double lat_(int z, int y) { + double n = Math.PI - 2.0 * Math.PI * y / Math.pow(2.0, z); + return Math.toDegrees(Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); + } + + private static double lon_(int z, int x) { + return x / Math.pow(2.0, z) * 360.0 - GeoConstants.MAX_LONGITUDE; + } + + /** + * Constructs a LatLngBounds from a Tile identifier. + * @param z Tile zoom level. + * @param x Tile X coordinate. + * @param y Tile Y coordinate. + */ + public static LatLngBounds from(int z, int x, int y) { + return new LatLngBounds(lat_(z, y), lon_(z, x + 1), lat_(z, y + 1), lon_(z, x)); + } + /** * Constructs a LatLngBounds from current bounds with an additional latitude-longitude pair. * diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CustomGeometrySource.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CustomGeometrySource.java new file mode 100644 index 0000000000..e10be1d4ba --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CustomGeometrySource.java @@ -0,0 +1,203 @@ +package com.mapbox.mapboxsdk.style.sources; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; + +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.style.layers.Filter; +import com.mapbox.services.commons.geojson.Feature; +import com.mapbox.services.commons.geojson.FeatureCollection; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Custom Vector Source, allows using FeatureCollections. + * + */ +@UiThread +public class CustomGeometrySource extends Source { + private ExecutorService executor; + private GeometryTileProvider provider; + private final Map<TileID, AtomicBoolean> cancelledTileRequests = new ConcurrentHashMap<>(); + + /** + * Create a CustomGeometrySource + * + * @param id The source id. + * @param provider The tile provider that returns geometry data for this source. + */ + public CustomGeometrySource(String id, GeometryTileProvider provider) { + this(id, provider, new GeoJsonOptions()); + } + + /** + * Create a CustomGeometrySource with non-default GeoJsonOptions. + * <p>Supported options are minZoom, maxZoom, buffer, and tolerance.</p> + * + * @param id The source id. + * @param provider The tile provider that returns geometry data for this source. + * @param options GeoJsonOptions. + */ + public CustomGeometrySource(String id, GeometryTileProvider provider, GeoJsonOptions options) { + this.provider = provider; + executor = Executors.newFixedThreadPool(4); + initialize(this, id, options); + } + + /** + * Invalidate previously provided features within a given bounds at all zoom levels. + * Invoking this method will result in new requests to `GeometryTileProvider` for regions + * that contain, include, or intersect with the provided bounds. + * + * @param bounds The region in which features should be invalidated at all zoom levels + */ + public void invalidateRegion(LatLngBounds bounds) { + nativeInvalidateBounds(bounds); + } + + /** + * Invalidate the geometry contents of a specific tile. Invoking this method will result + * in new requests to `GeometryTileProvider` for visible tiles. + * + * @param zoomLevel Tile zoom level. + * @param x Tile X coordinate. + * @param y Tile Y coordinate. + */ + public void invalidateTile(int zoomLevel, int x, int y) { + nativeInvalidateTile(zoomLevel, x, y); + } + + /** + * Set or update geometry contents of a specific tile. Use this method to update tiles + * for which `GeometryTileProvider` was previously invoked. This method can be called from + * background threads. + * + * @param zoomLevel Tile zoom level. + * @param x Tile X coordinate. + * @param y Tile Y coordinate. + * @param data Feature collection for the tile. + */ + public void setTileData(int zoomLevel, int x, int y, FeatureCollection data) { + nativeSetTileData(zoomLevel, x, y, data); + } + + /** + * Queries the source for features. + * + * @param filter an optional filter statement to filter the returned Features + * @return the features + */ + @NonNull + public List<Feature> querySourceFeatures(@Nullable Filter.Statement filter) { + Feature[] features = querySourceFeatures(filter != null ? filter.toArray() : null); + return features != null ? Arrays.asList(features) : new ArrayList<Feature>(); + } + + protected native void initialize(CustomGeometrySource self, String sourceId, Object options); + + private native Feature[] querySourceFeatures(Object[] filter); + + private native void nativeSetTileData(int z, int x, int y, FeatureCollection data); + + private native void nativeInvalidateTile(int z, int x, int y); + + private native void nativeInvalidateBounds(LatLngBounds bounds); + + @Override + protected native void finalize() throws Throwable; + + private void setTileData(TileID tileId, FeatureCollection data) { + cancelledTileRequests.remove(tileId); + nativeSetTileData(tileId.z, tileId.x, tileId.y, data); + } + + @WorkerThread + private void fetchTile(int z, int x, int y) { + AtomicBoolean cancelFlag = new AtomicBoolean(false); + TileID tileID = new TileID(z, x, y); + cancelledTileRequests.put(tileID, cancelFlag); + GeometryTileRequest request = new GeometryTileRequest(tileID, provider, this, cancelFlag); + executor.submit(request); + } + + @WorkerThread + private void cancelTile(int z, int x, int y) { + AtomicBoolean cancelFlag = cancelledTileRequests.get(new TileID(z, x, y)); + if (cancelFlag != null) { + cancelFlag.compareAndSet(false, true); + } + } + + private static class TileID { + public int z; + public int x; + public int y; + + public TileID(int _z, int _x, int _y) { + z = _z; + x = _x; + y = _y; + } + + public int hashCode() { + return Arrays.hashCode(new int[]{z, x, y}); + } + + public boolean equals(Object object) { + if (object == this) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + if (object instanceof TileID) { + TileID other = (TileID)object; + return this.z == other.z && this.x == other.x && this.y == other.y; + } + return false; + } + } + + private static class GeometryTileRequest implements Runnable { + private TileID id; + private GeometryTileProvider provider; + private WeakReference<CustomGeometrySource> sourceRef; + private AtomicBoolean cancelled; + + public GeometryTileRequest(TileID _id, GeometryTileProvider p, + CustomGeometrySource _source, AtomicBoolean _cancelled) { + id = _id; + provider = p; + sourceRef = new WeakReference<>(_source); + cancelled = _cancelled; + } + + public void run() { + if (isCancelled()) { + return; + } + + FeatureCollection data = provider.getFeaturesForBounds(LatLngBounds.from(id.z, id.x, id.y), id.z); + CustomGeometrySource source = sourceRef.get(); + if (!isCancelled() && source != null && !data.getFeatures().isEmpty()) { + source.setTileData(id, data); + } + } + + private Boolean isCancelled() { + return cancelled.get(); + } + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonOptions.java index 1a1711e547..81f7255b86 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonOptions.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonOptions.java @@ -16,6 +16,17 @@ public class GeoJsonOptions extends HashMap<String, Object> { * @param maxZoom the maximum zoom - Defaults to 18. * @return the current instance for chaining */ + public GeoJsonOptions withMinZoom(int minZoom) { + this.put("minzoom", minZoom); + return this; + } + + /** + * Maximum zoom level at which to create vector tiles (higher means greater detail at high zoom levels). + * + * @param maxZoom the maximum zoom - Defaults to 18. + * @return the current instance for chaining + */ public GeoJsonOptions withMaxZoom(int maxZoom) { this.put("maxzoom", maxZoom); return this; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeometryTileProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeometryTileProvider.java new file mode 100644 index 0000000000..3f1eb315d3 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeometryTileProvider.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.style.sources; + +import android.support.annotation.WorkerThread; + +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.services.commons.geojson.FeatureCollection; + +/** + * Interface that defines methods for working with {@link CustomGeometrySource}. + */ +public interface GeometryTileProvider { + + /*** + * Interface method called by {@link CustomGeometrySource} to request features for a tile. + * + * @param bounds {@link LatLngBounds} of the tile. + * @param zoomLevel Tile zoom level. + * @return Return a @{link FeatureCollection} to be displayed in the requested tile. + */ + @WorkerThread + FeatureCollection getFeaturesForBounds(LatLngBounds bounds, int zoomLevel); +} diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java index bb96c9939d..4eb8e237fd 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java @@ -4,6 +4,7 @@ import android.os.Parcelable; import com.mapbox.mapboxsdk.exceptions.InvalidLatLngBoundsException; import com.mapbox.mapboxsdk.utils.MockParcel; +import com.mapbox.services.android.telemetry.constants.GeoConstants; import org.junit.Before; import org.junit.Test; @@ -272,4 +273,22 @@ public class LatLngBoundsTest { Parcelable parcel = MockParcel.obtain(latLngBounds); assertEquals("Parcel should match original object", parcel, latLngBounds); } + + @Test + public void fromTileID() { + //GeoConstants.MAX_LATITUDE is not defined to a high enough precision + double MAX_LATITUDE = 85.05112877980659; + LatLngBounds bounds = LatLngBounds.from(0, 0, 0); + assertEquals(-GeoConstants.MAX_LONGITUDE, bounds.getLonWest(), DELTA); + assertEquals(-MAX_LATITUDE, bounds.getLatSouth(), DELTA); + assertEquals(GeoConstants.MAX_LONGITUDE, bounds.getLonEast(), DELTA); + assertEquals(MAX_LATITUDE, bounds.getLatNorth(), DELTA); + + bounds = LatLngBounds.from(10, 288, 385); + assertEquals(-78.75, bounds.getLonWest(), DELTA); + assertEquals(40.446947059600497, bounds.getLatSouth(), DELTA); + assertEquals(-78.3984375, bounds.getLonEast(), DELTA); + assertEquals(40.713955826286039, bounds.getLatNorth(), DELTA); + + } } diff --git a/platform/android/config.cmake b/platform/android/config.cmake index b3888a9418..0102d405d7 100644 --- a/platform/android/config.cmake +++ b/platform/android/config.cmake @@ -1,5 +1,4 @@ add_definitions(-DMBGL_USE_GLES2=1) - include(cmake/test-files.cmake) # Build thin archives. @@ -178,6 +177,8 @@ add_library(mbgl-android STATIC platform/android/src/style/layers/unknown_layer.hpp platform/android/src/style/sources/geojson_source.cpp platform/android/src/style/sources/geojson_source.hpp + platform/android/src/style/sources/custom_geometry_source.cpp + platform/android/src/style/sources/custom_geometry_source.hpp platform/android/src/style/sources/source.cpp platform/android/src/style/sources/source.hpp platform/android/src/style/sources/sources.cpp diff --git a/platform/android/src/style/sources/custom_geometry_source.cpp b/platform/android/src/style/sources/custom_geometry_source.cpp new file mode 100644 index 0000000000..e647975e4d --- /dev/null +++ b/platform/android/src/style/sources/custom_geometry_source.cpp @@ -0,0 +1,122 @@ +#include "custom_geometry_source.hpp" + +#include <mbgl/renderer/query.hpp> + +// Java -> C++ conversion +#include "../android_conversion.hpp" +#include "../conversion/filter.hpp" +//#include "../conversion/geojson.hpp" + +// C++ -> Java conversion +#include "../../conversion/conversion.hpp" +#include "../../conversion/collection.hpp" +#include "../../geojson/conversion/feature.hpp" +#include <mbgl/style/conversion/custom_geometry_source_options.hpp> + +#include <string> + +namespace mbgl { +namespace android { + + // This conversion is expected not to fail because it's used only in contexts where + // the value was originally a GeoJsonOptions object on the Java side. If it fails + // to convert, it's a bug in our serialization or Java-side static typing. + static style::CustomGeometrySource::Options convertCustomGeometrySourceOptions(jni::JNIEnv& env, jni::Object<> options, style::TileFunction fetchFn, style::TileFunction cancelFn) { + using namespace mbgl::style::conversion; + if (!options) { + return style::CustomGeometrySource::Options(); + } + Error error; + optional<style::CustomGeometrySource::Options> result = convert<style::CustomGeometrySource::Options>(Value(env, options), error); + if (!result) { + throw std::logic_error(error.message); + } + result->fetchTileFunction = fetchFn; + result->cancelTileFunction = cancelFn; + return *result; + } + + CustomGeometrySource::CustomGeometrySource(jni::JNIEnv& env, jni::Object<CustomGeometrySource> _obj, jni::String sourceId, jni::Object<> options) + : Source(env, std::make_unique<mbgl::style::CustomGeometrySource>( + jni::Make<std::string>(env, sourceId), + convertCustomGeometrySourceOptions(env, + options, + std::bind(&CustomGeometrySource::fetchTile, this, std::placeholders::_1), + std::bind(&CustomGeometrySource::cancelTile, this, std::placeholders::_1))) ), + javaPeer(_obj.NewGlobalRef(env)) { + } + + CustomGeometrySource::~CustomGeometrySource() = default; + + void CustomGeometrySource::fetchTile (const mbgl::CanonicalTileID& tileID) { + android::UniqueEnv _env = android::AttachEnv(); + static auto fetchTile = javaClass.GetMethod<void (jni::jint, jni::jint, jni::jint)>(*_env, "fetchTile"); + assert(javaPeer); + javaPeer->Call(*_env, fetchTile, (int)tileID.z, (int)tileID.x, (int)tileID.y); + }; + + void CustomGeometrySource::cancelTile(const mbgl::CanonicalTileID& tileID) { + android::UniqueEnv _env = android::AttachEnv(); + static auto cancelTile = javaClass.GetMethod<void (jni::jint, jni::jint, jni::jint)>(*_env, "cancelTile"); + assert(javaPeer); + javaPeer->Call(*_env, cancelTile, (int)tileID.z, (int)tileID.x, (int)tileID.y); + }; + + void CustomGeometrySource::setTileData(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, jni::Object<geojson::FeatureCollection> jFeatures) { + using namespace mbgl::android::geojson; + + // Convert the jni object + auto geometry = geojson::FeatureCollection::convert(env, jFeatures); + + // Update the core source + source.as<mbgl::style::CustomGeometrySource>()->CustomGeometrySource::setTileData(CanonicalTileID(z, x, y), GeoJSON(geometry)); + } + + void CustomGeometrySource::invalidateTile(jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y) { + source.as<mbgl::style::CustomGeometrySource>()->CustomGeometrySource::invalidateTile(CanonicalTileID(z, x, y)); + } + void CustomGeometrySource::invalidateBounds(jni::JNIEnv& env, jni::Object<LatLngBounds> jBounds) { + auto bounds = LatLngBounds::getLatLngBounds(env, jBounds); + source.as<mbgl::style::CustomGeometrySource>()->CustomGeometrySource::invalidateRegion(bounds); + } + + jni::Array<jni::Object<geojson::Feature>> CustomGeometrySource::querySourceFeatures(jni::JNIEnv& env, + jni::Array<jni::Object<>> jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + std::vector<mbgl::Feature> features; + if (rendererFrontend) { + features = rendererFrontend->querySourceFeatures(source.getID(), { {}, toFilter(env, jfilter) }); + } + return *convert<jni::Array<jni::Object<Feature>>, std::vector<mbgl::Feature>>(env, features); + } + + jni::Class<CustomGeometrySource> CustomGeometrySource::javaClass; + + jni::jobject* CustomGeometrySource::createJavaPeer(jni::JNIEnv& env) { + static auto constructor = CustomGeometrySource::javaClass.template GetConstructor<jni::jlong>(env); + return CustomGeometrySource::javaClass.New(env, constructor, reinterpret_cast<jni::jlong>(this)); + } + + void CustomGeometrySource::registerNative(jni::JNIEnv& env) { + // Lookup the class + CustomGeometrySource::javaClass = *jni::Class<CustomGeometrySource>::Find(env).NewGlobalRef(env).release(); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod<decltype(MethodPtr), (MethodPtr)>(name) + + // Register the peer + jni::RegisterNativePeer<CustomGeometrySource>( + env, CustomGeometrySource::javaClass, "nativePtr", + std::make_unique<CustomGeometrySource, JNIEnv&, jni::Object<CustomGeometrySource>, jni::String, jni::Object<>>, + "initialize", + "finalize", + METHOD(&CustomGeometrySource::querySourceFeatures, "querySourceFeatures"), + METHOD(&CustomGeometrySource::setTileData, "nativeSetTileData"), + METHOD(&CustomGeometrySource::invalidateTile, "nativeInvalidateTile"), + METHOD(&CustomGeometrySource::invalidateBounds, "nativeInvalidateBounds") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/platform/android/src/style/sources/custom_geometry_source.hpp b/platform/android/src/style/sources/custom_geometry_source.hpp new file mode 100644 index 0000000000..50ced9298a --- /dev/null +++ b/platform/android/src/style/sources/custom_geometry_source.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "source.hpp" +#include <mbgl/style/sources/custom_geometry_source.hpp> +#include <mbgl/util/geojson.hpp> +#include <mbgl/tile/tile_id.hpp> +#include "../../geojson/geometry.hpp" +#include "../../geojson/feature.hpp" +#include "../../geojson/feature_collection.hpp" +#include "../../geometry/lat_lng_bounds.hpp" +#include <jni/jni.hpp> + +namespace mbgl { +namespace android { + +class CustomGeometrySource : public Source { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/CustomGeometrySource"; }; + + static jni::Class<CustomGeometrySource> javaClass; + + static void registerNative(jni::JNIEnv&); + + CustomGeometrySource(jni::JNIEnv&, + jni::Object<CustomGeometrySource>, + jni::String, + jni::Object<>); + + ~CustomGeometrySource(); + + void fetchTile(const mbgl::CanonicalTileID& tileID); + void cancelTile(const mbgl::CanonicalTileID& tileID); + void setTileData(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, jni::Object<geojson::FeatureCollection> jf); + + void invalidateTile(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y); + void invalidateBounds(jni::JNIEnv& env, jni::Object<LatLngBounds> bounds); + + jni::Array<jni::Object<geojson::Feature>> querySourceFeatures(jni::JNIEnv&, + jni::Array<jni::Object<>> ); + + jni::jobject* createJavaPeer(jni::JNIEnv&); + + jni::UniqueObject<CustomGeometrySource> javaPeer; +}; // class CustomGeometrySource + +} // namespace android +} // namespace mbgl diff --git a/platform/android/src/style/sources/sources.cpp b/platform/android/src/style/sources/sources.cpp index 9ab3ca8e84..72d7bcda8c 100644 --- a/platform/android/src/style/sources/sources.cpp +++ b/platform/android/src/style/sources/sources.cpp @@ -12,6 +12,7 @@ #include "raster_source.hpp" #include "unknown_source.hpp" #include "vector_source.hpp" +#include "custom_geometry_source.hpp" namespace { @@ -54,6 +55,7 @@ void registerNativeSources(jni::JNIEnv& env) { RasterSource::registerNative(env); UnknownSource::registerNative(env); VectorSource::registerNative(env); + CustomGeometrySource::registerNative(env); } } |